diff --git a/solitaire_engine/examples/card_face_poc.rs b/solitaire_engine/examples/card_face_poc.rs new file mode 100644 index 0000000..a4bf87c --- /dev/null +++ b/solitaire_engine/examples/card_face_poc.rs @@ -0,0 +1,121 @@ +//! Card-face migration PoC — generates a Terminal-aesthetic Ace of +//! Spades to `/tmp/ace_spades_terminal.png`. +//! +//! Tracks `docs/ui-mockups/card-face-migration.md`'s recommended +//! generation pipeline (programmatic SVG via the existing +//! `usvg` + `resvg` + `tiny_skia` stack already used by +//! `solitaire_engine::assets::svg_loader`). One card is enough to +//! prove the pipeline; the next step in the migration loops the +//! same code over all 52 faces + 5 backs. +//! +//! Run with: +//! +//! ```sh +//! cargo run --example card_face_poc --release +//! ``` +//! +//! The example writes the PNG to `/tmp` rather than `assets/` so a +//! human can eyeball it before committing to a deck-wide rollout. +//! When the migration lands, the generator graduates into a real +//! binary that writes into `assets/cards/`. +//! +//! What this PoC proves: +//! +//! 1. The SVG-to-PNG pipeline produces valid output for a card-shaped +//! SVG (the existing rasteriser is used for full theme atlases +//! today; this confirms it works at the per-card grain). +//! 2. The Terminal palette (`#1a1a1a` background, `#d0d0d0` foreground +//! for spade glyphs) renders correctly. +//! 3. The bundled FiraMono in `svg_loader::shared_fontdb` resolves +//! `font-family="Fira Mono"` and renders both the rank "A" and +//! the Unicode suit glyph `♠` (U+2660). +//! 4. The 8 px corner radius (per `design-system.md`) renders as a +//! rounded rect at the 256×384 output size. +//! 5. SVG `transform="rotate(180 …)"` produces the bottom-right +//! flipped suit glyph called for in the spec. + +use bevy::math::UVec2; +use solitaire_engine::assets::rasterize_svg; +use tiny_skia::{IntSize, Pixmap}; + +fn main() { + let svg = ace_of_spades_svg(); + + // 256×384 = 2:3 aspect at half the default svg_loader resolution. + // See migration plan § "Output format" for the rationale. + let target = UVec2::new(256, 384); + let image = rasterize_svg(svg.as_bytes(), target) + .expect("rasterising the PoC SVG should succeed"); + + let bytes = image + .data + .expect("rasterized image must carry RGBA pixel data"); + assert_eq!( + bytes.len(), + (target.x * target.y * 4) as usize, + "raw byte count must match width × height × 4 RGBA bytes", + ); + + // Re-wrap the raw bytes in a `tiny_skia::Pixmap` so we can write + // a PNG via `save_png`. `rasterize_svg` already produced these + // bytes from a Pixmap inside `svg_loader`; this round-trip is + // the cost of going through Bevy's `Image` shape. + let size = IntSize::from_wh(target.x, target.y).expect("target size is non-zero"); + let pixmap = Pixmap::from_vec(bytes, size) + .expect("RGBA byte buffer should form a valid Pixmap"); + + let out = "/tmp/ace_spades_terminal.png"; + pixmap.save_png(out).expect("writing the PNG should succeed"); + + println!( + "Wrote {} ({}×{} RGBA8, {} bytes on disk)", + out, + target.x, + target.y, + std::fs::metadata(out).map(|m| m.len()).unwrap_or(0), + ); +} + +/// Builds the Ace-of-Spades SVG matching `design-system.md` +/// § Game Cards. The numbers below are the spec's logical sizes +/// scaled by 2× for the 256×384 output target (the spec describes +/// pixel sizes for a 128×192 logical card; doubling preserves the +/// visual proportions). +fn ace_of_spades_svg() -> String { + // Palette literals come straight from the design system. Quoted + // verbatim rather than constructed via the engine's `ui_theme` + // because the SVG renderer expects CSS colour strings, and a + // round-trip through `Color::srgb` → CSS would be both lossier + // and noisier than the inline string. + let bg = "#1a1a1a"; // BG_ELEVATED — card face + let suit = "#d0d0d0"; // TEXT_PRIMARY — spades use the foreground gray + + // Corner radius: 8 px logical → 16 px at 2× scale. + // Border: 1 px solid in suit colour → 2 px at 2× scale. + // Rank: JetBrains Mono Bold 18 px → 36 px. The bundled fontdb + // ships FiraMono only; usvg substitutes when JetBrains Mono + // isn't available, so we explicitly request `Fira Mono` to + // skip the substitution lookup. + // Small suit glyph: 10 px → 20 px. + // Large suit glyph: 32 px → 64 px, rotated 180° about its own + // centre so the bottom-right corner reads as the top-left + // when you flip the card. + format!( + r##" + + + + A + + + + +"## + ) +}