docs(ui): add card-face artwork migration plan
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>
This commit is contained in:
@@ -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/<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`.
|
||||
Reference in New Issue
Block a user