Compare commits
3 Commits
c344c60db0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 00a37690a6 | |||
| cb7230124c | |||
| f9a7d5b3cc |
@ -1,88 +1,289 @@
|
|||||||
import type { Sprite, Ticker, TickerCallback } from "pixi.js";
|
import type { Container, Point, Sprite, Ticker, TickerCallback } from "pixi.js";
|
||||||
import { CardStatus } from "./Hand";
|
import { CardStatus, Hand } from "./Hand";
|
||||||
import { getSpriteSheet } from "./spritesheet";
|
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();
|
const spritesheet = await getSpriteSheet();
|
||||||
|
|
||||||
export abstract class Animation {
|
export abstract class Animation {
|
||||||
protected ticker: Ticker;
|
protected ticker: Ticker;
|
||||||
protected cb: TickerCallback<any>;
|
protected boundCallback: TickerCallback<any>;
|
||||||
#next: Animation | null;
|
#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.cb = () => {};
|
this.ticker = hand.ticker;
|
||||||
this.#next = null;
|
this.#resolve = Promise.resolve;
|
||||||
|
this.boundCallback = () => this.#resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract getCallback(): TickerCallback<any>;
|
abstract onTick(ticker: Ticker): void;
|
||||||
|
|
||||||
next(animation: Animation) {
|
start() {
|
||||||
this.#next = animation;
|
return new Promise((resolve) => {
|
||||||
|
this.#resolve = resolve;
|
||||||
|
this.boundCallback = this.onTick.bind(this);
|
||||||
|
this.ticker.add(this.boundCallback);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
done() {
|
done() {
|
||||||
console.log("removing", this.ticker);
|
this.ticker.remove(this.boundCallback);
|
||||||
this.ticker.remove(this.cb);
|
this.#resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FlipCard extends Animation {
|
export class Flip extends Animation {
|
||||||
card: Sprite;
|
card: Sprite;
|
||||||
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.cb = () => {};
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
getCallback() {
|
onTick(ticker: Ticker) {
|
||||||
const callback = (ticker: Ticker) => {
|
this.status.isFaceDown = !this.status.isFaceDown;
|
||||||
const to = this.status.isFaceDown ? this.status.face : "unknown";
|
|
||||||
this.status.isFaceDown = !this.status.isFaceDown;
|
|
||||||
|
|
||||||
console.log(this);
|
// the card has just fipped
|
||||||
|
if (!this.flipped && this.card.width <= 10) {
|
||||||
|
this.flipped = true;
|
||||||
|
this.card.texture = spritesheet.textures[this.to];
|
||||||
|
}
|
||||||
|
|
||||||
this.card.pivot.set(this.cardWidth / 2, 0);
|
// the other side of the card is how showing
|
||||||
|
if (this.flipped) {
|
||||||
|
const change = 20 * ticker.deltaTime;
|
||||||
|
if (this.card.width + change >= this.cardWidth)
|
||||||
|
this.card.width = this.cardWidth;
|
||||||
|
else this.card.width += change;
|
||||||
|
}
|
||||||
|
|
||||||
// the card has just fipped
|
// the original side is still showing
|
||||||
if (!this.flipped && this.card.width <= 10) {
|
else {
|
||||||
this.flipped = true;
|
const change = 20 * ticker.deltaTime;
|
||||||
this.card.texture = spritesheet.textures[to];
|
if (this.card.width <= change) this.card.width = 0;
|
||||||
}
|
else this.card.width -= change;
|
||||||
|
}
|
||||||
|
|
||||||
// the other side of the card is how showing
|
if (this.flipped && this.card.width === this.cardWidth) {
|
||||||
if (this.flipped) {
|
console.log("remove", this.flipped, this.card.width, this.cardWidth);
|
||||||
const change = 20 * ticker.deltaTime;
|
super.done();
|
||||||
if (this.card.width + change >= this.cardWidth)
|
}
|
||||||
this.card.width = this.cardWidth;
|
|
||||||
else this.card.width += change;
|
|
||||||
}
|
|
||||||
|
|
||||||
// the original side is still showing
|
|
||||||
else {
|
|
||||||
const change = 20 * ticker.deltaTime;
|
|
||||||
if (this.card.width <= change) this.card.width = 0;
|
|
||||||
else this.card.width -= change;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.flipped && this.card.width === this.cardWidth) {
|
|
||||||
console.log("remove", this.flipped, this.card.width, this.cardWidth);
|
|
||||||
super.done();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.cb = callback.bind(this);
|
|
||||||
|
|
||||||
return this.cb;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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[]; // total distance travelled by each card
|
||||||
|
#finalPositions: Point[]; // 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(hand: Hand) {
|
||||||
|
super(hand);
|
||||||
|
|
||||||
|
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 total distances between each starting and ending point
|
||||||
|
const deltas: number[] = [];
|
||||||
|
for (let i = 0; i < currentPositions.length; i++) {
|
||||||
|
deltas.push(distanceBetween(currentPositions[i], finalPositions[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
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].x + CARD_WIDTH;
|
||||||
|
const finalHeight = finalPositions[count - 1].y + 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.#cards.length; i++) {
|
||||||
|
const card = this.#cards[i];
|
||||||
|
|
||||||
|
// x, y coordinates where the card will end up
|
||||||
|
const to = this.#finalPositions[i];
|
||||||
|
|
||||||
|
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
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,14 +1,7 @@
|
|||||||
import {
|
import { Assets, Container, Point, Sprite, Spritesheet, Ticker } from "pixi.js";
|
||||||
Assets,
|
|
||||||
Container,
|
|
||||||
Sprite,
|
|
||||||
Spritesheet,
|
|
||||||
Ticker,
|
|
||||||
type TickerCallback,
|
|
||||||
} from "pixi.js";
|
|
||||||
import { Card } from "./Card";
|
import { Card } from "./Card";
|
||||||
import { CARD_HOVER_DIST, CARD_WIDTH } from "./constants";
|
import { CARD_HEIGHT, CARD_HOVER_DIST, CARD_WIDTH } from "./constants";
|
||||||
import { Animation, FlipCard } from "./Animation";
|
import { Flip, Fly, ToHorizontal } from "./Animation";
|
||||||
|
|
||||||
type Pivot = "left" | "right" | "center";
|
type Pivot = "left" | "right" | "center";
|
||||||
type Layout = "horizontal" | "ascending" | "stacked";
|
type Layout = "horizontal" | "ascending" | "stacked";
|
||||||
@ -16,38 +9,35 @@ 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;
|
||||||
#hover: boolean;
|
#hover: boolean;
|
||||||
#layout: Layout;
|
#layout: Layout;
|
||||||
#maxWidth: number;
|
#maxWidth: number;
|
||||||
|
#maxHeight: number;
|
||||||
#pivot: Pivot;
|
#pivot: Pivot;
|
||||||
#ticker: Ticker | null;
|
ticker: Ticker | null;
|
||||||
|
#gap: number;
|
||||||
|
|
||||||
constructor(maxWidth: number, cards: Card[] = []) {
|
constructor(cards: Card[] = []) {
|
||||||
if (maxWidth < CARD_WIDTH) {
|
|
||||||
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 = Infinity;
|
||||||
|
this.#maxHeight = Infinity;
|
||||||
this.#pivot = "left";
|
this.#pivot = "left";
|
||||||
|
this.#gap = 0;
|
||||||
this.#layout = "horizontal";
|
this.#layout = "horizontal";
|
||||||
this.#statuses = cards.map((card) => ({
|
this.#statuses = cards.map((card) => ({
|
||||||
face: card,
|
face: card,
|
||||||
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[] = [];
|
||||||
@ -63,44 +53,65 @@ export class Hand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
add(card: Card, faceDown = false) {
|
async add(card: Card, faceDown = false, from: Point | null = null) {
|
||||||
const sprite = new Sprite(spritesheet.textures[card]);
|
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.#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);
|
||||||
this.#setCardHover(sprite);
|
this.#setCardHover(sprite);
|
||||||
this.fanCards();
|
this.fanCards();
|
||||||
this.setPivot(this.#pivot);
|
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) {
|
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());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flip(cardIdx: number) {
|
setGap(gap: number) {
|
||||||
|
this.#gap = gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Flip(this, cardIdx);
|
||||||
const status = this.#statuses[cardIdx];
|
await animation.start();
|
||||||
const ticker = this.#ticker;
|
}
|
||||||
|
|
||||||
this.#animations.push(new FlipCard(ticker, card, status));
|
size(): number {
|
||||||
|
return this.#cardSprites.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
async spread() {
|
||||||
|
if (!this.ticker)
|
||||||
|
throw new Error("can't animate hand before passing it a ticker");
|
||||||
|
|
||||||
|
const animation = new ToHorizontal(this);
|
||||||
|
await animation.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
#setCardHover(sprite: Sprite) {
|
#setCardHover(sprite: Sprite) {
|
||||||
@ -121,6 +132,29 @@ export class Hand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCardSprites() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMaxWidth(maxWidth: number) {
|
||||||
|
this.#maxWidth = maxWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMaxHeight(maxHeight: number) {
|
||||||
|
this.#maxHeight = maxHeight;
|
||||||
|
}
|
||||||
|
|
||||||
hover(hasHover: boolean) {
|
hover(hasHover: boolean) {
|
||||||
if (this.#hover === hasHover) return;
|
if (this.#hover === hasHover) return;
|
||||||
|
|
||||||
@ -157,32 +191,64 @@ export class Hand {
|
|||||||
this.#pivot = pivot;
|
this.#pivot = pivot;
|
||||||
}
|
}
|
||||||
|
|
||||||
fanCards() {
|
getCardPositions(layout: Layout, cardCount: number) {
|
||||||
const count = this.#cardSprites.length;
|
const positions: Point[] = [];
|
||||||
const max = this.#maxWidth;
|
let horizontalOffset = CARD_WIDTH / 3;
|
||||||
let offset = CARD_WIDTH / 3;
|
let verticalOffset = horizontalOffset;
|
||||||
|
|
||||||
if (this.#layout === "horizontal") {
|
if (layout === "horizontal") {
|
||||||
offset = count * CARD_WIDTH > max ? max / count : CARD_WIDTH;
|
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 x = 0;
|
||||||
let y = 0;
|
let y = 0;
|
||||||
for (const card of this.#cardSprites) {
|
for (let i = 0; i < cardCount; i++) {
|
||||||
card.x = x;
|
positions.push(new Point(x, y));
|
||||||
card.y = y;
|
switch (layout) {
|
||||||
switch (this.#layout) {
|
|
||||||
case "ascending":
|
case "ascending":
|
||||||
x += offset;
|
x += horizontalOffset;
|
||||||
y -= offset;
|
y -= verticalOffset;
|
||||||
break;
|
break;
|
||||||
case "horizontal":
|
case "horizontal":
|
||||||
x += offset;
|
x += horizontalOffset;
|
||||||
break;
|
break;
|
||||||
case "stacked":
|
case "stacked":
|
||||||
x += CARD_WIDTH / 20;
|
x += CARD_WIDTH / 20;
|
||||||
break;
|
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 {
|
getContainer(): Container {
|
||||||
|
|||||||
@ -1,19 +1,18 @@
|
|||||||
import {
|
import { Application, Graphics, Point, Text, TextStyleOptions } from "pixi.js";
|
||||||
Application,
|
|
||||||
Bounds,
|
|
||||||
FillGradient,
|
|
||||||
Graphics,
|
|
||||||
Text,
|
|
||||||
TextStyle,
|
|
||||||
TextStyleOptions,
|
|
||||||
} from "pixi.js";
|
|
||||||
import { Hand } from "./Hand";
|
import { Hand } from "./Hand";
|
||||||
import { Card } from "./Card";
|
import { Card } from "./Card";
|
||||||
import { CARD_HEIGHT, CARD_HOVER_DIST, CARD_WIDTH } from "./constants";
|
import { CARD_HEIGHT, CARD_HOVER_DIST, CARD_WIDTH } from "./constants";
|
||||||
|
|
||||||
const playerCards: Card[] = ["nineOfSpades", "sixOfClubs"];
|
const playerCards: Card[] = [
|
||||||
|
"nineOfSpades",
|
||||||
|
"sixOfClubs",
|
||||||
|
"kingOfClubs",
|
||||||
|
"nineOfDiamonds",
|
||||||
|
"sixOfHearts",
|
||||||
|
"sevenOfHearts",
|
||||||
|
];
|
||||||
|
|
||||||
const dealerCards: Card[] = ["unknown", "aceOfHearts"];
|
const dealerCards: Card[] = ["aceOfClubs", "aceOfHearts"];
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
@ -44,31 +43,6 @@ const dealerCards: Card[] = ["unknown", "aceOfHearts"];
|
|||||||
|
|
||||||
const tableMarkings = new Graphics();
|
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 = {
|
const dealerRulesStyle: TextStyleOptions = {
|
||||||
fontFamily: "Fira Sans",
|
fontFamily: "Fira Sans",
|
||||||
fontStyle: "italic",
|
fontStyle: "italic",
|
||||||
@ -81,11 +55,6 @@ const dealerCards: Card[] = ["unknown", "aceOfHearts"];
|
|||||||
style: dealerRulesStyle,
|
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 = {
|
const insuranceStyle: TextStyleOptions = {
|
||||||
fontFamily: "Fira Sans",
|
fontFamily: "Fira Sans",
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
@ -99,32 +68,22 @@ const dealerCards: Card[] = ["unknown", "aceOfHearts"];
|
|||||||
style: insuranceStyle,
|
style: insuranceStyle,
|
||||||
});
|
});
|
||||||
|
|
||||||
const positionInsurancePayout = () => {
|
const playerHand = new Hand();
|
||||||
insurancePayout.pivot.set(insurancePayout.width / 2, 0);
|
playerHand.setMaxWidth(CARD_WIDTH * 3);
|
||||||
insurancePayout.position.set(app.screen.width / 2, CARD_HEIGHT * 2.2);
|
playerHand.setMaxHeight(CARD_HEIGHT * 1.5);
|
||||||
|
|
||||||
|
const dealerHand = new Hand();
|
||||||
|
|
||||||
|
const positionElements = () => {
|
||||||
|
markTable(app, tableMarkings);
|
||||||
|
positionPayoutInfo(app, blackjackPayout);
|
||||||
|
positionDealerInfo(app, dealerRules);
|
||||||
|
positionInsurancePayout(app, insurancePayout);
|
||||||
|
positionPlayer(app, playerHand);
|
||||||
|
positionDealer(app, dealerHand);
|
||||||
};
|
};
|
||||||
|
|
||||||
const playerHand = new Hand(350, []);
|
positionElements();
|
||||||
const dealerHand = new Hand(350, []);
|
|
||||||
|
|
||||||
const positionPlayer = () => {
|
|
||||||
playerHand.setPivot("center");
|
|
||||||
playerHand.setPosition(app.screen.width / 2, CARD_HEIGHT * 3);
|
|
||||||
playerHand.setCardLayout("ascending");
|
|
||||||
};
|
|
||||||
|
|
||||||
const positionDealer = () => {
|
|
||||||
dealerHand.setPivot("center");
|
|
||||||
dealerHand.setPosition(app.screen.width / 2, 0 + CARD_HOVER_DIST);
|
|
||||||
dealerHand.setCardLayout("stacked");
|
|
||||||
};
|
|
||||||
|
|
||||||
markTable();
|
|
||||||
positionPayoutInfo();
|
|
||||||
positionDealerInfo();
|
|
||||||
positionInsurancePayout();
|
|
||||||
positionPlayer();
|
|
||||||
positionDealer();
|
|
||||||
|
|
||||||
app.stage.addChild(tableMarkings);
|
app.stage.addChild(tableMarkings);
|
||||||
app.stage.addChild(blackjackPayout);
|
app.stage.addChild(blackjackPayout);
|
||||||
@ -144,30 +103,113 @@ const dealerCards: Card[] = ["unknown", "aceOfHearts"];
|
|||||||
// https://pixijs.com/8.x/guides/concepts/scene-graph#local-vs-global-coordinates
|
// https://pixijs.com/8.x/guides/concepts/scene-graph#local-vs-global-coordinates
|
||||||
window.addEventListener?.("resize", () => {
|
window.addEventListener?.("resize", () => {
|
||||||
app.renderer.resize(window.innerWidth, window.innerHeight);
|
app.renderer.resize(window.innerWidth, window.innerHeight);
|
||||||
markTable();
|
positionElements();
|
||||||
positionPayoutInfo();
|
|
||||||
positionDealerInfo();
|
|
||||||
positionInsurancePayout();
|
|
||||||
positionPlayer();
|
|
||||||
positionDealer();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let ms = 0;
|
dealerHand.setGap(CARD_WIDTH * 0.1);
|
||||||
let elapsed = 0;
|
dealerHand.animationTicker(app.ticker);
|
||||||
app.ticker.add((time) => {
|
|
||||||
elapsed += time.elapsedMS;
|
|
||||||
if (elapsed >= ms) {
|
|
||||||
ms += 300;
|
|
||||||
if (playerCards.length) {
|
|
||||||
playerHand.add(playerCards.shift()!);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dealerCards.length) {
|
playerHand.setGap(CARD_WIDTH * 0.1);
|
||||||
dealerHand.add(dealerCards.shift()!);
|
playerHand.animationTicker(app.ticker);
|
||||||
}
|
|
||||||
}
|
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;
|
||||||
|
// 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) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
function positionPlayer(app: Application, h: Hand) {
|
||||||
|
h.setPivot("center");
|
||||||
|
h.setPosition(
|
||||||
|
app.screen.width / 2,
|
||||||
|
CARD_HEIGHT * 2.15 + 4 + CARD_HEIGHT / 2.5 + CARD_WIDTH,
|
||||||
|
);
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user