//! 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 "## ) }