refactor animations to be promise based

This commit is contained in:
2025-11-09 18:34:19 -08:00
parent f9a7d5b3cc
commit cb7230124c
3 changed files with 79 additions and 57 deletions

View File

@ -2,30 +2,41 @@ import type { Container, Sprite, Ticker, TickerCallback } from "pixi.js";
import { CardStatus, Hand } from "./Hand"; import { CardStatus, Hand } from "./Hand";
import { getSpriteSheet } from "./spritesheet"; import { getSpriteSheet } from "./spritesheet";
import { CARD_HEIGHT, CARD_WIDTH } from "./constants"; import { CARD_HEIGHT, CARD_WIDTH } from "./constants";
import { Card } from "./Card";
// I can't find an actual type for the promise resolution callback, but this is what
// it should look like.
type PromiseResolve = (value?: unknown) => void;
const spritesheet = await getSpriteSheet(); const spritesheet = await getSpriteSheet();
export abstract class Animation { export abstract class Animation {
protected ticker: Ticker; protected ticker: Ticker;
protected boundCallback: TickerCallback<any>; protected boundCallback: TickerCallback<any>;
#resolve: PromiseResolve;
constructor(ticker: Ticker) { constructor(hand: Hand) {
console.log("this got called", ticker); if (hand.ticker === null)
if (ticker === undefined) throw new Error("got nothing"); throw new Error("the hand does not have a ticker");
this.ticker = ticker;
this.boundCallback = () => {}; this.ticker = hand.ticker;
this.#resolve = Promise.resolve;
this.boundCallback = () => this.#resolve();
} }
abstract onTick(ticker: Ticker): void; abstract onTick(ticker: Ticker): void;
getCallback() { start() {
return new Promise((resolve) => {
this.#resolve = resolve;
this.boundCallback = this.onTick.bind(this); this.boundCallback = this.onTick.bind(this);
return this.boundCallback; this.ticker.add(this.boundCallback);
});
} }
done() { done() {
console.log("removing", this.ticker);
this.ticker.remove(this.boundCallback); this.ticker.remove(this.boundCallback);
this.#resolve();
} }
} }
@ -34,27 +45,28 @@ export class FlipCard extends Animation {
cardWidth: number; cardWidth: number;
status: CardStatus; status: CardStatus;
flipped: boolean; flipped: boolean;
to: Card;
constructor(ticker: Ticker, card: Sprite, status: CardStatus) { constructor(hand: Hand, index: number) {
super(ticker); super(hand);
this.boundCallback = () => {}; const { sprite, ...status } = hand.getCard(index);
this.card = card; this.card = sprite;
this.status = status; this.status = status;
this.flipped = false; this.flipped = false;
this.cardWidth = card.width; this.cardWidth = sprite.width;
this.to = this.status.isFaceDown ? this.status.face : "unknown";
this.card.pivot.set(this.cardWidth / 2, 0);
this.card.x += this.cardWidth / 2; // moving the pivot will shift the card
} }
onTick(ticker: Ticker) { onTick(ticker: Ticker) {
const to = this.status.isFaceDown ? this.status.face : "unknown";
this.status.isFaceDown = !this.status.isFaceDown; this.status.isFaceDown = !this.status.isFaceDown;
this.card.pivot.set(this.cardWidth / 2, 0);
// the card has just fipped // the card has just fipped
if (!this.flipped && this.card.width <= 10) { if (!this.flipped && this.card.width <= 10) {
this.flipped = true; this.flipped = true;
this.card.texture = spritesheet.textures[to]; this.card.texture = spritesheet.textures[this.to];
} }
// the other side of the card is how showing // the other side of the card is how showing
@ -88,8 +100,8 @@ export class ToHorizontal extends Animation {
#containerSizeDelta: [number, number]; // change in size of container #containerSizeDelta: [number, number]; // change in size of container
#containerFinalPosition: [number, number]; // final x and y of container #containerFinalPosition: [number, number]; // final x and y of container
constructor(ticker: Ticker, hand: Hand) { constructor(hand: Hand) {
super(ticker); super(hand);
if (hand.size() < 2) throw new Error("cannot spread fewer than two cards"); if (hand.size() < 2) throw new Error("cannot spread fewer than two cards");

View File

@ -1,14 +1,7 @@
import { import { Assets, Container, Sprite, Spritesheet, Ticker } from "pixi.js";
Assets,
Container,
Sprite,
Spritesheet,
Ticker,
TickerCallback,
} from "pixi.js";
import { Card } from "./Card"; import { Card } from "./Card";
import { CARD_HOVER_DIST, CARD_WIDTH } from "./constants"; import { CARD_HOVER_DIST, CARD_WIDTH } from "./constants";
import { Animation, FlipCard, ToHorizontal } from "./Animation"; import { FlipCard, ToHorizontal } from "./Animation";
type Pivot = "left" | "right" | "center"; type Pivot = "left" | "right" | "center";
type Layout = "horizontal" | "ascending" | "stacked"; type Layout = "horizontal" | "ascending" | "stacked";
@ -16,12 +9,10 @@ type Layout = "horizontal" | "ascending" | "stacked";
const spritesheet = await Assets.load<Spritesheet>("/public/assets/cards.json"); const spritesheet = await Assets.load<Spritesheet>("/public/assets/cards.json");
export type CardStatus = { export type CardStatus = {
face: Card; face: Card;
isAnimating: boolean;
isFaceDown: boolean; isFaceDown: boolean;
}; };
export class Hand { export class Hand {
#animations: Animation[];
#cardSprites: Sprite[]; #cardSprites: Sprite[];
#statuses: CardStatus[]; #statuses: CardStatus[];
#container: Container; #container: Container;
@ -29,7 +20,7 @@ export class Hand {
#layout: Layout; #layout: Layout;
#maxWidth: number; #maxWidth: number;
#pivot: Pivot; #pivot: Pivot;
#ticker: Ticker | null; ticker: Ticker | null;
#gap: number; #gap: number;
constructor(maxWidth: number, cards: Card[] = []) { constructor(maxWidth: number, cards: Card[] = []) {
@ -37,7 +28,6 @@ export class Hand {
throw new Error("hand cannot be narrower than a single card"); throw new Error("hand cannot be narrower than a single card");
} }
this.#animations = [];
this.#container = new Container(); this.#container = new Container();
this.#hover = false; this.#hover = false;
this.#maxWidth = maxWidth; this.#maxWidth = maxWidth;
@ -49,7 +39,7 @@ export class Hand {
isAnimating: false, isAnimating: false,
isFaceDown: false, isFaceDown: false,
})); }));
this.#ticker = null; this.ticker = null;
if (cards.length > 0) { if (cards.length > 0) {
const sprites: Sprite[] = []; const sprites: Sprite[] = [];
@ -66,12 +56,12 @@ export class Hand {
} }
add(card: Card, faceDown = false) { add(card: Card, faceDown = false) {
const sprite = new Sprite(spritesheet.textures[card]); const spriteName = faceDown ? "unknown" : card;
const sprite = new Sprite(spritesheet.textures[spriteName]);
this.#cardSprites.push(sprite); this.#cardSprites.push(sprite);
this.#statuses.push({ this.#statuses.push({
face: card, face: card,
isAnimating: false,
isFaceDown: faceDown, isFaceDown: faceDown,
}); });
this.#container.addChild(sprite); this.#container.addChild(sprite);
@ -81,42 +71,34 @@ export class Hand {
} }
animationTicker(ticker: Ticker) { animationTicker(ticker: Ticker) {
this.#ticker = ticker; this.ticker = ticker;
ticker.add((ticker) => {
if (this.#animations.length) {
const animation = this.#animations.shift()!;
ticker.add(animation.getCallback());
}
});
} }
setGap(gap: number) { setGap(gap: number) {
this.#gap = gap; this.#gap = gap;
} }
flip(cardIdx: number) { async flip(cardIdx: number) {
if (cardIdx >= this.#cardSprites.length) if (cardIdx >= this.#cardSprites.length)
throw new Error(`card index out of range: ${cardIdx}`); throw new Error(`card index out of range: ${cardIdx}`);
if (!this.#ticker) if (!this.ticker)
throw new Error("can't animate hand before passing it a ticker"); throw new Error("can't animate hand before passing it a ticker");
const card = this.#cardSprites[cardIdx]; const animation = new FlipCard(this, cardIdx);
const status = this.#statuses[cardIdx]; await animation.start();
const ticker = this.#ticker;
this.#animations.push(new FlipCard(ticker, card, status));
} }
size(): number { size(): number {
return this.#cardSprites.length; return this.#cardSprites.length;
} }
spread() { async spread() {
if (!this.#ticker) if (!this.ticker)
throw new Error("can't animate hand before passing it a ticker"); throw new Error("can't animate hand before passing it a ticker");
this.#animations.push(new ToHorizontal(this.#ticker, this)); const animation = new ToHorizontal(this);
await animation.start();
} }
#setCardHover(sprite: Sprite) { #setCardHover(sprite: Sprite) {
@ -141,6 +123,13 @@ export class Hand {
return this.#cardSprites; return this.#cardSprites;
} }
getCard(index: number) {
if (this.#cardSprites.length <= index)
throw new Error(`card index ${index} out of bounds`);
return { sprite: this.#cardSprites[index], ...this.#statuses[index] };
}
getLayout() { getLayout() {
return this.#layout; return this.#layout;
} }

View File

@ -10,7 +10,7 @@ const playerCards: Card[] = [
"nineOfDiamonds", "nineOfDiamonds",
]; ];
const dealerCards: Card[] = ["unknown", "aceOfHearts"]; const dealerCards: Card[] = ["aceOfClubs", "aceOfHearts"];
(async () => { (async () => {
try { try {
@ -113,11 +113,12 @@ const dealerCards: Card[] = ["unknown", "aceOfHearts"];
dealerHand.setGap(CARD_WIDTH * 0.1); dealerHand.setGap(CARD_WIDTH * 0.1);
dealerHand.animationTicker(app.ticker); dealerHand.animationTicker(app.ticker);
dealerHand.spread();
playerHand.setGap(CARD_WIDTH * 0.1); playerHand.setGap(CARD_WIDTH * 0.1);
playerHand.animationTicker(app.ticker); playerHand.animationTicker(app.ticker);
playerHand.spread();
await dealerHand.spread();
await dealerHand.flip(0);
// let ms = 0; // let ms = 0;
// let elapsed = 0; // let elapsed = 0;
@ -141,7 +142,10 @@ const dealerCards: Card[] = ["unknown", "aceOfHearts"];
function positionPlayer(app: Application, h: Hand) { function positionPlayer(app: Application, h: Hand) {
h.setPivot("center"); h.setPivot("center");
h.setPosition(app.screen.width / 2, CARD_HEIGHT * 3); h.setPosition(
app.screen.width / 2,
CARD_HEIGHT * 2.15 + 4 + CARD_HEIGHT / 2.5 + CARD_WIDTH,
);
h.setCardLayout("ascending"); h.setCardLayout("ascending");
} }
@ -180,6 +184,23 @@ function markTable(app: Application, g: Graphics) {
app.screen.width - CARD_WIDTH, app.screen.width - CARD_WIDTH,
CARD_HEIGHT / 2.5, CARD_HEIGHT / 2.5,
); );
g.stroke({
color: "#ffffff",
width: 8,
});
g.circle(
app.screen.width / 2,
CARD_HEIGHT * 2.15 +
4 +
CARD_HEIGHT / 2.5 +
CARD_WIDTH +
CARD_HEIGHT +
CARD_WIDTH,
CARD_HEIGHT / 2,
);
g.stroke({ g.stroke({
color: "#ffffff", color: "#ffffff",
width: 8, width: 8,