games: the money moves
The table dealt cards but settled money by editing a number. So the felt got the two things it was missing: a bet spot in front of you, and the house's rack beside the shoe. Every chip is now always travelling between one of those and the other. You build a bet by throwing chips onto the spot — the chip you clicked is the chip that flies. The stake sits there through the hand. The house pays out of its rack into the spot, and the pile is then swept back to your stack. A loss goes to the rack and does not come back. Two rules hold it together. The number under the pile is a readout of the pile, never the other way round: the bet starts at nothing rather than at a default nobody put down, and a settled hand leaves your stake back up on the spot, because otherwise the panel prints "your bet: 300" over an empty circle. And the chip bar does not move until the chips that justify it have landed — a counter that pays you before the dealer turns over is a counter that has told you the ending. casino-fx.js is the engine underneath: chips fly on an arc, out of a fixed overlay so no container clips one crossing from a button to the felt. It knows nothing about blackjack. Also: cards land with weight and a degree or two of tilt, so a hand looks dealt rather than typeset; the dealer takes a beat before drawing out; and a natural gets confetti, which is the only thing in the room that does. Driven in a real browser, which is the only way to review an animation — and which is what caught the verdict pill rendering white on white in a dark room, a chip rack sitting on top of the dealer, and Hit being offered over a table that was still being paid out. devcasino_test.go is that harness, kept.
This commit is contained in:
@@ -653,40 +653,235 @@ html[data-phase="night"] {
|
||||
}
|
||||
|
||||
/* The flight itself: out of the shoe (up and to the right), scaled down and
|
||||
spinning slightly, into place. */
|
||||
spinning slightly, into place — and then a beat of settling, because a card
|
||||
that stops dead on the felt looks like it was pasted there. It overshoots a
|
||||
little and rocks back onto --tilt, the small resting angle the JS gives each
|
||||
card so a hand looks handled rather than typeset. */
|
||||
@keyframes pete-deal {
|
||||
from {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(var(--deal-x, 14rem), var(--deal-y, -7rem)) scale(0.72) rotate(9deg);
|
||||
}
|
||||
to {
|
||||
62% {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0) scale(1) rotate(0);
|
||||
transform: translate(0, 0.35rem) scale(1.05) rotate(calc(var(--tilt, 0deg) - 2deg));
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0) scale(1) rotate(var(--tilt, 0deg));
|
||||
}
|
||||
}
|
||||
/* And the weight: the felt takes the hit. The shadow under a landing card
|
||||
spreads and darkens for the instant it arrives. */
|
||||
.pete-card::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: auto 6% -0.35rem 6%;
|
||||
height: 0.6rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
filter: blur(5px);
|
||||
animation: pete-thud 0.42s cubic-bezier(0.22, 1, 0.36, 1) backwards;
|
||||
}
|
||||
@keyframes pete-thud {
|
||||
0%, 55% { opacity: 0; transform: scale(0.4); }
|
||||
72% { opacity: 0.9; transform: scale(1.15); }
|
||||
100% { opacity: 0.45; transform: scale(1); }
|
||||
}
|
||||
|
||||
/* A settled hand: the winning side gets a little lift, so a win reads at a
|
||||
glance rather than only in the text. */
|
||||
.pete-hand[data-won="1"] .pete-card { animation: pete-deal 0.42s cubic-bezier(0.22,1,0.36,1) backwards, pete-win 0.6s ease 0.1s; }
|
||||
@keyframes pete-win {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
40% { transform: translateY(-0.6rem); }
|
||||
0%, 100% { transform: rotate(var(--tilt, 0deg)) translateY(0); }
|
||||
40% { transform: rotate(var(--tilt, 0deg)) translateY(-0.6rem); }
|
||||
}
|
||||
/* A losing hand doesn't just sit there being wrong: it greys off. */
|
||||
.pete-hand[data-won="-1"] .pete-card {
|
||||
filter: saturate(0.45) brightness(0.78);
|
||||
transition: filter 0.5s ease 0.2s;
|
||||
}
|
||||
|
||||
/* Chips: the denominations you bet with. */
|
||||
.pete-chip {
|
||||
background: radial-gradient(circle at 50% 35%, rgba(255,255,255,0.28), transparent 60%), var(--chip, #e07a5f);
|
||||
border: 3px dashed rgba(255,255,255,0.55);
|
||||
box-shadow: 0 3px 0 rgba(60,40,20,0.22), 0 6px 14px rgba(60,40,20,0.18);
|
||||
/* ---- chips ---------------------------------------------------------------
|
||||
One disc, three jobs: the buttons you bet with, the stack sitting in the bet
|
||||
spot, and the chip in mid-air between them. Same face on all three, because
|
||||
they are meant to read as the *same chip* being moved around. */
|
||||
|
||||
.pete-disc {
|
||||
position: relative;
|
||||
border-radius: 999px;
|
||||
background:
|
||||
radial-gradient(circle at 50% 30%, rgba(255,255,255,0.30), transparent 62%),
|
||||
var(--chip, #e07a5f);
|
||||
border: 2px solid rgba(0,0,0,0.22);
|
||||
box-shadow: 0 3px 0 rgba(0,0,0,0.28), 0 6px 14px rgba(0,0,0,0.20);
|
||||
}
|
||||
/* The edge spots every casino chip has, cut as a dashed ring inside the rim. */
|
||||
.pete-disc::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 11%;
|
||||
border-radius: 999px;
|
||||
border: 2px dashed rgba(255,255,255,0.55);
|
||||
}
|
||||
.pete-disc[data-chip="5"] { --chip: #5aa9e6; }
|
||||
.pete-disc[data-chip="25"] { --chip: #4caf7d; }
|
||||
.pete-disc[data-chip="100"] { --chip: #2b2118; }
|
||||
.pete-disc[data-chip="500"] { --chip: #b079d6; }
|
||||
|
||||
.pete-chip { /* the buttons */
|
||||
transition: transform 0.12s ease;
|
||||
}
|
||||
.pete-chip > span {
|
||||
position: relative; /* over the dashed ring */
|
||||
}
|
||||
.pete-chip:active { transform: translateY(1px) scale(0.94); }
|
||||
|
||||
/* The bet spot: where your stake actually sits while the hand is played. */
|
||||
.pete-spot {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 7rem;
|
||||
width: 7rem;
|
||||
flex: none;
|
||||
border-radius: 999px;
|
||||
border: 2px dashed rgba(255,255,255,0.35);
|
||||
background: radial-gradient(circle at 50% 40%, rgba(255,255,255,0.10), transparent 70%);
|
||||
box-shadow: inset 0 0 0 6px rgba(0,0,0,0.08);
|
||||
transition: box-shadow 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
.pete-spot[data-live="1"] {
|
||||
border-color: rgba(var(--glow, 242,181,61), 0.85);
|
||||
box-shadow: inset 0 0 0 6px rgba(0,0,0,0.08), 0 0 22px rgba(var(--glow, 242,181,61), 0.35);
|
||||
}
|
||||
.pete-spot-label {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255,255,255,0.35);
|
||||
}
|
||||
.pete-spot[data-live="1"] .pete-spot-label { opacity: 0; }
|
||||
|
||||
/* The stack in the spot. Each chip is offset up the pile by its index, so ten
|
||||
chips look like ten chips rather than one chip with a number on it. */
|
||||
.pete-stack {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.pete-stack .pete-disc {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
height: 2.6rem;
|
||||
width: 2.6rem;
|
||||
margin: -1.3rem 0 0 -1.3rem;
|
||||
/* Each chip sits proud of the one below it by more than its own rim, so a
|
||||
stack of three reads as three. Less than this and they hide inside each
|
||||
other and a 150 bet looks the same as a 25 one. */
|
||||
transform: translateY(calc(-0.5rem * var(--i, 0))) rotate(var(--spin, 0deg));
|
||||
box-shadow: 0 2px 0 rgba(0,0,0,0.35), 0 4px 8px rgba(0,0,0,0.25);
|
||||
animation: pete-chip-land 0.28s cubic-bezier(0.34, 1.56, 0.64, 1) backwards;
|
||||
}
|
||||
@keyframes pete-chip-land {
|
||||
from { transform: translateY(calc(-0.5rem * var(--i, 0) - 1.1rem)) rotate(var(--spin, 0deg)) scale(1.18); }
|
||||
}
|
||||
.pete-spot-total {
|
||||
position: absolute;
|
||||
bottom: -0.6rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
border-radius: 999px;
|
||||
background: rgba(0,0,0,0.45);
|
||||
padding: 0.1rem 0.6rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* A chip in mid-air. Lives in a fixed overlay so it can cross from the betting
|
||||
buttons, over the page, and onto the felt without any container clipping it. */
|
||||
.pete-fly-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 60;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.pete-fly {
|
||||
position: absolute;
|
||||
height: 2.6rem;
|
||||
width: 2.6rem;
|
||||
margin: -1.3rem 0 0 -1.3rem;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* The house's own chips, racked on the felt. This is where a payout comes
|
||||
from, and where a losing stake goes — so the money has somewhere to be.
|
||||
It sits alongside the shoe rather than over on the left, because the left of
|
||||
the felt is where the dealer's cards land. */
|
||||
.pete-rack {
|
||||
position: absolute;
|
||||
top: 1.25rem;
|
||||
right: 5.75rem;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 0.6rem 0.45rem;
|
||||
border-radius: 0.75rem;
|
||||
background: rgba(0,0,0,0.20);
|
||||
box-shadow: inset 0 0 0 2px rgba(255,255,255,0.06);
|
||||
}
|
||||
/* A column in the rack is a *stack*, so it's striped: one band per chip, with
|
||||
the shadow of the chip above it cut into each. A plain coloured lozenge here
|
||||
read as a blob, which is the one thing a pile of money shouldn't. */
|
||||
.pete-rack span {
|
||||
display: block;
|
||||
width: 1.7rem;
|
||||
border-radius: 0.35rem;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(0,0,0,0.28), transparent 35%, transparent 65%, rgba(0,0,0,0.28)),
|
||||
repeating-linear-gradient(180deg,
|
||||
rgba(255,255,255,0.18) 0 0.06rem,
|
||||
var(--chip, #e07a5f) 0.06rem 0.3rem,
|
||||
rgba(0,0,0,0.35) 0.3rem 0.36rem);
|
||||
border: 1px solid rgba(0,0,0,0.35);
|
||||
height: calc(0.36rem * var(--stack, 3));
|
||||
}
|
||||
.pete-rack span[data-chip="5"] { --chip: #5aa9e6; }
|
||||
.pete-rack span[data-chip="25"] { --chip: #4caf7d; }
|
||||
.pete-rack span[data-chip="100"] { --chip: #2b2118; }
|
||||
.pete-rack span[data-chip="500"] { --chip: #b079d6; }
|
||||
|
||||
/* The burst. A natural gets confetti; nothing else in the room does, which is
|
||||
what keeps it worth something. */
|
||||
.pete-spark {
|
||||
position: absolute;
|
||||
height: 0.75rem;
|
||||
width: 0.45rem;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.35);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
/* The dealer's beat before they draw out. */
|
||||
.pete-dealer-think {
|
||||
animation: pete-think 0.9s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pete-think {
|
||||
0%, 100% { opacity: 0.6; }
|
||||
50% { opacity: 1; transform: translateY(-1px); }
|
||||
}
|
||||
.pete-chip[data-chip="5"] { --chip: #5aa9e6; }
|
||||
.pete-chip[data-chip="25"] { --chip: #4caf7d; }
|
||||
.pete-chip[data-chip="100"] { --chip: #2b2118; }
|
||||
.pete-chip[data-chip="500"] { --chip: #b079d6; }
|
||||
.pete-chip[aria-pressed="true"] { outline: 3px solid var(--accent); outline-offset: 2px; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.pete-card { animation: none; }
|
||||
.pete-card,
|
||||
.pete-card::after,
|
||||
.pete-stack .pete-disc,
|
||||
.pete-dealer-think { animation: none; }
|
||||
.pete-card-inner { transition: none; }
|
||||
.pete-hand[data-won="1"] .pete-card { animation: none; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user