feat(poker): chip-flight animation between pot and viewer on hand settle
Co-authored-by: Sisyphus <sisyphus@ohmyopencode.dev>
This commit is contained in:
parent
0ddc090acb
commit
169f938b05
2 changed files with 122 additions and 1 deletions
|
|
@ -1414,6 +1414,78 @@
|
|||
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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { chipFlightChips, type FlightChip } from "../blackjack/BlackjackChipStack";
|
||||
import type { Card, PublicPlayerView } from "@/lib/minigames/poker";
|
||||
import type { PokerSnapshot } from "@/app/(frontend)/minigames/poker/actions";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
|
@ -122,6 +123,33 @@ export function PokerFeltCanvas({
|
|||
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 (
|
||||
<div className="poker-table-stage">
|
||||
<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 && (
|
||||
<div className="poker-popup-stack poker-popup-viewer-spot" aria-hidden="true">
|
||||
{viewerPopups.map((popup) => (
|
||||
|
|
|
|||
Loading…
Reference in a new issue