diff --git a/client/src/Animation.ts b/client/src/Animation.ts index 76946ad..cf6434d 100644 --- a/client/src/Animation.ts +++ b/client/src/Animation.ts @@ -1,4 +1,4 @@ -import type { Container, Sprite, Ticker, TickerCallback } from "pixi.js"; +import type { Container, Point, Sprite, Ticker, TickerCallback } from "pixi.js"; import { CardStatus, Hand } from "./Hand"; import { getSpriteSheet } from "./spritesheet"; import { CARD_HEIGHT, CARD_WIDTH } from "./constants"; @@ -40,7 +40,7 @@ export abstract class Animation { } } -export class FlipCard extends Animation { +export class Flip extends Animation { card: Sprite; cardWidth: number; status: CardStatus; @@ -91,9 +91,35 @@ export class FlipCard extends Animation { } } +export class Fly extends Animation { + #card: Sprite; + #to: Point; + #cardDelta: number; + + constructor(hand: Hand, index: number, to: Point) { + super(hand); + + this.#card = hand.getCard(index).sprite; + this.#to = to; + this.#cardDelta = distanceBetween(this.#card.position, to); + } + + onTick(ticker: Ticker) { + const [xTravelled, yTravelled] = moveToward( + this.#card, + this.#to, + this.#cardDelta * 0.04 * ticker.deltaTime, + ); + + if (!xTravelled && !yTravelled) { + this.done(); + } + } +} + export class ToHorizontal extends Animation { - #cardDeltas: [number, number][]; // change in x and y of each card - #finalPositions: [number, number][]; // final position of each card (relative to container) + #cardDeltas: number[]; // total distance travelled by each card + #finalPositions: Point[]; // final position of each card (relative to container) #hand: Hand; #cards: Sprite[]; #container: Container; @@ -121,18 +147,10 @@ export class ToHorizontal extends Animation { this.#hand.size(), ); - // calculate the x and y amounts that each card will move over the - // course of the animation - const deltas = []; + // calculate the total distances between each starting and ending point + const deltas: number[] = []; for (let i = 0; i < currentPositions.length; i++) { - const [currentX, currentY] = currentPositions[i]; - const [finalX, finalY] = finalPositions[i]; - - const adjustment: [number, number] = [ - finalX - currentX, - finalY - currentY, - ]; - deltas.push(adjustment); + deltas.push(distanceBetween(currentPositions[i], finalPositions[i])); } this.#finalPositions = finalPositions; @@ -154,8 +172,8 @@ export class ToHorizontal extends Animation { // of the container after the animation const initialWidth = this.#cards[count - 1].x + CARD_WIDTH; const initialHeight = this.#cards[count - 1].y + CARD_HEIGHT; - const finalWidth = finalPositions[count - 1][0] + CARD_WIDTH; - const finalHeight = finalPositions[count - 1][1] + CARD_HEIGHT; + const finalWidth = finalPositions[count - 1].x + CARD_WIDTH; + const finalHeight = finalPositions[count - 1].y + CARD_HEIGHT; // the containers change in width and height over the course of the // animation @@ -183,58 +201,22 @@ export class ToHorizontal extends Animation { // https://pixijs.download/release/docs/ticker.Ticker.html#deltatime const rate = 0.04; - for (let i = 0; i < this.#cardDeltas.length; i++) { + for (let i = 0; i < this.#cards.length; i++) { const card = this.#cards[i]; // x, y coordinates where the card will end up - const [finalX, finalY] = this.#finalPositions[i]; + const to = this.#finalPositions[i]; - // x, y distances travelled by the end of the animation - const [deltaX, deltaY] = this.#cardDeltas[i]; - - // These are the x, y distances remaining between where the - // card is in the animation, and where it will end up. They are - // going to be used to determine if the current adjustment - // should be the final adjustment. - const remainingX = this.#cards[i].x - this.#finalPositions[i][0]; - const remainingY = this.#cards[i].y - this.#finalPositions[i][1]; - - // These are the numbers of pixels that should be travelled this - // tick. They are calculated by taking the total travel - // distance (deltaX), taking a percentage of that journey that - // should be taken in a given target frame (rate), and then - // multiplying that by ticker.deltaTime to get the fraction of - // the target framerate that has actually elapsed (ideally, 1.0). - // - // For more information about ticker.deltaTime, see: - // https://pixijs.download/release/docs/ticker.Ticker.html#deltatime - const offsetX = deltaX * rate * ticker.deltaTime; - const offsetY = deltaY * rate * ticker.deltaTime; - - // If the remaining x distance is less than, or equal to the offset, - // then this is the last adjustment and, instead of offsetting, the - // card should just be set to its desired final position... - if (Math.abs(remainingX) <= Math.abs(offsetX)) { - card.x = finalX; - } - - // ...otherwise, apply the offset and set isXAnimating to true since - // there is yet more animating along x to do! - else { - card.x += offsetX; - isXAnimating = true; - } - - // and for y - if (Math.abs(remainingY) <= Math.abs(offsetY)) { - card.y = finalY; - } else { - card.y += offsetY; - isYAnimating = true; - } + const [xTravelled, yTravelled] = moveToward( + card, + to, + this.#cardDeltas[i] * rate * ticker.deltaTime, + ); + isXAnimating = !!xTravelled; + isYAnimating = !!yTravelled; } - // When neither x nor y still need to be animated, the animation is .complete + // When neither x nor y still need to be animated, the animation is complete if (!isXAnimating && !isYAnimating) { this.#hand.setCardLayout("horizontal"); // the hand layout has changed @@ -259,3 +241,49 @@ export class ToHorizontal extends Animation { } } } + +function moveToward( + sprite: Sprite, + to: Point, + distance: number, +): [number, number] { + // x, y coordinates of the current and destination points. + const { x: toX, y: toY } = to; + const { x: fromX, y: fromY } = sprite.position; + + const rise = toY - fromY; + const run = toX - fromX; + + // If we imagine that rise and run are sides of a right triangle, then + // the total travel distance is the hypotenuse of that triangle. Boom. + const totalDistance = distanceBetween(sprite.position, to); + + if (totalDistance <= distance) { + const travelled: [number, number] = [toX - sprite.x, toY - sprite.y]; + sprite.x = toX; + sprite.y = toY; + + return travelled; + } + + // The proportion of the total distance travelled. + const distanceProportion = distance / totalDistance; + + const yDist = rise * distanceProportion; + const xDist = run * distanceProportion; + + sprite.x += xDist; + sprite.y += yDist; + + return [xDist, yDist]; +} + +function distanceBetween(a: Point, b: Point) { + // If we imagine that rise and run are sides of a right triangle, then + // the total travel distance is the hypotenuse of that triangle. Boom. + const rise = b.y - a.y; + const run = b.y - a.y; + const totalDistance = Math.sqrt(Math.pow(rise, 2) + Math.pow(run, 2)); + + return totalDistance; +} diff --git a/client/src/Hand.ts b/client/src/Hand.ts index 4acb2c0..96eb16b 100644 --- a/client/src/Hand.ts +++ b/client/src/Hand.ts @@ -1,7 +1,7 @@ -import { Assets, Container, Sprite, Spritesheet, Ticker } from "pixi.js"; +import { Assets, Container, Point, Sprite, Spritesheet, Ticker } from "pixi.js"; import { Card } from "./Card"; -import { CARD_HOVER_DIST, CARD_WIDTH } from "./constants"; -import { FlipCard, ToHorizontal } from "./Animation"; +import { CARD_HEIGHT, CARD_HOVER_DIST, CARD_WIDTH } from "./constants"; +import { Flip, Fly, ToHorizontal } from "./Animation"; type Pivot = "left" | "right" | "center"; type Layout = "horizontal" | "ascending" | "stacked"; @@ -19,18 +19,16 @@ export class Hand { #hover: boolean; #layout: Layout; #maxWidth: number; + #maxHeight: number; #pivot: Pivot; ticker: Ticker | null; #gap: number; - constructor(maxWidth: number, cards: Card[] = []) { - if (maxWidth < CARD_WIDTH) { - throw new Error("hand cannot be narrower than a single card"); - } - + constructor(cards: Card[] = []) { this.#container = new Container(); this.#hover = false; - this.#maxWidth = maxWidth; + this.#maxWidth = Infinity; + this.#maxHeight = Infinity; this.#pivot = "left"; this.#gap = 0; this.#layout = "horizontal"; @@ -55,9 +53,11 @@ export class Hand { } } - add(card: Card, faceDown = false) { + async add(card: Card, faceDown = false, from: Point | null = null) { const spriteName = faceDown ? "unknown" : card; const sprite = new Sprite(spritesheet.textures[spriteName]); + sprite.width = CARD_WIDTH; + sprite.height = CARD_HEIGHT; this.#cardSprites.push(sprite); this.#statuses.push({ @@ -68,6 +68,19 @@ export class Hand { this.#setCardHover(sprite); this.fanCards(); this.setPivot(this.#pivot); + + if (from) { + const to = sprite.position.clone(); + const globalPosition = sprite.toGlobal(new Point()); + const { x: fromX, y: fromY } = from; + sprite.x -= globalPosition.x; + sprite.y -= globalPosition.y; + sprite.y += fromY; + sprite.x += fromX - CARD_WIDTH / 2; + + const animation = new Fly(this, this.#cardSprites.length - 1, to); + await animation.start(); + } } animationTicker(ticker: Ticker) { @@ -85,7 +98,7 @@ export class Hand { if (!this.ticker) throw new Error("can't animate hand before passing it a ticker"); - const animation = new FlipCard(this, cardIdx); + const animation = new Flip(this, cardIdx); await animation.start(); } @@ -134,6 +147,14 @@ export class Hand { return this.#layout; } + setMaxWidth(maxWidth: number) { + this.#maxWidth = maxWidth; + } + + setMaxHeight(maxHeight: number) { + this.#maxHeight = maxHeight; + } + hover(hasHover: boolean) { if (this.#hover === hasHover) return; @@ -171,28 +192,41 @@ export class Hand { } getCardPositions(layout: Layout, cardCount: number) { - const positions: [number, number][] = []; - const max = this.#maxWidth; - let offset = CARD_WIDTH / 3; + const positions: Point[] = []; + let horizontalOffset = CARD_WIDTH / 3; + let verticalOffset = horizontalOffset; if (layout === "horizontal") { - offset = - cardCount * (CARD_WIDTH + this.#gap) > max - ? max / cardCount - : CARD_WIDTH + this.#gap; + const fullCardWidth = CARD_WIDTH + this.#gap; + const fullHandWidth = cardCount * (CARD_WIDTH + this.#gap); + + // the sum of all the parts of card that peek out of a hand and are visible + // to the player + const cardPeek = (this.#maxWidth - CARD_WIDTH) / (cardCount - 1); + + horizontalOffset = + fullHandWidth > this.#maxWidth ? cardPeek : fullCardWidth; + } + + if (layout === "ascending") { + const fullHandHeight = CARD_HEIGHT + (cardCount - 1) * verticalOffset; + if (fullHandHeight > this.#maxHeight) { + verticalOffset = (this.#maxHeight - CARD_HEIGHT) / (cardCount - 1); + console.log("max height", this.#maxHeight, "offset", verticalOffset); + } } let x = 0; let y = 0; for (let i = 0; i < cardCount; i++) { - positions.push([x, y]); + positions.push(new Point(x, y)); switch (layout) { case "ascending": - x += offset; - y -= offset; + x += horizontalOffset; + y -= verticalOffset; break; case "horizontal": - x += offset; + x += horizontalOffset; break; case "stacked": x += CARD_WIDTH / 20; @@ -210,7 +244,7 @@ export class Hand { ); for (let i = 0; i < positions.length; i++) { - const [x, y] = positions[i]; + const { x, y } = positions[i]; const card = this.#cardSprites[i]; card.x = x; card.y = y; diff --git a/client/src/main.ts b/client/src/main.ts index c29c220..794acaa 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -1,4 +1,4 @@ -import { Application, Graphics, Text, TextStyleOptions } from "pixi.js"; +import { Application, Graphics, Point, Text, TextStyleOptions } from "pixi.js"; import { Hand } from "./Hand"; import { Card } from "./Card"; import { CARD_HEIGHT, CARD_HOVER_DIST, CARD_WIDTH } from "./constants"; @@ -8,6 +8,8 @@ const playerCards: Card[] = [ "sixOfClubs", "kingOfClubs", "nineOfDiamonds", + "sixOfHearts", + "sevenOfHearts", ]; const dealerCards: Card[] = ["aceOfClubs", "aceOfHearts"]; @@ -66,8 +68,11 @@ const dealerCards: Card[] = ["aceOfClubs", "aceOfHearts"]; style: insuranceStyle, }); - const playerHand = new Hand(350, []); - const dealerHand = new Hand(350, []); + const playerHand = new Hand(); + playerHand.setMaxWidth(CARD_WIDTH * 3); + playerHand.setMaxHeight(CARD_HEIGHT * 1.5); + + const dealerHand = new Hand(); const positionElements = () => { markTable(app, tableMarkings); @@ -101,24 +106,26 @@ const dealerCards: Card[] = ["aceOfClubs", "aceOfHearts"]; positionElements(); }); - for (const card of playerCards) { - playerHand.add(card, false); - } - - let firstCard = true; - for (const card of dealerCards) { - dealerHand.add(card, firstCard); - firstCard = false; - } - dealerHand.setGap(CARD_WIDTH * 0.1); dealerHand.animationTicker(app.ticker); playerHand.setGap(CARD_WIDTH * 0.1); playerHand.animationTicker(app.ticker); - await dealerHand.spread(); - await dealerHand.flip(0); + for (const card of playerCards) { + await playerHand.add(card, false, new Point(app.screen.width / 2, 0)); + } + + let firstCard = true; + for (const card of dealerCards) { + await dealerHand.add(card, firstCard, new Point(app.screen.width / 2, 0)); + firstCard = false; + } + + // await dealerHand.spread(); + // await dealerHand.flip(0); + + // await playerHand.spread(); // let ms = 0; // let elapsed = 0;