add ToHorizontal animation
This commit is contained in:
@ -1,31 +1,31 @@
|
||||
import type { Sprite, Ticker, TickerCallback } from "pixi.js";
|
||||
import { CardStatus } from "./Hand";
|
||||
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";
|
||||
|
||||
const spritesheet = await getSpriteSheet();
|
||||
|
||||
export abstract class Animation {
|
||||
protected ticker: Ticker;
|
||||
protected cb: TickerCallback<any>;
|
||||
#next: Animation | null;
|
||||
protected boundCallback: TickerCallback<any>;
|
||||
|
||||
constructor(ticker: Ticker) {
|
||||
console.log("this got called", ticker);
|
||||
if (ticker === undefined) throw new Error("got nothing");
|
||||
this.ticker = ticker;
|
||||
this.cb = () => {};
|
||||
this.#next = null;
|
||||
this.boundCallback = () => {};
|
||||
}
|
||||
|
||||
abstract getCallback(): TickerCallback<any>;
|
||||
abstract onTick(ticker: Ticker): void;
|
||||
|
||||
next(animation: Animation) {
|
||||
this.#next = animation;
|
||||
getCallback() {
|
||||
this.boundCallback = this.onTick.bind(this);
|
||||
return this.boundCallback;
|
||||
}
|
||||
|
||||
done() {
|
||||
console.log("removing", this.ticker);
|
||||
this.ticker.remove(this.cb);
|
||||
this.ticker.remove(this.boundCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,20 +38,17 @@ export class FlipCard extends Animation {
|
||||
constructor(ticker: Ticker, card: Sprite, status: CardStatus) {
|
||||
super(ticker);
|
||||
|
||||
this.cb = () => {};
|
||||
this.boundCallback = () => {};
|
||||
this.card = card;
|
||||
this.status = status;
|
||||
this.flipped = false;
|
||||
this.cardWidth = card.width;
|
||||
}
|
||||
|
||||
getCallback() {
|
||||
const callback = (ticker: Ticker) => {
|
||||
onTick(ticker: Ticker) {
|
||||
const to = this.status.isFaceDown ? this.status.face : "unknown";
|
||||
this.status.isFaceDown = !this.status.isFaceDown;
|
||||
|
||||
console.log(this);
|
||||
|
||||
this.card.pivot.set(this.cardWidth / 2, 0);
|
||||
|
||||
// the card has just fipped
|
||||
@ -79,10 +76,174 @@ export class FlipCard extends Animation {
|
||||
console.log("remove", this.flipped, this.card.width, this.cardWidth);
|
||||
super.done();
|
||||
}
|
||||
};
|
||||
|
||||
this.cb = callback.bind(this);
|
||||
|
||||
return this.cb;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
#hand: Hand;
|
||||
#cards: Sprite[];
|
||||
#container: Container;
|
||||
#containerSizeDelta: [number, number]; // change in size of container
|
||||
#containerFinalPosition: [number, number]; // final x and y of container
|
||||
|
||||
constructor(ticker: Ticker, hand: Hand) {
|
||||
super(ticker);
|
||||
|
||||
if (hand.size() < 2) throw new Error("cannot spread fewer than two cards");
|
||||
|
||||
this.#hand = hand;
|
||||
this.#cards = this.#hand.getCardSprites();
|
||||
this.#container = hand.getContainer();
|
||||
|
||||
// figure out the current positions of each card
|
||||
const currentPositions = this.#hand.getCardPositions(
|
||||
this.#hand.getLayout(),
|
||||
this.#hand.size(),
|
||||
);
|
||||
|
||||
// figure out where each card would be if the layout was horizontal
|
||||
const finalPositions = this.#hand.getCardPositions(
|
||||
"horizontal",
|
||||
this.#hand.size(),
|
||||
);
|
||||
|
||||
// calculate the x and y amounts that each card will move over the
|
||||
// course of the animation
|
||||
const deltas = [];
|
||||
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);
|
||||
}
|
||||
|
||||
this.#finalPositions = finalPositions;
|
||||
this.#cardDeltas = deltas;
|
||||
|
||||
// The container will almost certainly change its size over the course
|
||||
// of the animation. This will cause the container to no longer be
|
||||
// aligned properly. For each frame where cards are adjusted, it's easy
|
||||
// to also adjust to position of the container to keep it aligned.
|
||||
//
|
||||
// TODO: Currently I am assuming that the container is center aligned,
|
||||
// but it can actually be aligned in some other ways. Adjust this
|
||||
// so it can keep left and right-aligned containers in position
|
||||
// as well.
|
||||
|
||||
const count = hand.size();
|
||||
|
||||
// figure out the current width and height, and final width and height
|
||||
// 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;
|
||||
|
||||
// the containers change in width and height over the course of the
|
||||
// animation
|
||||
this.#containerSizeDelta = [
|
||||
finalWidth - initialWidth,
|
||||
finalHeight - initialHeight,
|
||||
];
|
||||
|
||||
// the final x and y of the container after the animation is completed
|
||||
this.#containerFinalPosition = [
|
||||
this.#container.x + (this.#container.width - finalWidth) / 2,
|
||||
this.#container.y, // currently this does not change...
|
||||
];
|
||||
}
|
||||
|
||||
onTick(ticker: Ticker) {
|
||||
let isXAnimating = false; // true if the x value of the card updated
|
||||
let isYAnimating = false; // true of the y value of the card updated
|
||||
|
||||
// rate is the percent of the total distance travelled that each
|
||||
// card will complete per frame at the target framerate.
|
||||
//
|
||||
// For more information about the number this rate will be
|
||||
// multiplied against, see:
|
||||
// https://pixijs.download/release/docs/ticker.Ticker.html#deltatime
|
||||
const rate = 0.04;
|
||||
|
||||
for (let i = 0; i < this.#cardDeltas.length; i++) {
|
||||
const card = this.#cards[i];
|
||||
|
||||
// x, y coordinates where the card will end up
|
||||
const [finalX, finalY] = 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// The container should now be at its final tageted coordinates.
|
||||
const [containerX, containerY] = this.#containerFinalPosition;
|
||||
this.#container.x = containerX;
|
||||
this.#container.y = containerY;
|
||||
|
||||
// Clear the callback our of the ticker.
|
||||
super.done();
|
||||
}
|
||||
|
||||
// If x is still animating, then the container may need to be adjusted to
|
||||
// keep it aligned as its width changes.
|
||||
//
|
||||
// TODO: Currently this only handles horizontal, centered alignment. Some
|
||||
// additional logic, and maybe even some checks of y, will be needed
|
||||
// for fancier layouts of the future.
|
||||
if (isXAnimating) {
|
||||
const offset = this.#containerSizeDelta[0] * rate * ticker.deltaTime;
|
||||
this.#container.x -= offset / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,11 +4,11 @@ import {
|
||||
Sprite,
|
||||
Spritesheet,
|
||||
Ticker,
|
||||
type TickerCallback,
|
||||
TickerCallback,
|
||||
} from "pixi.js";
|
||||
import { Card } from "./Card";
|
||||
import { CARD_HOVER_DIST, CARD_WIDTH } from "./constants";
|
||||
import { Animation, FlipCard } from "./Animation";
|
||||
import { Animation, FlipCard, ToHorizontal } from "./Animation";
|
||||
|
||||
type Pivot = "left" | "right" | "center";
|
||||
type Layout = "horizontal" | "ascending" | "stacked";
|
||||
@ -30,6 +30,7 @@ export class Hand {
|
||||
#maxWidth: number;
|
||||
#pivot: Pivot;
|
||||
#ticker: Ticker | null;
|
||||
#gap: number;
|
||||
|
||||
constructor(maxWidth: number, cards: Card[] = []) {
|
||||
if (maxWidth < CARD_WIDTH) {
|
||||
@ -41,6 +42,7 @@ export class Hand {
|
||||
this.#hover = false;
|
||||
this.#maxWidth = maxWidth;
|
||||
this.#pivot = "left";
|
||||
this.#gap = 0;
|
||||
this.#layout = "horizontal";
|
||||
this.#statuses = cards.map((card) => ({
|
||||
face: card,
|
||||
@ -89,6 +91,10 @@ export class Hand {
|
||||
});
|
||||
}
|
||||
|
||||
setGap(gap: number) {
|
||||
this.#gap = gap;
|
||||
}
|
||||
|
||||
flip(cardIdx: number) {
|
||||
if (cardIdx >= this.#cardSprites.length)
|
||||
throw new Error(`card index out of range: ${cardIdx}`);
|
||||
@ -99,10 +105,20 @@ export class Hand {
|
||||
const card = this.#cardSprites[cardIdx];
|
||||
const status = this.#statuses[cardIdx];
|
||||
const ticker = this.#ticker;
|
||||
|
||||
this.#animations.push(new FlipCard(ticker, card, status));
|
||||
}
|
||||
|
||||
size(): number {
|
||||
return this.#cardSprites.length;
|
||||
}
|
||||
|
||||
spread() {
|
||||
if (!this.#ticker)
|
||||
throw new Error("can't animate hand before passing it a ticker");
|
||||
|
||||
this.#animations.push(new ToHorizontal(this.#ticker, this));
|
||||
}
|
||||
|
||||
#setCardHover(sprite: Sprite) {
|
||||
if (this.#hover) {
|
||||
const { y } = sprite;
|
||||
@ -121,6 +137,14 @@ export class Hand {
|
||||
}
|
||||
}
|
||||
|
||||
getCardSprites() {
|
||||
return this.#cardSprites;
|
||||
}
|
||||
|
||||
getLayout() {
|
||||
return this.#layout;
|
||||
}
|
||||
|
||||
hover(hasHover: boolean) {
|
||||
if (this.#hover === hasHover) return;
|
||||
|
||||
@ -157,20 +181,23 @@ export class Hand {
|
||||
this.#pivot = pivot;
|
||||
}
|
||||
|
||||
fanCards() {
|
||||
const count = this.#cardSprites.length;
|
||||
getCardPositions(layout: Layout, cardCount: number) {
|
||||
const positions: [number, number][] = [];
|
||||
const max = this.#maxWidth;
|
||||
let offset = CARD_WIDTH / 3;
|
||||
|
||||
if (this.#layout === "horizontal") {
|
||||
offset = count * CARD_WIDTH > max ? max / count : CARD_WIDTH;
|
||||
if (layout === "horizontal") {
|
||||
offset =
|
||||
cardCount * (CARD_WIDTH + this.#gap) > max
|
||||
? max / cardCount
|
||||
: CARD_WIDTH + this.#gap;
|
||||
}
|
||||
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
for (const card of this.#cardSprites) {
|
||||
card.x = x;
|
||||
card.y = y;
|
||||
switch (this.#layout) {
|
||||
for (let i = 0; i < cardCount; i++) {
|
||||
positions.push([x, y]);
|
||||
switch (layout) {
|
||||
case "ascending":
|
||||
x += offset;
|
||||
y -= offset;
|
||||
@ -183,6 +210,22 @@ export class Hand {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
fanCards() {
|
||||
const positions = this.getCardPositions(
|
||||
this.#layout,
|
||||
this.#cardSprites.length,
|
||||
);
|
||||
|
||||
for (let i = 0; i < positions.length; i++) {
|
||||
const [x, y] = positions[i];
|
||||
const card = this.#cardSprites[i];
|
||||
card.x = x;
|
||||
card.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
getContainer(): Container {
|
||||
|
||||
@ -1,17 +1,14 @@
|
||||
import {
|
||||
Application,
|
||||
Bounds,
|
||||
FillGradient,
|
||||
Graphics,
|
||||
Text,
|
||||
TextStyle,
|
||||
TextStyleOptions,
|
||||
} from "pixi.js";
|
||||
import { Application, Graphics, Text, TextStyleOptions } from "pixi.js";
|
||||
import { Hand } from "./Hand";
|
||||
import { Card } from "./Card";
|
||||
import { CARD_HEIGHT, CARD_HOVER_DIST, CARD_WIDTH } from "./constants";
|
||||
|
||||
const playerCards: Card[] = ["nineOfSpades", "sixOfClubs"];
|
||||
const playerCards: Card[] = [
|
||||
"nineOfSpades",
|
||||
"sixOfClubs",
|
||||
"kingOfClubs",
|
||||
"nineOfDiamonds",
|
||||
];
|
||||
|
||||
const dealerCards: Card[] = ["unknown", "aceOfHearts"];
|
||||
|
||||
@ -44,31 +41,6 @@ const dealerCards: Card[] = ["unknown", "aceOfHearts"];
|
||||
|
||||
const tableMarkings = new Graphics();
|
||||
|
||||
const markTable = () => {
|
||||
tableMarkings.clear();
|
||||
|
||||
// dark green dealer area
|
||||
tableMarkings.rect(0, 0, app.screen.width, CARD_HEIGHT * 1.8);
|
||||
tableMarkings.fill("#055010");
|
||||
|
||||
// white insurance info box
|
||||
tableMarkings.rect(
|
||||
CARD_WIDTH / 2,
|
||||
CARD_HEIGHT * 2.15,
|
||||
app.screen.width - CARD_WIDTH,
|
||||
CARD_HEIGHT / 2.5,
|
||||
);
|
||||
tableMarkings.stroke({
|
||||
color: "#ffffff",
|
||||
width: 8,
|
||||
});
|
||||
};
|
||||
|
||||
const positionPayoutInfo = () => {
|
||||
blackjackPayout.pivot.set(blackjackPayout.width / 2, 0);
|
||||
blackjackPayout.position.set(app.screen.width / 2, CARD_HEIGHT * 1.5);
|
||||
};
|
||||
|
||||
const dealerRulesStyle: TextStyleOptions = {
|
||||
fontFamily: "Fira Sans",
|
||||
fontStyle: "italic",
|
||||
@ -81,11 +53,6 @@ const dealerCards: Card[] = ["unknown", "aceOfHearts"];
|
||||
style: dealerRulesStyle,
|
||||
});
|
||||
|
||||
const positionDealerInfo = () => {
|
||||
dealerRules.pivot.set(dealerRules.width / 2, 0);
|
||||
dealerRules.position.set(app.screen.width / 2, CARD_HEIGHT * 1.85);
|
||||
};
|
||||
|
||||
const insuranceStyle: TextStyleOptions = {
|
||||
fontFamily: "Fira Sans",
|
||||
fontWeight: "bold",
|
||||
@ -99,32 +66,19 @@ const dealerCards: Card[] = ["unknown", "aceOfHearts"];
|
||||
style: insuranceStyle,
|
||||
});
|
||||
|
||||
const positionInsurancePayout = () => {
|
||||
insurancePayout.pivot.set(insurancePayout.width / 2, 0);
|
||||
insurancePayout.position.set(app.screen.width / 2, CARD_HEIGHT * 2.2);
|
||||
};
|
||||
|
||||
const playerHand = new Hand(350, []);
|
||||
const dealerHand = new Hand(350, []);
|
||||
|
||||
const positionPlayer = () => {
|
||||
playerHand.setPivot("center");
|
||||
playerHand.setPosition(app.screen.width / 2, CARD_HEIGHT * 3);
|
||||
playerHand.setCardLayout("ascending");
|
||||
const positionElements = () => {
|
||||
markTable(app, tableMarkings);
|
||||
positionPayoutInfo(app, blackjackPayout);
|
||||
positionDealerInfo(app, dealerRules);
|
||||
positionInsurancePayout(app, insurancePayout);
|
||||
positionPlayer(app, playerHand);
|
||||
positionDealer(app, dealerHand);
|
||||
};
|
||||
|
||||
const positionDealer = () => {
|
||||
dealerHand.setPivot("center");
|
||||
dealerHand.setPosition(app.screen.width / 2, 0 + CARD_HOVER_DIST);
|
||||
dealerHand.setCardLayout("stacked");
|
||||
};
|
||||
|
||||
markTable();
|
||||
positionPayoutInfo();
|
||||
positionDealerInfo();
|
||||
positionInsurancePayout();
|
||||
positionPlayer();
|
||||
positionDealer();
|
||||
positionElements();
|
||||
|
||||
app.stage.addChild(tableMarkings);
|
||||
app.stage.addChild(blackjackPayout);
|
||||
@ -144,30 +98,90 @@ const dealerCards: Card[] = ["unknown", "aceOfHearts"];
|
||||
// https://pixijs.com/8.x/guides/concepts/scene-graph#local-vs-global-coordinates
|
||||
window.addEventListener?.("resize", () => {
|
||||
app.renderer.resize(window.innerWidth, window.innerHeight);
|
||||
markTable();
|
||||
positionPayoutInfo();
|
||||
positionDealerInfo();
|
||||
positionInsurancePayout();
|
||||
positionPlayer();
|
||||
positionDealer();
|
||||
positionElements();
|
||||
});
|
||||
|
||||
let ms = 0;
|
||||
let elapsed = 0;
|
||||
app.ticker.add((time) => {
|
||||
elapsed += time.elapsedMS;
|
||||
if (elapsed >= ms) {
|
||||
ms += 300;
|
||||
if (playerCards.length) {
|
||||
playerHand.add(playerCards.shift()!);
|
||||
for (const card of playerCards) {
|
||||
playerHand.add(card, false);
|
||||
}
|
||||
|
||||
if (dealerCards.length) {
|
||||
dealerHand.add(dealerCards.shift()!);
|
||||
let firstCard = true;
|
||||
for (const card of dealerCards) {
|
||||
dealerHand.add(card, firstCard);
|
||||
firstCard = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dealerHand.setGap(CARD_WIDTH * 0.1);
|
||||
dealerHand.animationTicker(app.ticker);
|
||||
dealerHand.spread();
|
||||
|
||||
playerHand.setGap(CARD_WIDTH * 0.1);
|
||||
playerHand.animationTicker(app.ticker);
|
||||
playerHand.spread();
|
||||
|
||||
// let ms = 0;
|
||||
// let elapsed = 0;
|
||||
// app.ticker.add((time) => {
|
||||
// elapsed += time.elapsedMS;
|
||||
// if (elapsed >= ms) {
|
||||
// ms += 300;
|
||||
// if (playerCards.length) {
|
||||
// playerHand.add(playerCards.shift()!);
|
||||
// }
|
||||
|
||||
// if (dealerCards.length) {
|
||||
// dealerHand.add(dealerCards.shift()!);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
|
||||
function positionPlayer(app: Application, h: Hand) {
|
||||
h.setPivot("center");
|
||||
h.setPosition(app.screen.width / 2, CARD_HEIGHT * 3);
|
||||
h.setCardLayout("ascending");
|
||||
}
|
||||
|
||||
function positionDealer(app: Application, h: Hand) {
|
||||
h.setPivot("center");
|
||||
h.setPosition(app.screen.width / 2, 0 + CARD_HOVER_DIST);
|
||||
h.setCardLayout("stacked");
|
||||
}
|
||||
|
||||
function positionDealerInfo(app: Application, t: Text) {
|
||||
t.pivot.set(t.width / 2, 0);
|
||||
t.position.set(app.screen.width / 2, CARD_HEIGHT * 1.85);
|
||||
}
|
||||
|
||||
function positionPayoutInfo(app: Application, t: Text) {
|
||||
t.pivot.set(t.width / 2, 0);
|
||||
t.position.set(app.screen.width / 2, CARD_HEIGHT * 1.5);
|
||||
}
|
||||
|
||||
function positionInsurancePayout(app: Application, t: Text) {
|
||||
t.pivot.set(t.width / 2, 0);
|
||||
t.position.set(app.screen.width / 2, CARD_HEIGHT * 2.2);
|
||||
}
|
||||
|
||||
function markTable(app: Application, g: Graphics) {
|
||||
g.clear();
|
||||
|
||||
// dark green dealer area
|
||||
g.rect(0, 0, app.screen.width, CARD_HEIGHT * 1.8);
|
||||
g.fill("#055010");
|
||||
|
||||
// white insurance info box
|
||||
g.rect(
|
||||
CARD_WIDTH / 2,
|
||||
CARD_HEIGHT * 2.15,
|
||||
app.screen.width - CARD_WIDTH,
|
||||
CARD_HEIGHT / 2.5,
|
||||
);
|
||||
g.stroke({
|
||||
color: "#ffffff",
|
||||
width: 8,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user