1
0
Fork 0

feat(poker): chip-flight animation between pot and viewer on hand settle

Co-authored-by: Sisyphus <sisyphus@ohmyopencode.dev>
This commit is contained in:
Jason Fraley 2026-09-14 19:26:30 -04:00
parent 0ddc090acb
commit 169f938b05
2 changed files with 122 additions and 1 deletions

View file

@ -1414,6 +1414,78 @@
overflow: hidden; overflow: hidden;
} }
/* Poker chip flight: every chip starts stacked at one origin and launches
one after another (130ms stagger, fill-mode both holds unlaunched chips
in the pile). Wins drop from the pot plaque (~40% of the felt) down to
the viewer card spot (bottom 7rem, so about 14rem of travel); losses fly
the reverse leg from the card spot up to the pot. Chips are the shared
.bj-chip pieces; the geometry is poker-scoped because blackjack flies
between different anchors. */
.poker-chip-flight {
position: absolute;
left: 50%;
z-index: 35;
width: 1.6rem;
height: 0.95rem;
transform: translateX(-50%);
pointer-events: none;
}
.poker-chip-flight-win {
top: 40%;
}
.poker-chip-flight-loss {
bottom: 7.6rem;
}
.poker-chip-flight .bj-chip {
bottom: auto;
left: 0;
}
.poker-chip-flight-win .bj-chip {
animation: poker-chip-fly-win 700ms ease-in both;
}
.poker-chip-flight-loss .bj-chip {
animation: poker-chip-fly-loss 700ms ease-in both;
}
@keyframes poker-chip-fly-win {
0% {
opacity: 1;
transform: translateY(0) scale(1);
}
70% {
opacity: 1;
}
100% {
opacity: 0;
transform: translateY(14rem) scale(0.7);
}
}
@keyframes poker-chip-fly-loss {
0% {
opacity: 1;
transform: translateY(0) scale(1);
}
70% {
opacity: 1;
}
100% {
opacity: 0;
transform: translateY(-14rem) scale(0.7);
}
}
@media (prefers-reduced-motion: reduce) {
.poker-chip-flight {
display: none;
}
}
/* Fullscreen contain-fit: absolute with zero insets + auto margins sizes the /* Fullscreen contain-fit: absolute with zero insets + auto margins sizes the
felt to the largest 4:3 rectangle inside the viewport area (the same rules felt to the largest 4:3 rectangle inside the viewport area (the same rules
browsers use for replaced elements), so it can never collapse to content browsers use for replaced elements), so it can never collapse to content

View file

@ -1,7 +1,8 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { chipFlightChips, type FlightChip } from "../blackjack/BlackjackChipStack";
import type { Card, PublicPlayerView } from "@/lib/minigames/poker"; import type { Card, PublicPlayerView } from "@/lib/minigames/poker";
import type { PokerSnapshot } from "@/app/(frontend)/minigames/poker/actions"; import type { PokerSnapshot } from "@/app/(frontend)/minigames/poker/actions";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@ -122,6 +123,33 @@ export function PokerFeltCanvas({
return () => query.removeEventListener("change", update); return () => query.removeEventListener("change", update);
}, []); }, []);
// Chip flight: on the settle transition the viewer's net chips fly between
// the pot plaque (felt center) and the viewer card spot: winnings drop
// from the pot, lost stakes climb to it. The hand number guards against
// refiring while the snapshot settles through showdown and hand-complete.
const previousSettleHandRef = useRef<number | null>(null);
const [chipFlights, setChipFlights] = useState<
{ key: number; direction: "win" | "loss"; chips: FlightChip[] }[]
>([]);
useEffect(() => {
const settling = view.phase === "showdown" || view.phase === "hand-complete";
if (!settling || !result || result.netChange === 0) {
previousSettleHandRef.current = null;
return;
}
if (previousSettleHandRef.current === view.handNumber) return;
previousSettleHandRef.current = view.handNumber;
setChipFlights([
{
key: Date.now(),
direction: result.netChange > 0 ? "win" : "loss",
chips: chipFlightChips(Math.abs(result.netChange)),
},
]);
const timer = setTimeout(() => setChipFlights([]), 2400);
return () => clearTimeout(timer);
}, [view.phase, view.handNumber, result]);
return ( return (
<div className="poker-table-stage"> <div className="poker-table-stage">
<div <div
@ -272,6 +300,27 @@ export function PokerFeltCanvas({
/> />
)} )}
{chipFlights.map((flight) => (
<div
key={flight.key}
className={cn(
"poker-chip-flight",
flight.direction === "win"
? "poker-chip-flight-win"
: "poker-chip-flight-loss",
)}
aria-hidden="true"
>
{flight.chips.map((chip, chipIndex) => (
<span
key={chip.key}
className={cn("bj-chip", chip.tone)}
style={{ animationDelay: `${chipIndex * 130}ms` }}
/>
))}
</div>
))}
{viewerPopups.length > 0 && ( {viewerPopups.length > 0 && (
<div className="poker-popup-stack poker-popup-viewer-spot" aria-hidden="true"> <div className="poker-popup-stack poker-popup-viewer-spot" aria-hidden="true">
{viewerPopups.map((popup) => ( {viewerPopups.map((popup) => (