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;
|
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
|
||||||
|
|
|
||||||
|
|
@ -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) => (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue