diff --git a/assets/backgrounds/bg_0.png b/assets/backgrounds/bg_0.png index 8848cb6..c75dc16 100644 Binary files a/assets/backgrounds/bg_0.png and b/assets/backgrounds/bg_0.png differ diff --git a/assets/backgrounds/bg_1.png b/assets/backgrounds/bg_1.png index c5d24dd..ef9525e 100644 Binary files a/assets/backgrounds/bg_1.png and b/assets/backgrounds/bg_1.png differ diff --git a/assets/backgrounds/bg_2.png b/assets/backgrounds/bg_2.png index 8501a49..2023a48 100644 Binary files a/assets/backgrounds/bg_2.png and b/assets/backgrounds/bg_2.png differ diff --git a/assets/backgrounds/bg_3.png b/assets/backgrounds/bg_3.png index c61c299..7302d3c 100644 Binary files a/assets/backgrounds/bg_3.png and b/assets/backgrounds/bg_3.png differ diff --git a/assets/backgrounds/bg_4.png b/assets/backgrounds/bg_4.png index a0413dc..382637c 100644 Binary files a/assets/backgrounds/bg_4.png and b/assets/backgrounds/bg_4.png differ diff --git a/assets/cards/backs/back_0.png b/assets/cards/backs/back_0.png index e49bed3..b673bf0 100644 Binary files a/assets/cards/backs/back_0.png and b/assets/cards/backs/back_0.png differ diff --git a/assets/cards/backs/back_1.png b/assets/cards/backs/back_1.png index db1e94b..8cc06c0 100644 Binary files a/assets/cards/backs/back_1.png and b/assets/cards/backs/back_1.png differ diff --git a/assets/cards/backs/back_2.png b/assets/cards/backs/back_2.png index 253fd51..8346fa9 100644 Binary files a/assets/cards/backs/back_2.png and b/assets/cards/backs/back_2.png differ diff --git a/assets/cards/backs/back_3.png b/assets/cards/backs/back_3.png index 0532928..ac070a0 100644 Binary files a/assets/cards/backs/back_3.png and b/assets/cards/backs/back_3.png differ diff --git a/assets/cards/backs/back_4.png b/assets/cards/backs/back_4.png index 24d78fe..9f16ff8 100644 Binary files a/assets/cards/backs/back_4.png and b/assets/cards/backs/back_4.png differ diff --git a/solitaire_assetgen/src/bin/gen_art.rs b/solitaire_assetgen/src/bin/gen_art.rs index f084f33..e4fa3d7 100644 --- a/solitaire_assetgen/src/bin/gen_art.rs +++ b/solitaire_assetgen/src/bin/gen_art.rs @@ -3,8 +3,8 @@ //! 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 (16×16 placeholder patterns). -//! - 5 background PNGs (16×16 placeholder patterns). +//! - 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` @@ -50,6 +50,41 @@ impl Canvas { 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; @@ -352,67 +387,277 @@ fn save_png_wh(path: &Path, data: &[u8], w: u32, h: u32) { .unwrap_or_else(|e| panic!("png data error for {}: {e}", path.display())); } -fn save_small_png(path: &Path, pixels: &[u8; 1024]) { - save_png_wh(path, pixels, 16, 16); +// --------------------------------------------------------------------------- +// 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 } -fn make_small [u8; 4]>(f: F) -> [u8; 1024] { - let mut out = [0u8; 1024]; - for y in 0u32..16 { - for x in 0u32..16 { - let c = f(x, y); - let i = ((y * 16 + x) * 4) as usize; - out[i..i + 4].copy_from_slice(&c); +/// 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); } } - out + // 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 } // --------------------------------------------------------------------------- -// Card backs (16×16 placeholder patterns) +// Backgrounds (120×168 textured patterns) // --------------------------------------------------------------------------- -fn make_back_0() -> [u8; 1024] { - make_small(|_, y| if y % 4 < 2 { [0xFF, 0xFF, 0xFF, 40] } else { [0x26, 0x4D, 0x8C, 0xFF] }) -} -fn make_back_1() -> [u8; 1024] { - make_small(|x, y| if (x + y) % 4 < 2 { [0xFF, 0xFF, 0xFF, 40] } else { [0x8C, 0x1A, 0x1A, 0xFF] }) -} -fn make_back_2() -> [u8; 1024] { - make_small(|x, y| if x.is_multiple_of(4) && y.is_multiple_of(4) { [0xFF, 0xFF, 0xFF, 0xFF] } else { [0x0D, 0x66, 0x1A, 0xFF] }) -} -fn make_back_3() -> [u8; 1024] { - make_small(|x, y| { - let dx = (x as i32 - 8).unsigned_abs(); - let dy = (y as i32 - 8).unsigned_abs(); - if dx + dy <= 4 { [0xFF, 0xFF, 0xFF, 0xFF] } else { [0x59, 0x14, 0x85, 0xFF] } - }) -} -fn make_back_4() -> [u8; 1024] { - make_small(|x, y| if x == 0 || x == 15 || y == 0 || y == 15 { [0xFF, 0xFF, 0xFF, 0xFF] } else { [0x0D, 0x66, 0x6B, 0xFF] }) +/// 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 } -// --------------------------------------------------------------------------- -// Backgrounds (16×16 placeholder patterns) -// --------------------------------------------------------------------------- +/// 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 +} -fn make_bg_0() -> [u8; 1024] { - make_small(|x, y| if x.is_multiple_of(8) || y.is_multiple_of(8) { [0xFF, 0xFF, 0xFF, 30] } else { [0x1A, 0x4D, 0x1A, 0xFF] }) +/// 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 } -fn make_bg_1() -> [u8; 1024] { - make_small(|_, y| if y.is_multiple_of(2) { [0xFF, 0xFF, 0xFF, 20] } else { [0x40, 0x2D, 0x1A, 0xFF] }) + +/// 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 } -fn make_bg_2() -> [u8; 1024] { - make_small(|x, y| { - let off: u32 = if (y / 4).is_multiple_of(2) { 0 } else { 4 }; - if (x + off).is_multiple_of(8) && y.is_multiple_of(8) { [0xFF, 0xFF, 0xFF, 0xFF] } else { [0x0D, 0x14, 0x38, 0xFF] } - }) -} -fn make_bg_3() -> [u8; 1024] { - make_small(|x, y| if (x + y).is_multiple_of(8) { [0xFF, 0xFF, 0xFF, 30] } else { [0x4D, 0x0D, 0x14, 0xFF] }) -} -fn make_bg_4() -> [u8; 1024] { - make_small(|x, y| if (x + y).is_multiple_of(2) && x.is_multiple_of(3) { [0xFF, 0xFF, 0xFF, 20] } else { [0x1F, 0x1F, 0x24, 0xFF] }) + +/// 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 } // --------------------------------------------------------------------------- @@ -451,16 +696,16 @@ fn main() { } // Card backs - for (i, pixels) in [make_back_0(), make_back_1(), make_back_2(), make_back_3(), make_back_4()].iter().enumerate() { + 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_small_png(&path, pixels); + save_card_png(&path, cv); println!("wrote {}", path.display()); } // Backgrounds - for (i, pixels) in [make_bg_0(), make_bg_1(), make_bg_2(), make_bg_3(), make_bg_4()].iter().enumerate() { + 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_small_png(&path, pixels); + save_card_png(&path, cv); println!("wrote {}", path.display()); }