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>
This commit is contained in:
funman300
2026-04-29 01:27:31 +00:00
parent 505f0ebda3
commit 2b04718f33
11 changed files with 298 additions and 53 deletions
+298 -53
View File
@@ -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<F: Fn(u32, u32) -> [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());
}