Compare commits

..

3 Commits

Author SHA1 Message Date
00a37690a6 added fly animation, refactoring, scale cards. 2026-01-14 13:16:48 -08:00
cb7230124c refactor animations to be promise based 2025-11-09 18:34:19 -08:00
f9a7d5b3cc add ToHorizontal animation 2025-11-09 13:23:17 -08:00
3 changed files with 501 additions and 192 deletions

View File

@ -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;
}

View File

@ -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 {

View File

@ -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,
});
}