Files
Ferrous-Solitaire/solitaire_assetgen/src/bin/gen_art.rs
T
funman300 2b04718f33 feat(assetgen): upgrade card backs and backgrounds to 120×168 with richer patterns
Replace 16×16 placeholder PNGs with 120×168 canvas-drawn art matching card
face dimensions. Each card back has a distinctive coloured pattern (blue diamond
grid, red crosshatch, green circle array, purple concentric diamonds, teal
horizontal stripes). Each background has textured detail (green felt weave, wood
plank grain, navy star field, burgundy diagonal tile, charcoal checkerboard).
Removes the now-unused save_small_png/make_small helpers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 01:27:31 +00:00

714 lines
25 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.
//! Generates PNG assets for Solitaire Quest.
//!
//! Produces:
//! - 52 card face PNGs (120×168) — one per card, with rank, suit symbol, and
//! pip or face-letter layout baked in.
//! - 5 card back PNGs (120×168) with distinctive coloured patterns.
//! - 5 background PNGs (120×168) with textured felt/wood patterns.
//!
//! Run with: `cargo run -p solitaire_assetgen --bin gen_art`
use ab_glyph::{Font, FontRef, PxScale, ScaleFont};
use std::fs::File;
use std::io::BufWriter;
use std::path::Path;
// ---------------------------------------------------------------------------
// Card dimensions and palette
// ---------------------------------------------------------------------------
const W: u32 = 120;
const H: u32 = 168;
const BG: [u8; 4] = [0xFE, 0xFE, 0xF2, 0xFF];
const BORDER: [u8; 4] = [0x99, 0x99, 0x99, 0xFF];
const RED: [u8; 4] = [0xCC, 0x11, 0x11, 0xFF];
const DARK: [u8; 4] = [0x11, 0x11, 0x11, 0xFF];
fn suit_color(suit: u8) -> [u8; 4] {
if suit == 1 || suit == 2 { RED } else { DARK }
}
fn rank_str(rank: u8) -> &'static str {
["A","2","3","4","5","6","7","8","9","10","J","Q","K"][rank as usize]
}
// ---------------------------------------------------------------------------
// Pixel canvas (120×168 RGBA)
// ---------------------------------------------------------------------------
struct Canvas {
data: Vec<u8>,
}
impl Canvas {
fn new() -> Self {
let mut data = vec![0u8; (W * H * 4) as usize];
for i in 0..(W * H) as usize {
data[i * 4..i * 4 + 4].copy_from_slice(&BG);
}
Self { data }
}
/// Fill every pixel with a solid colour, erasing whatever was there before.
fn fill_solid(&mut self, c: [u8; 4]) {
for i in 0..(W * H) as usize {
self.data[i * 4..i * 4 + 4].copy_from_slice(&c);
}
}
/// Draw a 1-pixel-wide axis-aligned horizontal line.
fn hline(&mut self, y: i32, x0: i32, x1: i32, c: [u8; 4]) {
for x in x0..=x1 {
self.set(x, y, c);
}
}
/// Draw a 1-pixel-wide axis-aligned vertical line.
fn vline(&mut self, x: i32, y0: i32, y1: i32, c: [u8; 4]) {
for y in y0..=y1 {
self.set(x, y, c);
}
}
/// Draw a filled diamond outline (ring) of given half-extents and line thickness.
fn diamond_ring(&mut self, cx: f32, cy: f32, rx: f32, ry: f32, thickness: f32, c: [u8; 4]) {
for y in (cy - ry - 2.0) as i32..=(cy + ry + 2.0) as i32 {
for x in (cx - rx - 2.0) as i32..=(cx + rx + 2.0) as i32 {
let nx = (x as f32 - cx).abs() / rx;
let ny = (y as f32 - cy).abs() / ry;
let dist = nx + ny;
if dist <= 1.0 && dist >= 1.0 - (thickness / rx.min(ry)) {
self.set(x, y, c);
}
}
}
}
fn set(&mut self, x: i32, y: i32, c: [u8; 4]) {
if x < 0 || y < 0 || x >= W as i32 || y >= H as i32 { return; }
let i = (y as u32 * W + x as u32) as usize * 4;
let a = c[3] as f32 / 255.0;
if a >= 0.99 {
self.data[i..i + 4].copy_from_slice(&c);
} else if a > 0.01 {
self.data[i] = (self.data[i] as f32 * (1.0 - a) + c[0] as f32 * a) as u8;
self.data[i + 1] = (self.data[i + 1] as f32 * (1.0 - a) + c[1] as f32 * a) as u8;
self.data[i + 2] = (self.data[i + 2] as f32 * (1.0 - a) + c[2] as f32 * a) as u8;
self.data[i + 3] = 255;
}
}
fn circle(&mut self, cx: f32, cy: f32, r: f32, c: [u8; 4]) {
for y in (cy - r - 1.0) as i32..=(cy + r + 1.0) as i32 {
for x in (cx - r - 1.0) as i32..=(cx + r + 1.0) as i32 {
if (x as f32 - cx).powi(2) + (y as f32 - cy).powi(2) <= r * r {
self.set(x, y, c);
}
}
}
}
fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, c: [u8; 4]) {
for ry in y..y + h {
for rx in x..x + w {
self.set(rx, ry, c);
}
}
}
fn triangle(&mut self, pts: [(f32, f32); 3], c: [u8; 4]) {
let min_x = pts.iter().map(|p| p.0).fold(f32::INFINITY, f32::min) as i32;
let max_x = pts.iter().map(|p| p.0).fold(f32::NEG_INFINITY, f32::max) as i32;
let min_y = pts.iter().map(|p| p.1).fold(f32::INFINITY, f32::min) as i32;
let max_y = pts.iter().map(|p| p.1).fold(f32::NEG_INFINITY, f32::max) as i32;
let (ax, ay) = pts[0];
let (bx, by) = pts[1];
let (ex, ey) = pts[2];
for y in min_y..=max_y {
for x in min_x..=max_x {
let px = x as f32 + 0.5;
let py = y as f32 + 0.5;
let d0 = (bx - ax) * (py - ay) - (by - ay) * (px - ax);
let d1 = (ex - bx) * (py - by) - (ey - by) * (px - bx);
let d2 = (ax - ex) * (py - ey) - (ay - ey) * (px - ex);
let neg = d0 < 0.0 || d1 < 0.0 || d2 < 0.0;
let pos = d0 > 0.0 || d1 > 0.0 || d2 > 0.0;
if !(neg && pos) {
self.set(x, y, c);
}
}
}
}
fn diamond(&mut self, cx: f32, cy: f32, rx: f32, ry: f32, c: [u8; 4]) {
for y in (cy - ry - 1.0) as i32..=(cy + ry + 1.0) as i32 {
for x in (cx - rx - 1.0) as i32..=(cx + rx + 1.0) as i32 {
let nx = (x as f32 - cx).abs() / rx;
let ny = (y as f32 - cy).abs() / ry;
if nx + ny <= 1.0 {
self.set(x, y, c);
}
}
}
}
}
// ---------------------------------------------------------------------------
// Suit symbol drawing
// ---------------------------------------------------------------------------
fn draw_suit(cv: &mut Canvas, cx: f32, cy: f32, sz: f32, suit: u8, c: [u8; 4]) {
match suit {
0 => draw_club(cv, cx, cy, sz, c),
1 => draw_diamond_sym(cv, cx, cy, sz, c),
2 => draw_heart(cv, cx, cy, sz, c),
_ => draw_spade(cv, cx, cy, sz, c),
}
}
fn draw_heart(cv: &mut Canvas, cx: f32, cy: f32, sz: f32, c: [u8; 4]) {
let r = sz * 0.33;
let oy = cy - sz * 0.04;
cv.circle(cx - sz * 0.22, oy, r, c);
cv.circle(cx + sz * 0.22, oy, r, c);
cv.triangle([
(cx - sz * 0.52, oy + r * 0.4),
(cx + sz * 0.52, oy + r * 0.4),
(cx, cy + sz * 0.52),
], c);
}
fn draw_spade(cv: &mut Canvas, cx: f32, cy: f32, sz: f32, c: [u8; 4]) {
cv.triangle([
(cx, cy - sz * 0.52),
(cx - sz * 0.52, cy + sz * 0.1),
(cx + sz * 0.52, cy + sz * 0.1),
], c);
cv.circle(cx - sz * 0.22, cy + sz * 0.06, sz * 0.3, c);
cv.circle(cx + sz * 0.22, cy + sz * 0.06, sz * 0.3, c);
// stem + base
cv.triangle([
(cx, cy + sz * 0.12),
(cx - sz * 0.13, cy + sz * 0.5),
(cx + sz * 0.13, cy + sz * 0.5),
], c);
cv.fill_rect(
(cx - sz * 0.26) as i32,
(cy + sz * 0.43) as i32,
(sz * 0.52) as i32,
(sz * 0.1) as i32,
c,
);
}
fn draw_diamond_sym(cv: &mut Canvas, cx: f32, cy: f32, sz: f32, c: [u8; 4]) {
cv.diamond(cx, cy, sz * 0.44, sz * 0.57, c);
}
fn draw_club(cv: &mut Canvas, cx: f32, cy: f32, sz: f32, c: [u8; 4]) {
let r = sz * 0.29;
cv.circle(cx, cy - sz * 0.22, r, c);
cv.circle(cx - sz * 0.28, cy + sz * 0.1, r, c);
cv.circle(cx + sz * 0.28, cy + sz * 0.1, r, c);
cv.fill_rect(
(cx - sz * 0.08) as i32,
(cy + sz * 0.22) as i32,
(sz * 0.16) as i32 + 1,
(sz * 0.27) as i32,
c,
);
cv.fill_rect(
(cx - sz * 0.26) as i32,
(cy + sz * 0.45) as i32,
(sz * 0.52) as i32,
(sz * 0.09) as i32,
c,
);
}
// ---------------------------------------------------------------------------
// Text rendering via ab_glyph
// ---------------------------------------------------------------------------
fn draw_text(cv: &mut Canvas, font: &FontRef<'_>, text: &str, px: f32, left: f32, top: f32, c: [u8; 4]) {
let scale = PxScale::from(px);
let baseline = top + font.as_scaled(scale).ascent();
let mut x = left;
for ch in text.chars() {
let gid = font.glyph_id(ch);
let glyph = gid.with_scale_and_position(scale, ab_glyph::point(x, baseline));
let adv = font.as_scaled(scale).h_advance(gid);
if let Some(outlined) = font.outline_glyph(glyph) {
let bounds = outlined.px_bounds();
outlined.draw(|gx, gy, cov| {
if cov > 0.02 {
let alpha = (cov * c[3] as f32) as u8;
cv.set(
(bounds.min.x + gx as f32) as i32,
(bounds.min.y + gy as f32) as i32,
[c[0], c[1], c[2], alpha],
);
}
});
}
x += adv;
}
}
fn text_w(font: &FontRef<'_>, text: &str, px: f32) -> f32 {
let scale = PxScale::from(px);
let sf = font.as_scaled(scale);
text.chars().map(|c| sf.h_advance(font.glyph_id(c))).sum()
}
fn text_h(font: &FontRef<'_>, px: f32) -> f32 {
let scale = PxScale::from(px);
let sf = font.as_scaled(scale);
sf.ascent() - sf.descent()
}
// ---------------------------------------------------------------------------
// Pip layout (rank 0=Ace … 9=Ten; rank 10-12 are face cards)
// ---------------------------------------------------------------------------
fn pip_positions(rank: u8) -> &'static [(f32, f32)] {
match rank {
0 => &[(0.5, 0.5)],
1 => &[(0.5, 0.2), (0.5, 0.8)],
2 => &[(0.5, 0.12), (0.5, 0.5), (0.5, 0.88)],
3 => &[(0.25, 0.18), (0.75, 0.18), (0.25, 0.82), (0.75, 0.82)],
4 => &[(0.25, 0.18), (0.75, 0.18), (0.5, 0.5), (0.25, 0.82), (0.75, 0.82)],
5 => &[(0.25, 0.12), (0.75, 0.12), (0.25, 0.5), (0.75, 0.5), (0.25, 0.88), (0.75, 0.88)],
6 => &[(0.25, 0.1), (0.75, 0.1), (0.5, 0.31), (0.25, 0.5), (0.75, 0.5), (0.25, 0.9), (0.75, 0.9)],
7 => &[(0.25, 0.1), (0.75, 0.1), (0.5, 0.28), (0.25, 0.48), (0.75, 0.48), (0.5, 0.70), (0.25, 0.9), (0.75, 0.9)],
8 => &[(0.25, 0.1), (0.75, 0.1), (0.25, 0.35), (0.75, 0.35), (0.5, 0.5), (0.25, 0.65), (0.75, 0.65), (0.25, 0.9), (0.75, 0.9)],
9 => &[(0.25, 0.09), (0.75, 0.09), (0.5, 0.27), (0.25, 0.44), (0.75, 0.44), (0.25, 0.56), (0.75, 0.56), (0.5, 0.73), (0.25, 0.91), (0.75, 0.91)],
_ => &[],
}
}
// Pip area within the card (avoids the corner labels).
const PIP_X: f32 = 22.0;
const PIP_Y: f32 = 46.0;
const PIP_W: f32 = 76.0;
const PIP_H: f32 = 80.0;
// ---------------------------------------------------------------------------
// Card face generation
// ---------------------------------------------------------------------------
fn make_card_face(font: &FontRef<'_>, rank: u8, suit: u8) -> Canvas {
let mut cv = Canvas::new();
let sc = suit_color(suit);
// Border (2 px)
for x in 0..W as i32 {
cv.set(x, 0, BORDER);
cv.set(x, 1, BORDER);
cv.set(x, H as i32 - 2, BORDER);
cv.set(x, H as i32 - 1, BORDER);
}
for y in 0..H as i32 {
cv.set(0, y, BORDER);
cv.set(1, y, BORDER);
cv.set(W as i32 - 2, y, BORDER);
cv.set(W as i32 - 1, y, BORDER);
}
let rank_s = rank_str(rank);
let rank_px = 18.0f32;
let suit_sz = 11.0f32;
let rh = text_h(font, rank_px);
let rw = text_w(font, rank_s, rank_px);
let corner_h = rh + 2.0 + suit_sz * 1.5;
// Top-left corner
let tl_x = 6.0f32;
let tl_y = 5.0f32;
draw_text(&mut cv, font, rank_s, rank_px, tl_x, tl_y, sc);
draw_suit(&mut cv, tl_x + suit_sz * 0.62, tl_y + rh + 2.0 + suit_sz * 0.75, suit_sz, suit, sc);
// Bottom-right corner (right-aligned rank, suit above it)
let br_rx = W as f32 - 6.0;
let br_by = H as f32 - 5.0;
let br_ty = br_by - corner_h;
draw_text(&mut cv, font, rank_s, rank_px, br_rx - rw, br_ty, sc);
draw_suit(&mut cv, br_rx - suit_sz * 0.62, br_ty + rh + 2.0 + suit_sz * 0.75, suit_sz, suit, sc);
// Center content
if rank >= 10 {
// Face cards: large rank letter + suit symbol below
let big_px = 52.0f32;
let big_w = text_w(font, rank_s, big_px);
let big_h = text_h(font, big_px);
let big_x = (W as f32 - big_w) / 2.0;
let big_y = H as f32 * 0.28;
draw_text(&mut cv, font, rank_s, big_px, big_x, big_y, sc);
let sym_sz = 22.0f32;
draw_suit(&mut cv, W as f32 * 0.5, big_y + big_h + sym_sz * 1.0, sym_sz, suit, sc);
} else {
// Pip cards
let pip_sz = if rank == 0 {
24.0f32 // Ace: large single pip
} else if rank <= 5 {
14.0
} else {
12.0
};
for &(nx, ny) in pip_positions(rank) {
let cx = PIP_X + nx * PIP_W;
let cy = PIP_Y + ny * PIP_H;
draw_suit(&mut cv, cx, cy, pip_sz, suit, sc);
}
}
cv
}
// ---------------------------------------------------------------------------
// PNG encoding helpers
// ---------------------------------------------------------------------------
fn save_card_png(path: &Path, cv: &Canvas) {
save_png_wh(path, &cv.data, W, H);
}
fn save_png_wh(path: &Path, data: &[u8], w: u32, h: u32) {
let file = File::create(path)
.unwrap_or_else(|e| panic!("cannot create {}: {e}", path.display()));
let mut bw = BufWriter::new(file);
let mut enc = png::Encoder::new(&mut bw, w, h);
enc.set_color(png::ColorType::Rgba);
enc.set_depth(png::BitDepth::Eight);
let mut writer = enc.write_header()
.unwrap_or_else(|e| panic!("png header error for {}: {e}", path.display()));
writer.write_image_data(data)
.unwrap_or_else(|e| panic!("png data error for {}: {e}", path.display()));
}
// ---------------------------------------------------------------------------
// Card backs (120×168 with distinctive patterns)
// ---------------------------------------------------------------------------
/// back_0 blue: repeating diamond grid pattern
fn make_back_0() -> Canvas {
const BASE: [u8; 4] = [0x26, 0x4D, 0x8C, 0xFF];
const LIGHT: [u8; 4] = [0x5A, 0x80, 0xBF, 0xFF];
const HIGHLIGHT: [u8; 4] = [0xA0, 0xC0, 0xFF, 0xB0];
let mut cv = Canvas::new();
cv.fill_solid(BASE);
// 2-pixel border
let bw = 4i32;
for x in 0..W as i32 { for t in 0..bw { cv.set(x, t, LIGHT); cv.set(x, H as i32 - 1 - t, LIGHT); } }
for y in 0..H as i32 { for t in 0..bw { cv.set(t, y, LIGHT); cv.set(W as i32 - 1 - t, y, LIGHT); } }
// Diamond grid: row/col spacing
let gx = 18.0f32;
let gy = 18.0f32;
let rx = gx * 0.45;
let ry = gy * 0.45;
let mut row = 0;
let mut cy = 6.0f32 + gy * 0.5;
while cy < H as f32 - 4.0 {
let offset = if row % 2 == 0 { 0.0 } else { gx * 0.5 };
let mut cx = 6.0f32 + gx * 0.5 + offset;
while cx < W as f32 - 4.0 {
cv.diamond_ring(cx, cy, rx, ry, 1.5, LIGHT);
// tiny highlight dot at centre of each diamond
cv.circle(cx, cy, 1.5, HIGHLIGHT);
cx += gx;
}
cy += gy;
row += 1;
}
cv
}
/// back_1 red: diagonal crosshatch
fn make_back_1() -> Canvas {
const BASE: [u8; 4] = [0x8C, 0x1A, 0x1A, 0xFF];
const LINE: [u8; 4] = [0xCC, 0x55, 0x55, 0xC0];
const BORDER: [u8; 4] = [0xDD, 0x88, 0x88, 0xFF];
let mut cv = Canvas::new();
cv.fill_solid(BASE);
// Diagonal lines every 12 px (NW→SE)
let spacing = 12i32;
for k in (-(H as i32)..W as i32).step_by(spacing as usize) {
for t in 0..W as i32 {
let y = t + k;
cv.set(t, y, LINE);
// 1 px thick — also set neighbour for slightly bolder line
cv.set(t, y + 1, LINE);
}
}
// Diagonal lines (NE→SW)
for k in (0..(W as i32 + H as i32)).step_by(spacing as usize) {
for t in 0..W as i32 {
let y = k - t;
cv.set(t, y, LINE);
cv.set(t, y + 1, LINE);
}
}
// 4-pixel border
let bw = 4i32;
for x in 0..W as i32 { for t in 0..bw { cv.set(x, t, BORDER); cv.set(x, H as i32 - 1 - t, BORDER); } }
for y in 0..H as i32 { for t in 0..bw { cv.set(t, y, BORDER); cv.set(W as i32 - 1 - t, y, BORDER); } }
cv
}
/// back_2 green: evenly spaced small circle array
fn make_back_2() -> Canvas {
const BASE: [u8; 4] = [0x0D, 0x66, 0x1A, 0xFF];
const DOT: [u8; 4] = [0x40, 0xCC, 0x55, 0xE0];
const BORDER: [u8; 4] = [0x55, 0xDD, 0x66, 0xFF];
let mut cv = Canvas::new();
cv.fill_solid(BASE);
// 4-pixel border
let bw = 4i32;
for x in 0..W as i32 { for t in 0..bw { cv.set(x, t, BORDER); cv.set(x, H as i32 - 1 - t, BORDER); } }
for y in 0..H as i32 { for t in 0..bw { cv.set(t, y, BORDER); cv.set(W as i32 - 1 - t, y, BORDER); } }
// Circle array (staggered rows)
let gx = 16.0f32;
let gy = 16.0f32;
let r = 3.5f32;
let mut row = 0;
let mut cy = 8.0f32 + gy * 0.5;
while cy < H as f32 - 6.0 {
let offset = if row % 2 == 0 { 0.0 } else { gx * 0.5 };
let mut cx = 8.0f32 + gx * 0.5 + offset;
while cx < W as f32 - 6.0 {
cv.circle(cx, cy, r, DOT);
cx += gx;
}
cy += gy;
row += 1;
}
cv
}
/// back_3 purple: concentric diamond outlines
fn make_back_3() -> Canvas {
const BASE: [u8; 4] = [0x59, 0x14, 0x85, 0xFF];
const RING: [u8; 4] = [0xA0, 0x60, 0xDD, 0xD0];
const BORDER: [u8; 4] = [0xBB, 0x77, 0xFF, 0xFF];
let mut cv = Canvas::new();
cv.fill_solid(BASE);
// Concentric diamonds from centre
let cx = W as f32 * 0.5;
let cy = H as f32 * 0.5;
let mut rx = 8.0f32;
let step = 12.0f32;
while rx < (W as f32).max(H as f32) {
let ry = rx * (H as f32 / W as f32);
cv.diamond_ring(cx, cy, rx, ry, 1.5, RING);
rx += step;
}
// 4-pixel border
let bw = 4i32;
for x in 0..W as i32 { for t in 0..bw { cv.set(x, t, BORDER); cv.set(x, H as i32 - 1 - t, BORDER); } }
for y in 0..H as i32 { for t in 0..bw { cv.set(t, y, BORDER); cv.set(W as i32 - 1 - t, y, BORDER); } }
cv
}
/// back_4 teal: horizontal stripes with thin decorative lines
fn make_back_4() -> Canvas {
const BASE: [u8; 4] = [0x0D, 0x66, 0x6B, 0xFF];
const STRIPE: [u8; 4] = [0x1A, 0x99, 0xA0, 0x90];
const DECO: [u8; 4] = [0x55, 0xCC, 0xD4, 0xA0];
const BORDER: [u8; 4] = [0x44, 0xBB, 0xC4, 0xFF];
let mut cv = Canvas::new();
cv.fill_solid(BASE);
// Horizontal stripes every 10 px (2 px wide)
let mut y = 6i32;
while y < H as i32 - 4 {
cv.hline(y, 5, W as i32 - 6, STRIPE);
cv.hline(y + 1, 5, W as i32 - 6, STRIPE);
y += 10;
}
// Thin decorative horizontal lines between stripes
let mut y = 10i32;
while y < H as i32 - 4 {
cv.hline(y, 14, W as i32 - 15, DECO);
y += 10;
}
// 4-pixel border
let bw = 4i32;
for x in 0..W as i32 { for t in 0..bw { cv.set(x, t, BORDER); cv.set(x, H as i32 - 1 - t, BORDER); } }
for y in 0..H as i32 { for t in 0..bw { cv.set(t, y, BORDER); cv.set(W as i32 - 1 - t, y, BORDER); } }
cv
}
// ---------------------------------------------------------------------------
// Backgrounds (120×168 textured patterns)
// ---------------------------------------------------------------------------
/// bg_0 dark green felt: subtle grid of faint lines giving a woven texture
fn make_bg_0() -> Canvas {
const BASE: [u8; 4] = [0x1A, 0x4D, 0x1A, 0xFF];
const WARP: [u8; 4] = [0x22, 0x60, 0x22, 0x90]; // slightly lighter horizontal threads
const WEFT: [u8; 4] = [0x15, 0x40, 0x15, 0x90]; // slightly darker vertical threads
let mut cv = Canvas::new();
cv.fill_solid(BASE);
// Horizontal warp lines every 4 px
for y in (0..H as i32).step_by(4) {
cv.hline(y, 0, W as i32 - 1, WARP);
}
// Vertical weft lines every 4 px
for x in (0..W as i32).step_by(4) {
cv.vline(x, 0, H as i32 - 1, WEFT);
}
cv
}
/// bg_1 wood brown: horizontal planks with grain lines
fn make_bg_1() -> Canvas {
const BASE: [u8; 4] = [0x40, 0x2D, 0x1A, 0xFF];
const PLANK_EDGE: [u8; 4] = [0x28, 0x1A, 0x0A, 0xFF]; // dark plank separator
const GRAIN: [u8; 4] = [0x55, 0x3D, 0x28, 0xA0]; // lighter grain streak
let mut cv = Canvas::new();
cv.fill_solid(BASE);
// Horizontal plank edges every 24 px
for y in (0..H as i32).step_by(24) {
cv.hline(y, 0, W as i32 - 1, PLANK_EDGE);
cv.hline(y + 1, 0, W as i32 - 1, PLANK_EDGE);
}
// Grain lines within each plank (every 3 px between plank edges)
for y in (0..H as i32).step_by(3) {
// Skip the plank edge rows
if y % 24 < 2 { continue; }
cv.hline(y, 2, W as i32 - 3, GRAIN);
}
cv
}
/// bg_2 navy: star-field dots scattered in a regular grid
fn make_bg_2() -> Canvas {
const BASE: [u8; 4] = [0x0D, 0x14, 0x38, 0xFF];
const STAR_A: [u8; 4] = [0xCC, 0xDD, 0xFF, 0xD0];
const STAR_B: [u8; 4] = [0x80, 0xA0, 0xDD, 0x80];
let mut cv = Canvas::new();
cv.fill_solid(BASE);
// Bright small stars on a staggered grid
let gx = 14.0f32;
let gy = 16.0f32;
let mut row = 0u32;
let mut cy = gy * 0.5;
while cy < H as f32 {
let offset = if row.is_multiple_of(2) { 0.0 } else { gx * 0.5 };
let mut cx = gx * 0.5 + offset;
while cx < W as f32 {
// alternate bright/dim to give depth
let c = if (row + (cx / gx) as u32).is_multiple_of(3) { STAR_A } else { STAR_B };
cv.circle(cx, cy, 1.0, c);
cx += gx;
}
cy += gy;
row += 1;
}
cv
}
/// bg_3 burgundy: diagonal tile pattern
fn make_bg_3() -> Canvas {
const BASE: [u8; 4] = [0x4D, 0x0D, 0x14, 0xFF];
const LINE: [u8; 4] = [0x77, 0x22, 0x30, 0xB0];
const ACCENT: [u8; 4] = [0x99, 0x33, 0x44, 0x80];
let mut cv = Canvas::new();
cv.fill_solid(BASE);
// Diagonal lines in one direction every 16 px
let spacing = 16i32;
for k in (-(H as i32)..W as i32 + H as i32).step_by(spacing as usize) {
for t in 0..W as i32 {
let y = t + k;
cv.set(t, y, LINE);
}
}
// Diagonal lines in the other direction every 16 px (accent colour)
for k in (0..W as i32 + H as i32).step_by(spacing as usize) {
for t in 0..W as i32 {
let y = k - t;
cv.set(t, y, ACCENT);
}
}
cv
}
/// bg_4 charcoal: subtle checkerboard texture
fn make_bg_4() -> Canvas {
const DARK: [u8; 4] = [0x1F, 0x1F, 0x24, 0xFF];
const LIGHT: [u8; 4] = [0x2C, 0x2C, 0x33, 0xFF];
let mut cv = Canvas::new();
cv.fill_solid(DARK);
// 4×4 checkerboard
for y in 0..H as i32 {
for x in 0..W as i32 {
if ((x / 4) + (y / 4)) % 2 == 0 {
cv.set(x, y, LIGHT);
}
}
}
cv
}
// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------
fn workspace_root() -> std::path::PathBuf {
let crate_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
crate_dir.parent().unwrap().to_path_buf()
}
fn main() {
let root = workspace_root();
std::fs::create_dir_all(root.join("assets/cards/faces")).unwrap();
std::fs::create_dir_all(root.join("assets/cards/backs")).unwrap();
std::fs::create_dir_all(root.join("assets/backgrounds")).unwrap();
// Load font from disk (dev tool — runtime load is fine here).
let font_path = root.join("assets/fonts/main.ttf");
let font_bytes = std::fs::read(&font_path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", font_path.display()));
let font = FontRef::try_from_slice(&font_bytes)
.expect("failed to parse assets/fonts/main.ttf");
// 52 card faces
let suits = ["c", "d", "h", "s"];
let ranks = ["a","2","3","4","5","6","7","8","9","10","j","q","k"];
for suit in 0u8..4 {
for rank in 0u8..13 {
let cv = make_card_face(&font, rank, suit);
let name = format!("{}_{}.png", ranks[rank as usize], suits[suit as usize]);
let path = root.join("assets/cards/faces").join(&name);
save_card_png(&path, &cv);
println!("wrote {}", path.display());
}
}
// Card backs
for (i, cv) in [make_back_0(), make_back_1(), make_back_2(), make_back_3(), make_back_4()].iter().enumerate() {
let path = root.join(format!("assets/cards/backs/back_{i}.png"));
save_card_png(&path, cv);
println!("wrote {}", path.display());
}
// Backgrounds
for (i, cv) in [make_bg_0(), make_bg_1(), make_bg_2(), make_bg_3(), make_bg_4()].iter().enumerate() {
let path = root.join(format!("assets/backgrounds/bg_{i}.png"));
save_card_png(&path, cv);
println!("wrote {}", path.display());
}
println!("gen_art: all assets generated successfully.");
}