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>
|
Before Width: | Height: | Size: 451 B After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 218 B After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 247 B After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 559 B After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 675 B After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 221 B After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 943 B After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 344 B After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 337 B After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 299 B After Width: | Height: | Size: 4.0 KiB |
@@ -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());
|
||||
}
|
||||
|
||||
|
||||