Files
Ferrous-Solitaire/solitaire_engine/examples/card_face_poc.rs
T
funman300 6e407a3ea7
Build and Deploy / build-and-push (push) Successful in 3m54s
fix(engine,server): safe area clamp, analytics batch, achievement save order, daily rollover, replay validation, leaderboard opt-in (#56, #60, #61, #62, #66, #68)
- #66: Clamp safe-area insets to 25% of window height with warn!() on excess
- #68: Move fire_flush outside per-event loop in analytics (batch flush once)
- #56: Persist progress before marking reward_granted to prevent XP loss on crash
- #60: Add DateRolloverTimer + check_date_rollover system for midnight seed refresh
- #62: Add validate_header() in replay upload with mode/draw_mode allowlists
- #61: Restore two-query leaderboard opt-in check (SELECT then UPDATE); original
       queries already in .sqlx cache; EXISTS variant would require sqlx prepare

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 13:07:22 -07:00

124 lines
5.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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}">&#x2660;</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)">&#x2660;</text>
</svg>"##
)
}