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