feat(engine): add card-face SVG generator PoC example
Rasterises one Ace of Spades to /tmp/ace_spades_terminal.png via the existing usvg + resvg + tiny_skia stack already used by svg_loader. Proves the per-card grain works before looping over all 52 faces + 5 backs in step 2 of the migration plan. Run with: cargo run --example card_face_poc --release Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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##"<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="1" y="1" width="254" height="382" rx="16" ry="16"
|
||||
fill="{bg}" stroke="{suit}" stroke-width="2"/>
|
||||
|
||||
<!-- Top-left rank + small suit glyph. -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="{suit}">A</text>
|
||||
<text x="14" y="68" font-family="Fira Mono" font-size="20"
|
||||
fill="{suit}">♠</text>
|
||||
|
||||
<!-- Bottom-right large suit glyph, rotated 180° about its own
|
||||
baseline anchor so the glyph reads upside-down. -->
|
||||
<text x="242" y="350" font-family="Fira Mono" font-size="64"
|
||||
fill="{suit}" text-anchor="end"
|
||||
transform="rotate(180 242 332)">♠</text>
|
||||
</svg>"##
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user