// The deck, as the room draws it. // // 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. // // This started life inside blackjack.js. It's out here because solitaire deals // off the same deck, and the second table in a casino is exactly the moment a // copied card renderer becomes two card renderers that drift. // // Exposed as window.PeteCards. Nothing in here knows what game is being played. (function () { "use strict"; var SUIT_ART = { "♠": '', "♥": '', "♦": '', "♣": '' + '', }; // 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": [[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" }; var SUIT_NAMES = { "♠": "spades", "♥": "hearts", "♦": "diamonds", "♣": "clubs" }; // 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 '' + SUIT_ART[suit] + ""; } // 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 — and in // solitaire, from a column where all you can see is the top eighth of it. function index(face) { var g = '' + '' + face.rank + "" + '' + SUIT_ART[face.suit] + "" + ""; return g + '' + g + ""; } // paint draws the face. Every table uses this one, because they all deal out of // the same deck. function paint(front, face) { front.dataset.red = face.red ? "1" : "0"; var body = ""; if (COURT[face.rank]) { // 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 = '' + pipAt(face.suit, 50, 38, 0.95) + '' + face.rank + "" + pipAt(face.suit, 50, 102, 0.95); } else { var spots = PIPS[face.rank] || []; for (var i = 0; i < spots.length; i++) { body += pipAt(face.suit, spots[i][0], spots[i][1], spots[i][2]); } } front.innerHTML = '' + index(face) + body + ""; } // "A♠" is not something a screen reader should be asked to pronounce. function aria(face) { var name = COURT[face.rank] || (face.rank === "A" ? "Ace" : face.rank); var suit = SUIT_NAMES[face.suit]; return suit ? name + " of " + suit : face.label; } var dealt = 0; // how many cards this page has put down, ever — the tilt seed // el builds one card. face === null means face-down: the card is on the table, // but this browser has not been told what it is. // // opts.deal — fly it in from the shoe (blackjack). Solitaire turns this off: // it animates its own cards from wherever they actually came from, // and a board that re-renders would otherwise re-deal itself out of // the corner on every single move. // opts.tilt — a degree or two of resting angle. A dealt hand wants it; a // solitaire column does not, because thirteen tilted cards stacked // an eighth of an inch apart read as a mistake rather than as a // hand that was handled. function el(face, opts) { opts = opts || {}; var deal = opts.deal !== false; var tilt = opts.tilt !== false; var wrap = document.createElement("div"); wrap.className = "pete-card"; wrap.dataset.face = face ? "up" : "down"; if (face) wrap.dataset.key = face.label; // one deck, so the label is an id if (deal) { // Every card flies out of the shoe, which sits in the top-right of the felt. wrap.style.setProperty("--deal-x", "14rem"); wrap.style.setProperty("--deal-y", "-6rem"); } else { wrap.style.animation = "none"; } wrap.style.setProperty("--tilt", tilt ? window.PeteFX.jitter(dealt++, 2.4).toFixed(2) + "deg" : "0deg"); var inner = document.createElement("div"); inner.className = "pete-card-inner"; var front = document.createElement("div"); front.className = "pete-card-front"; var back = document.createElement("div"); back.className = "pete-card-back"; inner.appendChild(front); inner.appendChild(back); wrap.appendChild(inner); if (face) paint(front, face); return wrap; } // turnOver flips a face-down card up, now that we've been told what it is. function turnOver(wrap, face) { if (!wrap) return; paint(wrap.querySelector(".pete-card-front"), face); wrap.dataset.face = "up"; wrap.dataset.key = face.label; } window.PeteCards = { el: el, paint: paint, turnOver: turnOver, aria: aria, art: SUIT_ART, }; })();