//! Generates PNG assets for Ferrous Solitaire. //! //! 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, } 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."); }