56233687b0
Lays out the lockstep migration from legacy white-card PNGs + constants to the Terminal aesthetic. Steps 4 + 5 (artwork + constant + test updates) must land in one commit so the PNG path and the constant-fallback path don't visually diverge. Tracks Option D from the SESSION_HANDOFF Resume prompt. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
252 lines
11 KiB
Markdown
252 lines
11 KiB
Markdown
# 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/<rank><suit>.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<Image, _>`
|
||
- 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/<theme>/`
|
||
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`.
|