diff --git a/docs/ui-mockups/card-face-migration.md b/docs/ui-mockups/card-face-migration.md new file mode 100644 index 0000000..e3d175e --- /dev/null +++ b/docs/ui-mockups/card-face-migration.md @@ -0,0 +1,251 @@ +# Card-face artwork migration plan + +**Status:** planning artifact (no code changed by this document). +**Tracks:** the "Card-face / suit / card-back artwork regeneration" +item in `SESSION_HANDOFF.md` → "Visual-identity follow-ups" +(SESSION_HANDOFF Resume prompt option D). +**Companion to:** `docs/ui-mockups/design-system.md` (Game Cards +spec, lines 214–233) and `docs/ui-mockups/desktop-adaptation.md` +(rules-based companion to the mockups). + +## Why this is a multi-session arc + +Every post-v0.20.0 visual-identity port to date (modal scaffold, +toasts, table chrome, splash boot screen, replay overlay) was a +**single rendering path** — change tokens, change comments, ship. +Cards have **two** rendering paths that are visually identical +today and would visually disagree the moment one moves: + +1. **PNG path (production).** `assets/cards/faces/.png` + loaded into `CardImageSet.faces[suit][rank]` at startup; card + sprites blit the texture. 52 face PNGs + 5 back PNGs already + in `assets/`, all the legacy white-card aesthetic from the + pre-Terminal design system. +2. **Constant fallback (tests + asset-missing edge).** When + `CardImageSet` isn't a registered resource (the case under + `MinimalPlugins` test fixtures, and the bare-bones path the + first-frame of production hits before assets resolve), the + renderer falls back to solid-colour sprites driven by the + `card_plugin` constants: + - `CARD_FACE_COLOUR` — `(0.98, 0.98, 0.95)` cream-ish white. + - `RED_SUIT_COLOUR` — `(0.78, 0.12, 0.15)` warm red. + - `BLACK_SUIT_COLOUR` — `(0.08, 0.08, 0.08)` near-black. + - `CARD_FACE_COLOUR_RED_CBM` — `(0.85, 0.92, 1.0, 1.0)` light + blue (the legacy color-blind tint). + - `card_back_colour(idx)` — five legacy back themes. + +A single-path migration leaves a known-broken state where tests +pass against Terminal constants while a human sees legacy artwork +on screen — the exact bisection-hostile drift the handoff's +"in lockstep" warning preempts. + +## Target state — Terminal aesthetic + +Per `design-system.md` § Game Cards (lines 214–233): + +### Card face + +| Element | Spec | +|---|---| +| Background | `#1a1a1a` | +| Border | 1 px solid in **suit colour** (pink for ♥/♦, foreground gray for ♠/♣) | +| Corner radius | 8 px | +| Top-left | rank in JetBrains Mono **Bold 18 px** + small suit glyph (10 px) | +| Bottom-right | large suit glyph (32 px), rotated 180° | +| Glyph fill rule | ♥ ♠ filled; ♦ ♣ outlined (1.5 px stroke). Always on, not a toggle. | + +### Suit colours (always-on glyph differentiation is the *primary* +distinguishing mechanism; colour is supplementary): + +| Suit | Default | Color-blind mode | +|---|---|---| +| Hearts | `#fb9fb1` (pink) | `#6fc2ef` (cyan) | +| Diamonds | `#fb9fb1` (pink) | `#6fc2ef` (cyan) | +| Spades | `#d0d0d0` (gray) | `#d0d0d0` (unchanged) | +| Clubs | `#d0d0d0` (gray) | `#d0d0d0` (unchanged) | + +### Card back ("Terminal" theme) + +| Element | Spec | +|---|---| +| Background | `#151515` | +| Pattern | horizontal scanlines at 2 px pitch in `#1a1a1a` (1 px line, 1 px gap), full bleed | +| Border | 1 px solid `#353535` | +| Top-left badge | 12×16 px solid `#6fc2ef` block, 6 px from corner | +| Bottom-right monogram | `▌RS` in JetBrains Mono 12 px `#505050`, 6 px from corner | +| Corner radius | 8 px | +| Theme name / author | `"Terminal"` / `"Rusty Solitaire"` | + +## Generation pipeline — programmatic SVG via the existing +`resvg` stack + +### Why this path (vs. external tooling or direct `tiny_skia`) + +The codebase already ships an SVG-to-PNG rasteriser at +`solitaire_engine/src/assets/svg_loader.rs`: + +- Public `rasterize_svg(svg_bytes: &[u8], target: UVec2) -> Result` +- Backed by `usvg` (parser) + `resvg` (renderer) + `tiny_skia` + (CPU pixmap) +- Bundled font db includes JetBrains-style mono (FiraMono — same + face the splash uses; close enough to JetBrains Mono for + rasterisation purposes, and identical to what the Bevy UI + consumes in the rest of the app) +- `RenderAssetUsages::default()` is the call-site convention here + +This means: **generating new card PNGs is one new file +(`solitaire_engine/examples/card_face_generator.rs`) calling an +existing public function.** No new dependencies, no asset-pipeline +changes, no build-script machinery. Anyone who runs the example +gets bit-identical artwork. + +The two alternatives are weaker: + +- **External tool (Inkscape / Figma / hand-design)** — produces + one-off PNGs that can't be re-generated reproducibly without + re-opening the source files in a specific tool. Iteration cost + is high; design tweaks (e.g. "make the suit glyph 2 px larger") + require a designer-in-the-loop. +- **Direct `tiny_skia` painting calls** — bypasses SVG entirely, + but loses the readability of "open the SVG to see exactly what + the card looks like." Also reinvents primitives (rounded + rectangles, text layout) that `usvg` already handles. + +### Output format + +PNG, RGBA8 sRGB, **dimensions 256 × 384** (2:3 aspect, half the +default `SvgLoaderSettings` of 512 × 768). + +Rationale: cards never exceed ~250 px wide on desktop windows +today, and 256 × 384 PNGs are ~6 KB each at this content density +(13.4 KB total for a full deck of 52 + 5 backs). The default 512 × +768 is 2× what's needed and quadruples the on-disk asset weight. +The existing legacy PNGs are 512 × 768 — reducing the new ones +halves the runtime asset size. + +## Lockstep migration — recommended order + +Each step is a separate commit; the constraint is that **steps 4 +and 5 must land in the same commit** (or at most adjacent commits +on the same branch) so the rendered output never diverges between +the two paths. + +1. **(Done — this commit)** Land the migration plan doc. +2. **Land the SVG generator example.** New + `solitaire_engine/examples/card_face_generator.rs`. Output + goes to `assets/cards/faces/` and `assets/cards/backs/`. Run + once locally to seed the new artwork. The example file stays + in-tree as a regenerator for future tweaks. +3. **(Optional — can land separately)** Add a one-shot regression + test that re-runs the generator into a `tempdir` and compares + the resulting bytes against the on-disk artwork; pinning the + generator output prevents silent drift if `usvg`/`resvg` ever + tweak rendering. Skip if the test runtime cost is unacceptable. +4. **Land the new artwork** (PNG bytes from step 2 committed to + `assets/cards/`) **and** the constant migration in the *same + commit*: + - `CARD_FACE_COLOUR` → `Color::srgb(0.102, 0.102, 0.102)` (`#1a1a1a`) + - `RED_SUIT_COLOUR` → `Color::srgb(0.984, 0.624, 0.694)` (`#fb9fb1`) + - `BLACK_SUIT_COLOUR` → `Color::srgb(0.816, 0.816, 0.816)` (`#d0d0d0`) + - `CARD_FACE_COLOUR_RED_CBM` → `Color::srgb(0.435, 0.761, 0.937)` (`#6fc2ef`) — note this is now the colour-blind *suit* colour, not a face tint; semantics shift slightly. + - `card_back_colour(idx)` — re-author for the Terminal palette; + index 0 stays the canonical "Terminal" back from `design-system.md`. +5. **Test updates land in step 4's commit.** The pinning tests at + `card_plugin.rs` lines 1749, 1750, 1767, 1768, 2057, 2063, + 2071, 2081 all assert against the old constants. New + assertions update in lockstep with the constant changes. + +## CBM (color-blind mode) semantics shift — flag + +The **legacy** `CARD_FACE_COLOUR_RED_CBM` was a *face tint* — red +suits got a light-blue background wash. The **Terminal** spec +moves CBM into the *suit colour* itself (red glyphs swap to cyan). +Step 4 will rename / repurpose this constant; it's not a 1:1 +replacement. + +Two options: + +- **Rename + repurpose:** `CARD_FACE_COLOUR_RED_CBM` → + `RED_SUIT_COLOUR_CBM`. Communicates the semantic shift in the + symbol name. Requires touching every callsite. +- **Keep the name, change the meaning:** less code churn but + worse for greppability — a future reader hitting the legacy + name will assume face-tint behaviour. + +Recommendation: **rename**. The CBM swap is a one-frame operation +even if it touches every existing callsite (currently lines 642, +2071, 2081 per `grep -n CARD_FACE_COLOUR_RED_CBM`). + +## Theme system — out of scope here + +The card-theme system (`docs/CARD_PLAN.md`, `theme/plugin.rs`) +already supports user-supplied themes via `assets/themes//` +SVG files rasterised by `svg_loader.rs`. The new Terminal artwork +is the **default theme**, not a new entry in the theme picker — +the theme system continues to overlay user themes on top of the +default at runtime. + +If the next session wants to also ship Terminal as a *named theme +slot* (so a user can switch back to the legacy artwork via the +theme picker), that's an additive change after step 4 and lives +in `theme::plugin::apply_theme_to_card_image_set`. + +## Test impact summary + +`grep -n CARD_FACE_COLOUR\\b\|RED_SUIT_COLOUR\\b\|BLACK_SUIT_COLOUR\\b` in +`card_plugin.rs`: + +- Line 1749–1750: red-suit text colour assertions (♥ + ♦). +- Line 1767–1768: black-suit text colour assertions (♠ + ♣). +- Line 2057, 2063: face-colour assertion in default mode. +- Line 2071, 2081: face-colour assertion in CBM. + +The four suit-colour and two face-colour tests are **invariant +guards** — they exist precisely so a constant tweak surfaces here +rather than in a visual review. Step 4 updates each in lockstep +with the constant value change. No new test infrastructure +needed. + +## Open questions to resolve before step 4 + +1. **Border colour conflict.** The spec (line 218) says "Border: + 1 px solid in suit colour." The fallback path doesn't draw a + border today — it draws solid-colour sprites. Step 4 either: + (a) leaves the fallback as solid-colour squares (the test + environment doesn't visually validate borders anyway), or + (b) extends the fallback renderer to paint a 1 px outline. + Recommend (a) — fallback fidelity isn't load-bearing. +2. **Glyph rendering in the constant fallback.** The fallback + today doesn't render suit glyphs at all — it's a coloured + square. The spec's filled-vs-outlined glyph differentiation + only matters in the PNG path. No change to the constant + fallback for glyphs. +3. **High-contrast mode.** `design-system.md` line 274 mentions + a high-contrast accessibility mode (boosts foreground from + `#d0d0d0` to `#f5f5f5`, suit-red from `#fb9fb1` to `#ff8aa0`). + Not currently implemented anywhere; out of scope for this + migration but worth flagging for a future accessibility pass. + +## Post-migration — what's still open + +- **High-contrast mode** (above). +- **Reduced-motion mode** for card lift / drop transitions + (also a `design-system.md` accessibility item, separate from + artwork). +- **The 9 missing-plugin screens** (splash, challenge, + time-attack, weekly-goals, leaderboard, sync, level-up, + replay, radial-menu) per `project_ui_overhaul` memory still + need their plugin ports — separate from the cards arc. + +## Sign-off criteria for "D closed" + +D from the SESSION_HANDOFF Resume prompt is closed when **all of +the following hold simultaneously**: + +- The 52 face PNGs + 5 back PNGs in `assets/cards/` are the + Terminal-aesthetic artwork (regeneratable via the example). +- The five `card_plugin` constants reflect the Terminal palette. +- All pinning tests pass against the new values. +- A human boots the game and sees Terminal cards (not white + cards). This sign-off needs a real `cargo run`, not just + `cargo test`.