games: draw the card, don't type it

The last attempt built a card face out of text: a "♠" in a span for every
pip. At the size a card actually is, a suit character renders as a speck —
the shape is whatever font answered, it doesn't scale, and it can't be put
on the half-row a real pip layout needs. The result read worse than the
plain rank it replaced.

So each face is one SVG on a 100×140 field, suits as vector shapes, pips at
the coordinates a printed deck puts them. Courts get a framed panel with the
suit above the letter and again below it upside down — mirroring a letter,
which is what the first pass did, just stacks two of them into a blob; a real
court mirrors a figure.

Also restores .pete-card-back, which went out with the text rules it was
sitting among: without it a face-down card had no back at all, so the
dealer's hole card was invisible on the felt. Caught by driving a hand.
This commit is contained in:
prosolis
2026-07-13 23:49:17 -07:00
parent 8ec13eab5b
commit b00da21a47
3 changed files with 112 additions and 129 deletions

View File

@@ -565,15 +565,15 @@ html[data-phase="night"] {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
min-height: 6.6rem;
min-height: 8.6rem;
}
/* One card. The wrapper does the flight, the inner face does the flip, so the
two never fight over the same transform. */
.pete-card {
perspective: 700px;
height: 7rem;
width: 5rem;
height: 8.4rem;
width: 6rem;
animation: pete-deal 0.42s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.pete-card-inner {
@@ -597,70 +597,23 @@ html[data-phase="night"] {
-webkit-backface-visibility: hidden;
box-shadow: 0 3px 0 rgba(0,0,0,0.18), 0 6px 14px rgba(0,0,0,0.22);
}
/* A real card face: index in two opposite corners, pips or a court figure in
the middle. Laid out as three rows — corner, body, corner — so the body gets
whatever's left and the indices stay pinned where they're printed. */
/* The face is a single SVG on a 100×140 field (see blackjack.js) — suits as
vector shapes, pips at printed-deck coordinates. The card element only has
to hold it and colour it: `fill: currentColor` means the red/black switch
below repaints every pip, index and court letter at once. */
.pete-card-front {
/* minmax(0,…) on the body row, or the pip grid's min-content height (seven
rows of glyphs) wins the argument and pushes the bottom index off the
card entirely. */
grid-template-rows: auto minmax(0, 1fr) auto;
overflow: hidden;
padding: 0.2rem 0.28rem;
place-items: stretch;
background: #fdfaf2;
border: 2px solid rgba(30,20,10,0.12);
color: #2b2118;
font-family: "Fredoka", "Nunito", system-ui, sans-serif;
line-height: 1;
overflow: hidden;
}
.pete-card-front[data-red="1"] { color: #cc3d4a; }
/* The corner index: rank over suit, tight. The bottom-right one is the same
thing upside down, because that's what makes a card readable from a fanned
hand held either way up. */
.pete-card-corner {
display: flex;
flex-direction: column;
align-items: center;
line-height: 0.95;
}
.pete-card-corner-tl { justify-self: start; }
.pete-card-corner-br { justify-self: end; transform: rotate(180deg); }
.pete-card-corner-rank { font-size: 0.9rem; font-weight: 700; }
.pete-card-corner-suit { font-size: 0.72rem; }
/* The pip field: three columns, seven rows — the skeleton every printed deck
shares. blackjack.js places each pip on it. */
.pete-card-pips {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(7, minmax(0, 1fr));
place-items: center;
min-height: 0;
width: 100%;
height: 100%;
padding: 0 0.08rem;
}
.pete-card-pip { font-size: 0.82rem; line-height: 1; }
.pete-card-pip[data-flip="1"] { transform: rotate(180deg); }
/* An ace is one pip, and it's allowed to be enormous. */
.pete-card-pips[data-ace="1"] .pete-card-pip { font-size: 2.1rem; }
/* Court cards: the letter with a suit over each shoulder. */
.pete-card-court {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.05rem;
}
.pete-card-court-letter {
font-size: 1.7rem;
font-weight: 700;
}
.pete-card-court-suit { font-size: 0.8rem; }
.pete-card-court-suit[data-flip="1"] { transform: rotate(180deg); }
/* The back. Turned away from you until the card is: this is what the dealer's
hole card shows while you're still acting. */
.pete-card-back {
transform: rotateY(180deg);
background:
@@ -669,6 +622,36 @@ html[data-phase="night"] {
border: 2px solid rgba(0,0,0,0.15);
}
.pete-card-svg {
width: 100%;
height: 100%;
display: block;
fill: currentColor;
}
/* The corner index. Tabular-ish and tight, so a 10 doesn't shove the suit. */
.pete-card-idx {
font-size: 26px;
font-weight: 700;
text-anchor: middle;
fill: currentColor;
}
/* The court panel: a frame, not a portrait. */
.pete-card-panel {
fill: currentColor;
fill-opacity: 0.06;
stroke: currentColor;
stroke-opacity: 0.35;
stroke-width: 1.5;
}
.pete-card-court {
font-size: 34px;
font-weight: 700;
text-anchor: middle;
fill: currentColor;
}
/* The flight itself: out of the shoe (up and to the right), scaled down and
spinning slightly, into place. */
@keyframes pete-deal {

File diff suppressed because one or more lines are too long

View File

@@ -73,96 +73,96 @@
return wrap;
}
// Pip layouts, the way a real deck lays them out — which is not "N suits in a
// row". Each entry is [column, row] on a 3×7 grid: three columns, seven rows,
// the same skeleton every printed deck has used for about four hundred years.
// A pip below the halfway line is printed upside down, so it does that here.
// ---- the face ------------------------------------------------------------
//
// Sevens and tens have a pip that sits *between* two rows, so those span a pair
// of rows and centre themselves across the pair.
// A card is drawn, not typed. The first attempt set the pips as text — "♠" in a
// span — and at the size a card actually is, a suit character renders as a
// speck: the shape is whatever font happened to answer, it doesn't scale, and
// it can't be positioned to the half-row a real pip layout needs.
//
// So each face is one SVG on a 100×140 field (the proportions of a real card),
// with the suits as vector shapes. Everything below is coordinates on that
// field, which is why the pips land where a printed deck puts them instead of
// where a flexbox felt like putting them.
var SUIT_ART = {
"♠": '<path d="M50 6C50 6 16 34 16 58a17 17 0 0 0 28 13c-1 12-5 19-12 25h36c-7-6-11-13-12-25a17 17 0 0 0 28-13C84 34 50 6 50 6Z"/>',
"♥": '<path d="M50 96C50 96 8 66 8 38A22 22 0 0 1 50 24 22 22 0 0 1 92 38c0 28-42 58-42 58Z"/>',
"♦": '<path d="M50 4 88 50 50 96 12 50Z"/>',
"♣": '<g><circle cx="50" cy="28" r="19"/><circle cx="24" cy="62" r="19"/><circle cx="76" cy="62" r="19"/>' +
'<path d="M44 60h12l7 36H37Z"/></g>',
};
// Pip layouts, the way a real deck lays them out — which is not "N suits in a
// row". [x, y] on the 100×140 field. The seven canonical rows sit at y = 27,
// 41, 56, 70, 84, 99, 113; sevens, eights and tens carry a pip *between* two of
// them, which is the whole reason this is a table of coordinates and not a
// grid. Anything below the middle is printed upside down, so it is.
var R = [0, 27, 41.4, 55.7, 70, 84.3, 98.6, 113]; // 1-indexed, R[4] is the middle
var L = 30, C = 50, Rr = 70; // the three columns
var PIPS = {
"A": [[2, 4]],
"2": [[2, 1], [2, 7]],
"3": [[2, 1], [2, 4], [2, 7]],
"4": [[1, 1], [3, 1], [1, 7], [3, 7]],
"5": [[1, 1], [3, 1], [2, 4], [1, 7], [3, 7]],
"6": [[1, 1], [3, 1], [1, 4], [3, 4], [1, 7], [3, 7]],
"7": [[1, 1], [3, 1], [2, "2/4"], [1, 4], [3, 4], [1, 7], [3, 7]],
"8": [[1, 1], [3, 1], [2, "2/4"], [1, 4], [3, 4], [2, "4/6"], [1, 7], [3, 7]],
"9": [[1, 1], [3, 1], [1, 3], [3, 3], [2, 4], [1, 5], [3, 5], [1, 7], [3, 7]],
"10": [[1, 1], [3, 1], [2, "2/4"], [1, 3], [3, 3], [1, 5], [3, 5], [2, "4/6"], [1, 7], [3, 7]],
"A": [[C, 70, 2.1]],
"2": [[C, R[1]], [C, R[7]]],
"3": [[C, R[1]], [C, R[4]], [C, R[7]]],
"4": [[L, R[1]], [Rr, R[1]], [L, R[7]], [Rr, R[7]]],
"5": [[L, R[1]], [Rr, R[1]], [C, R[4]], [L, R[7]], [Rr, R[7]]],
"6": [[L, R[1]], [Rr, R[1]], [L, R[4]], [Rr, R[4]], [L, R[7]], [Rr, R[7]]],
"7": [[L, R[1]], [Rr, R[1]], [C, 48.5], [L, R[4]], [Rr, R[4]], [L, R[7]], [Rr, R[7]]],
"8": [[L, R[1]], [Rr, R[1]], [C, 48.5], [L, R[4]], [Rr, R[4]], [C, 91.5], [L, R[7]], [Rr, R[7]]],
"9": [[L, R[1]], [Rr, R[1]], [L, R[3]], [Rr, R[3]], [C, R[4]], [L, R[5]], [Rr, R[5]], [L, R[7]], [Rr, R[7]]],
"10": [[L, R[1]], [Rr, R[1]], [C, 48.5], [L, R[3]], [Rr, R[3]], [L, R[5]], [Rr, R[5]], [C, 91.5], [L, R[7]], [Rr, R[7]]],
};
var COURT = { "J": "Jack", "Q": "Queen", "K": "King" };
function corner(face, where) {
var el = document.createElement("span");
el.className = "pete-card-corner pete-card-corner-" + where;
var r = document.createElement("span");
r.className = "pete-card-corner-rank";
r.textContent = face.rank;
var s = document.createElement("span");
s.className = "pete-card-corner-suit";
s.textContent = face.suit;
el.appendChild(r);
el.appendChild(s);
return el;
// One pip: the suit art, scaled and dropped at [x, y], turned over if it sits
// below the middle of the card.
function pipAt(suit, x, y, scale) {
var s = (scale || 1) * 0.17;
var turn = y > 70 ? " rotate(180 50 50)" : "";
return '<g transform="translate(' + x + ' ' + y + ') scale(' + s + ') translate(-50 -50)' + turn + '">' +
SUIT_ART[suit] + "</g>";
}
function pip(face, col, row) {
var el = document.createElement("span");
el.className = "pete-card-pip";
el.textContent = face.suit;
el.style.gridColumn = String(col);
// A row given as "2/4" spans that pair and centres between them.
el.style.gridRow = String(row);
// The bottom half of a real card is printed the other way up.
var mid = typeof row === "number" ? row > 4 : row === "4/6";
if (mid) el.dataset.flip = "1";
return el;
// The corner index: rank over suit. Printed in both corners, the second one
// upside down, which is what lets you read a card from a fanned hand.
function index(face) {
var g =
'<g>' +
'<text x="12" y="24" class="pete-card-idx">' + face.rank + "</text>" +
'<g transform="translate(12 36) scale(0.13) translate(-50 -50)">' + SUIT_ART[face.suit] + "</g>" +
"</g>";
return g + '<g transform="rotate(180 50 70)">' + g + "</g>";
}
// paintFace draws an actual playing card: index in two opposite corners, and
// either the pips or a court figure in the middle. The dealer's cards and
// yours use the same face, because they came out of the same shoe.
// paintFace draws the card. The dealer's cards and yours use the same face,
// because they came out of the same shoe.
function paintFace(front, face) {
front.dataset.red = face.red ? "1" : "0";
front.innerHTML = "";
front.appendChild(corner(face, "tl"));
var body = document.createElement("span");
var body = "";
if (COURT[face.rank]) {
// Court cards: the letter, big, with the suit sitting over each shoulder.
// No portrait — a drawn face would fight the room, and this reads instantly.
body.className = "pete-card-court";
var top = document.createElement("span");
top.className = "pete-card-court-suit";
top.textContent = face.suit;
var letter = document.createElement("span");
letter.className = "pete-card-court-letter";
letter.textContent = face.rank;
var bot = document.createElement("span");
bot.className = "pete-card-court-suit";
bot.dataset.flip = "1";
bot.textContent = face.suit;
body.appendChild(top);
body.appendChild(letter);
body.appendChild(bot);
// Court cards: a framed panel, the suit above the letter and again below it
// the other way up. A real court mirrors a *figure*; mirroring a letter just
// stacks two of them into a blob, which is exactly what the first attempt
// did. No portrait either — a drawn king would fight the room, and this
// reads instantly at the size a card actually is.
body =
'<rect x="20" y="22" width="60" height="96" rx="6" class="pete-card-panel"/>' +
pipAt(face.suit, 50, 38, 0.95) +
'<text x="50" y="82" class="pete-card-court">' + face.rank + "</text>" +
pipAt(face.suit, 50, 102, 0.95);
} else {
body.className = "pete-card-pips";
// An ace gets one big pip in the middle, the way an ace does.
if (face.rank === "A") body.dataset.ace = "1";
var spots = PIPS[face.rank] || [];
for (var i = 0; i < spots.length; i++) {
body.appendChild(pip(face, spots[i][0], spots[i][1]));
body += pipAt(face.suit, spots[i][0], spots[i][1], spots[i][2]);
}
}
front.appendChild(body);
front.appendChild(corner(face, "br"));
front.setAttribute("aria-label", ariaFor(face));
front.innerHTML =
'<svg class="pete-card-svg" viewBox="0 0 100 140" xmlns="http://www.w3.org/2000/svg" ' +
'role="img" aria-label="' + ariaFor(face) + '">' + index(face) + body + "</svg>";
}
// "A♠" is not something a screen reader should be asked to pronounce.