2b04718f33
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>
714 lines
25 KiB
Rust
714 lines
25 KiB
Rust
//! 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.");
|
||
}
|