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 { getSpriteSheet } from "./spritesheet";
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();
export abstract class Animation {
protected ticker: Ticker;
protected boundCallback: TickerCallback<any>;
#resolve: PromiseResolve;
constructor(ticker: Ticker) {
console.log("this got called", ticker);
if (ticker === undefined) throw new Error("got nothing");
this.ticker = ticker;
this.boundCallback = () => {};
constructor(hand: Hand) {
if (hand.ticker === null)
throw new Error("the hand does not have a ticker");
this.ticker = hand.ticker;
this.#resolve = Promise.resolve;
this.boundCallback = () => this.#resolve();
}
abstract onTick(ticker: Ticker): void;
getCallback() {
start() {
return new Promise((resolve) => {
this.#resolve = resolve;
this.boundCallback = this.onTick.bind(this);
return this.boundCallback;
this.ticker.add(this.boundCallback);
});
}
done() {
console.log("removing", this.ticker);
this.ticker.remove(this.boundCallback);
this.#resolve();
}
}
@ -34,27 +45,28 @@ export class FlipCard extends Animation {
cardWidth: number;
status: CardStatus;
flipped: boolean;
to: Card;
constructor(ticker: Ticker, card: Sprite, status: CardStatus) {
super(ticker);
constructor(hand: Hand, index: number) {
super(hand);
this.boundCallback = () => {};
this.card = card;
const { sprite, ...status } = hand.getCard(index);
this.card = sprite;
this.status = status;
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) {
const to = this.status.isFaceDown ? this.status.face : "unknown";
this.status.isFaceDown = !this.status.isFaceDown;
this.card.pivot.set(this.cardWidth / 2, 0);
// the card has just fipped
if (!this.flipped && this.card.width <= 10) {
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
@ -88,8 +100,8 @@ export class ToHorizontal extends Animation {
#containerSizeDelta: [number, number]; // change in size of container
#containerFinalPosition: [number, number]; // final x and y of container
constructor(ticker: Ticker, hand: Hand) {
super(ticker);
constructor(hand: Hand) {
super(hand);
if (hand.size() < 2) throw new Error("cannot spread fewer than two cards");

View File

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

View File

@ -10,7 +10,7 @@ const playerCards: Card[] = [
"nineOfDiamonds",
];
const dealerCards: Card[] = ["unknown", "aceOfHearts"];
const dealerCards: Card[] = ["aceOfClubs", "aceOfHearts"];
(async () => {
try {
@ -113,11 +113,12 @@ const dealerCards: Card[] = ["unknown", "aceOfHearts"];
dealerHand.setGap(CARD_WIDTH * 0.1);
dealerHand.animationTicker(app.ticker);
dealerHand.spread();
playerHand.setGap(CARD_WIDTH * 0.1);
playerHand.animationTicker(app.ticker);
playerHand.spread();
await dealerHand.spread();
await dealerHand.flip(0);
// let ms = 0;
// let elapsed = 0;
@ -141,7 +142,10 @@ const dealerCards: Card[] = ["unknown", "aceOfHearts"];
function positionPlayer(app: Application, h: Hand) {
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");
}
@ -180,6 +184,23 @@ function markTable(app: Application, g: Graphics) {
app.screen.width - CARD_WIDTH,
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({
color: "#ffffff",
width: 8,