feat(web): add classic/dark card theme picker
Build and Deploy / build-and-push (push) Successful in 4m10s

- Reorganise card PNGs into assets/cards/faces/{classic,dark}/ and
  assets/cards/backs/{classic,dark}/
- Rasterise dark SVG theme alongside existing classic set
- Add "Dark / Classic" toggle button in the game HUD; persists to
  localStorage as fs_theme (defaults to classic)
- Preload both themes on page load so switching is instant

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-14 16:55:43 -07:00
parent 2bf990388b
commit 8d31a37a39
113 changed files with 51 additions and 32 deletions
+1
View File
@@ -39,6 +39,7 @@
<label class="toggle-label" title="Draw one or three cards">
<input type="checkbox" id="chk-draw3"> Draw 3
</label>
<button id="btn-theme" title="Switch card theme">Dark</button>
</div>
</header>
+26 -8
View File
@@ -53,15 +53,21 @@ const SUIT_CODE = { clubs: "C", diamonds: "D", hearts: "H", spades: "S" };
const RANK_LABELS = ["","A","2","3","4","5","6","7","8","9","10","J","Q","K"];
const RED_SUITS = new Set(["diamonds", "hearts"]);
// Preload all card images so face-up transitions never flash white.
(function preloadCards() {
// ── Theme ─────────────────────────────────────────────────────────────────────
let cardTheme = localStorage.getItem("fs_theme") || "classic";
function preloadTheme(theme) {
const suits = Object.values(SUIT_CODE);
const ranks = RANK_LABELS.slice(1); // skip empty index 0
const ranks = RANK_LABELS.slice(1);
for (const r of ranks) for (const s of suits) {
new Image().src = `/assets/cards/faces/${r}${s}.png`;
new Image().src = `/assets/cards/faces/${theme}/${r}${s}.png`;
}
new Image().src = "/assets/cards/backs/back_0.png";
}());
new Image().src = `/assets/cards/backs/${theme}/back_0.png`;
}
// Preload both themes on load so switching is instant.
preloadTheme("classic");
preloadTheme("dark");
// ── State ────────────────────────────────────────────────────────────────────
let game = null;
@@ -100,6 +106,7 @@ const hudSeed = document.getElementById("hud-seed");
const btnUndo = document.getElementById("btn-undo");
const btnNew = document.getElementById("btn-new");
const chkDraw3 = document.getElementById("chk-draw3");
const btnTheme = document.getElementById("btn-theme");
const winOverlay = document.getElementById("win-overlay");
const winScore = document.getElementById("win-score");
const winMoves = document.getElementById("win-moves");
@@ -121,9 +128,14 @@ function scaleBoard() {
board.style.transformOrigin = "center center";
}
function syncThemeButton() {
if (btnTheme) btnTheme.textContent = cardTheme === "classic" ? "Dark" : "Classic";
}
// ── Bootstrap ────────────────────────────────────────────────────────────────
async function bootstrap() {
await init();
syncThemeButton();
const params = new URLSearchParams(window.location.search);
const urlSeed = params.has("seed") ? Number(params.get("seed")) : randomSeed();
@@ -302,12 +314,12 @@ function updateCardEl(el, card, pileName, idx, total) {
if (!card.face_up) {
el.className = "card face-down";
el.style.backgroundImage = "url('/assets/cards/backs/back_0.png')";
el.style.backgroundImage = `url('/assets/cards/backs/${cardTheme}/back_0.png')`;
el.innerHTML = "";
} else {
const isRed = RED_SUITS.has(card.suit);
el.className = `card ${isRed ? "red" : "black"}`;
el.style.backgroundImage = `url('/assets/cards/faces/${RANK_LABELS[card.rank]}${SUIT_CODE[card.suit]}.png')`;
el.style.backgroundImage = `url('/assets/cards/faces/${cardTheme}/${RANK_LABELS[card.rank]}${SUIT_CODE[card.suit]}.png')`;
el.innerHTML = "";
}
}
@@ -383,6 +395,12 @@ function attachHandlers() {
drawThree = chkDraw3.checked;
startGame(randomSeed());
});
btnTheme.addEventListener("click", () => {
cardTheme = cardTheme === "classic" ? "dark" : "classic";
localStorage.setItem("fs_theme", cardTheme);
syncThemeButton();
if (snap) render(snap);
});
document.addEventListener("keydown", (e) => {
if (e.target.tagName === "INPUT") return;