use glam::Vec3;
#[derive(Clone, Debug)]
pub struct StyleSpec {
pub target_size: u32,
pub canvas_fill: f32,
pub outline_color: [u8; 3],
pub light_dir: Vec3,
pub highlight_threshold: f32,
pub shadow_threshold: f32,
pub eye_relative_y: f32,
pub eye_relative_x: f32,
pub eye_size: u32,
pub bg_threshold: f32,
}
impl Default for StyleSpec {
fn default() -> Self {
Self {
target_size: 32,
canvas_fill: 0.50,
outline_color: [0, 0, 0],
light_dir: Vec3::new(-0.577, -0.577, 0.577),
highlight_threshold: 0.3,
shadow_threshold: -0.05,
eye_relative_y: 0.25,
eye_relative_x: 0.7,
eye_size: 1,
bg_threshold: 0.35,
}
}
}
#[derive(Clone, Debug)]
pub struct CreaturePalette {
pub body: [u8; 3],
pub shadow: [u8; 3],
pub highlight: [u8; 3],
pub eye: [u8; 3],
}
pub fn extract_silhouette(rgba: &[u8], width: usize, height: usize, bg_threshold: f32) -> Vec<bool> {
let n = width * height;
let mut mask = vec![false; n];
let corners = [0, width - 1, (height - 1) * width, (height - 1) * width + width - 1];
let mut bg_r = 0u32; let mut bg_g = 0u32; let mut bg_b = 0u32; let mut bg_count = 0u32;
for &corner in &corners {
let cx = corner % width;
let cy = corner / width;
for dy in 0..5usize {
for dx in 0..5usize {
let x = if cx < 3 { dx } else { cx - 2 + dx };
let y = if cy < 3 { dy } else { cy - 2 + dy };
if x < width && y < height {
let i = (y * width + x) * 4;
bg_r += rgba[i] as u32; bg_g += rgba[i + 1] as u32; bg_b += rgba[i + 2] as u32; bg_count += 1;
}
}
}
}
let bg_r = (bg_r / bg_count) as f32;
let bg_g = (bg_g / bg_count) as f32;
let bg_b = (bg_b / bg_count) as f32;
let threshold_sq = (bg_threshold * 255.0) * (bg_threshold * 255.0);
for i in 0..n {
let r = rgba[i * 4] as f32; let g = rgba[i * 4 + 1] as f32; let b = rgba[i * 4 + 2] as f32;
let dr = r - bg_r; let dg = g - bg_g; let db = b - bg_b;
mask[i] = dr * dr + dg * dg + db * db > threshold_sq;
}
let mut visited = vec![false; n];
let mut best_region: Vec<usize> = Vec::new();
for start in 0..n {
if !mask[start] || visited[start] { continue; }
let mut region = Vec::new();
let mut queue = vec![start];
visited[start] = true;
while let Some(pos) = queue.pop() {
region.push(pos);
let x = pos % width; let y = pos / width;
for (dx, dy) in &[(0isize, -1isize), (0, 1), (-1, 0), (1, 0)] {
let nx = x as isize + dx; let ny = y as isize + dy;
if nx >= 0 && nx < width as isize && ny >= 0 && ny < height as isize {
let ni = ny as usize * width + nx as usize;
if !visited[ni] && mask[ni] { visited[ni] = true; queue.push(ni); }
}
}
}
if region.len() > best_region.len() { best_region = region; }
}
let mut clean_mask = vec![false; n];
for idx in best_region { clean_mask[idx] = true; }
clean_mask
}
pub fn extract_palette(rgba: &[u8], mask: &[bool], _width: usize, _height: usize) -> CreaturePalette {
let mut lum_pixels: Vec<(f32, f32, usize)> = Vec::new();
for (i, &fg) in mask.iter().enumerate() {
if !fg { continue; }
let r = rgba[i * 4] as f32; let g = rgba[i * 4 + 1] as f32; let b = rgba[i * 4 + 2] as f32;
let lum = 0.299 * r + 0.587 * g + 0.114 * b;
let max_c = r.max(g).max(b); let min_c = r.min(g).min(b);
let sat = if max_c > 0.0 { (max_c - min_c) / max_c } else { 0.0 };
lum_pixels.push((lum, sat, i));
}
if lum_pixels.is_empty() {
return CreaturePalette { body: [128, 128, 128], shadow: [80, 80, 80], highlight: [180, 180, 180], eye: [255, 255, 255] };
}
let count = lum_pixels.len();
lum_pixels.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let dark_end = (count * 15) / 100;
let shadow = avg_color(rgba, &lum_pixels[..dark_end.max(1)]);
let mid_start = count / 4;
let mid_end = (count * 3) / 4;
let body = avg_color(rgba, &lum_pixels[mid_start..mid_end]);
let mut sat_pixels: Vec<(f32, f32, usize)> = lum_pixels.clone();
sat_pixels.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let top_sat = &sat_pixels[..((count * 10) / 100).max(1)];
let avg_sat: f32 = top_sat.iter().map(|p| p.1).sum::<f32>() / top_sat.len() as f32;
let highlight = if avg_sat > 0.25 {
avg_color(rgba, top_sat)
} else {
let light_start = (count * 85) / 100;
avg_color(rgba, &lum_pixels[light_start..])
};
CreaturePalette { body, shadow, highlight, eye: [255, 255, 255] }
}
fn avg_color(rgba: &[u8], pixels: &[(f32, f32, usize)]) -> [u8; 3] {
let (mut r, mut g, mut b, mut c) = (0u64, 0u64, 0u64, 0u64);
for &(_, _, i) in pixels {
r += rgba[i * 4] as u64; g += rgba[i * 4 + 1] as u64; b += rgba[i * 4 + 2] as u64; c += 1;
}
[(r / c.max(1)) as u8, (g / c.max(1)) as u8, (b / c.max(1)) as u8]
}
pub fn generate_sprite_direct(
photo_rgba: &[u8], photo_w: usize, photo_h: usize,
normals: Option<&[Vec3]>, style: &StyleSpec, max_colors: usize,
) -> Vec<u8> {
let ts = style.target_size as usize;
let mut rgba = vec![0u8; ts * ts * 4];
let silhouette = extract_silhouette(photo_rgba, photo_w, photo_h, style.bg_threshold);
let fg_count = silhouette.iter().filter(|&&m| m).count();
if fg_count == 0 { return rgba; }
let mut min_x = photo_w; let mut min_y = photo_h; let mut max_x = 0usize; let mut max_y = 0usize;
for y in 0..photo_h { for x in 0..photo_w {
if silhouette[y * photo_w + x] { min_x = min_x.min(x); min_y = min_y.min(y); max_x = max_x.max(x); max_y = max_y.max(y); }
}}
let bbox_w = max_x - min_x + 1;
let bbox_h = max_y - min_y + 1;
let inner = ts - 2;
let scale_x = inner as f32 / bbox_w as f32;
let scale_y = inner as f32 / bbox_h as f32;
let scale = (scale_x.min(scale_y)) * style.canvas_fill;
let sprite_w = (bbox_w as f32 * scale) as usize;
let sprite_h = (bbox_h as f32 * scale) as usize;
let ox = (ts - sprite_w) / 2;
let oy = (ts - sprite_h) / 2;
let mut mask = vec![false; ts * ts];
for ty in 0..sprite_h {
for tx in 0..sprite_w {
let src_x0 = min_x + (tx as f32 / scale) as usize;
let src_y0 = min_y + (ty as f32 / scale) as usize;
let src_x1 = min_x + ((tx + 1) as f32 / scale) as usize;
let src_y1 = min_y + ((ty + 1) as f32 / scale) as usize;
let mut r_sum = 0u32; let mut g_sum = 0u32; let mut b_sum = 0u32;
let mut fg = 0u32; let mut total = 0u32;
for sy in src_y0..=src_y1.min(photo_h - 1) {
for sx in src_x0..=src_x1.min(photo_w - 1) {
total += 1;
if silhouette[sy * photo_w + sx] {
fg += 1;
let si = (sy * photo_w + sx) * 4;
r_sum += photo_rgba[si] as u32; g_sum += photo_rgba[si + 1] as u32; b_sum += photo_rgba[si + 2] as u32;
}
}
}
let px = ox + tx; let py = oy + ty;
if px < ts && py < ts && total > 0 && fg as f32 / total as f32 > 0.3 && fg > 0 {
let pi = (py * ts + px) * 4;
rgba[pi] = (r_sum / fg) as u8; rgba[pi + 1] = (g_sum / fg) as u8; rgba[pi + 2] = (b_sum / fg) as u8; rgba[pi + 3] = 255;
mask[py * ts + px] = true;
}
}
}
if let Some(normals) = normals {
let light = style.light_dir.normalize();
let src_n = normals.len();
for ty in 0..sprite_h {
for tx in 0..sprite_w {
let px = ox + tx; let py = oy + ty;
if px >= ts || py >= ts { continue; }
let ti = py * ts + px;
if !mask[ti] { continue; }
let sx = ((tx as f32 / sprite_w as f32) * photo_w as f32) as usize;
let sy = ((ty as f32 / sprite_h as f32) * photo_h as f32) as usize;
let ni = (sy * photo_w + sx).min(src_n - 1);
let dot = normals[ni].dot(light);
let pi = ti * 4;
let factor = 1.0 + dot * 0.3;
rgba[pi] = (rgba[pi] as f32 * factor).clamp(0.0, 255.0) as u8;
rgba[pi + 1] = (rgba[pi + 1] as f32 * factor).clamp(0.0, 255.0) as u8;
rgba[pi + 2] = (rgba[pi + 2] as f32 * factor).clamp(0.0, 255.0) as u8;
}
}
}
if max_colors > 0 && max_colors < 256 {
let mut colors: Vec<[u8; 3]> = Vec::new();
for i in 0..ts * ts {
if mask[i] { let pi = i * 4; colors.push([rgba[pi], rgba[pi + 1], rgba[pi + 2]]); }
}
if colors.len() > max_colors {
colors.sort_by(|a, b| {
let la = a[0] as u32 * 3 + a[1] as u32 * 6 + a[2] as u32;
let lb = b[0] as u32 * 3 + b[1] as u32 * 6 + b[2] as u32;
la.cmp(&lb)
});
let bucket_size = colors.len() / max_colors;
let palette: Vec<[u8; 3]> = (0..max_colors).map(|i| {
let start = i * bucket_size;
let end = if i == max_colors - 1 { colors.len() } else { (i + 1) * bucket_size };
let slice = &colors[start..end];
let (mut r, mut g, mut b) = (0u64, 0u64, 0u64);
for c in slice { r += c[0] as u64; g += c[1] as u64; b += c[2] as u64; }
let n = slice.len() as u64;
[(r / n.max(1)) as u8, (g / n.max(1)) as u8, (b / n.max(1)) as u8]
}).collect();
for i in 0..ts * ts {
if !mask[i] { continue; }
let pi = i * 4;
let r = rgba[pi]; let g = rgba[pi + 1]; let b = rgba[pi + 2];
let nearest = palette.iter().min_by_key(|c| {
let dr = r as i32 - c[0] as i32; let dg = g as i32 - c[1] as i32; let db = b as i32 - c[2] as i32;
(dr * dr + dg * dg + db * db) as u32
}).unwrap();
rgba[pi] = nearest[0]; rgba[pi + 1] = nearest[1]; rgba[pi + 2] = nearest[2];
}
}
}
let mut outline_mask = vec![false; ts * ts];
for y in 0..ts { for x in 0..ts {
if mask[y * ts + x] { continue; }
let neighbors = [
(x > 0).then(|| mask[y * ts + (x - 1)]),
(x + 1 < ts).then(|| mask[y * ts + (x + 1)]),
(y > 0).then(|| mask[(y - 1) * ts + x]),
(y + 1 < ts).then(|| mask[(y + 1) * ts + x]),
];
if neighbors.iter().any(|n| *n == Some(true)) { outline_mask[y * ts + x] = true; }
}}
for i in 0..ts * ts {
if outline_mask[i] {
let pi = i * 4;
rgba[pi] = style.outline_color[0]; rgba[pi + 1] = style.outline_color[1]; rgba[pi + 2] = style.outline_color[2]; rgba[pi + 3] = 255;
}
}
rgba
}
pub struct ElementVFX {
pub glow_inner: [u8; 3],
pub glow_outer: [u8; 3],
pub glow_far: [u8; 3],
pub emissive_boost: f32,
pub saturation_threshold: f32,
}
impl ElementVFX {
pub fn fire() -> Self {
Self { glow_inner: [255, 140, 20], glow_outer: [200, 80, 10], glow_far: [120, 40, 5], emissive_boost: 1.8, saturation_threshold: 0.25 }
}
pub fn water() -> Self {
Self { glow_inner: [40, 200, 255], glow_outer: [20, 120, 200], glow_far: [10, 60, 120], emissive_boost: 1.5, saturation_threshold: 0.2 }
}
pub fn earth() -> Self {
Self { glow_inner: [220, 180, 50], glow_outer: [160, 120, 30], glow_far: [100, 70, 15], emissive_boost: 1.4, saturation_threshold: 0.2 }
}
pub fn air() -> Self {
Self { glow_inner: [200, 220, 255], glow_outer: [140, 160, 220], glow_far: [80, 100, 160], emissive_boost: 1.3, saturation_threshold: 0.15 }
}
}
pub fn apply_element_vfx(rgba: &mut [u8], size: usize, vfx: &ElementVFX) {
let mut mask = vec![false; size * size];
for i in 0..size * size { mask[i] = rgba[i * 4 + 3] > 0; }
for i in 0..size * size {
if !mask[i] { continue; }
let pi = i * 4;
let r = rgba[pi] as f32; let g = rgba[pi + 1] as f32; let b = rgba[pi + 2] as f32;
let max_c = r.max(g).max(b); let min_c = r.min(g).min(b);
let sat = if max_c > 0.0 { (max_c - min_c) / max_c } else { 0.0 };
if sat > vfx.saturation_threshold && r > g * 0.8 {
let boost = 1.0 + (sat - vfx.saturation_threshold) * vfx.emissive_boost;
rgba[pi] = (r * boost).min(255.0) as u8;
rgba[pi + 1] = (g * boost * 0.7).min(255.0) as u8;
rgba[pi + 2] = (b * boost * 0.4).min(255.0) as u8;
}
}
let mut outline = vec![false; size * size];
for y in 0..size { for x in 0..size {
if !mask[y * size + x] { continue; }
let has_empty_neighbor =
(x == 0 || !mask[y * size + (x - 1)]) ||
(x + 1 >= size || !mask[y * size + (x + 1)]) ||
(y == 0 || !mask[(y - 1) * size + x]) ||
(y + 1 >= size || !mask[(y + 1) * size + x]);
if has_empty_neighbor { outline[y * size + x] = true; }
}}
for i in 0..size * size {
if outline[i] {
let pi = i * 4;
let r = rgba[pi] as f32; let g = rgba[pi + 1] as f32; let b = rgba[pi + 2] as f32;
rgba[pi] = (r * 0.3 + vfx.glow_inner[0] as f32 * 0.7).min(255.0) as u8;
rgba[pi + 1] = (g * 0.3 + vfx.glow_inner[1] as f32 * 0.7).min(255.0) as u8;
rgba[pi + 2] = (b * 0.3 + vfx.glow_inner[2] as f32 * 0.7).min(255.0) as u8;
}
}
let mut aura1 = vec![false; size * size];
let mut aura2 = vec![false; size * size];
for y in 0..size { for x in 0..size {
if mask[y * size + x] { continue; }
let adj_filled = [
(x > 0).then(|| mask[y * size + (x - 1)]).unwrap_or(false),
(x + 1 < size).then(|| mask[y * size + (x + 1)]).unwrap_or(false),
(y > 0).then(|| mask[(y - 1) * size + x]).unwrap_or(false),
(y + 1 < size).then(|| mask[(y + 1) * size + x]).unwrap_or(false),
(x > 0 && y > 0).then(|| mask[(y - 1) * size + (x - 1)]).unwrap_or(false),
(x + 1 < size && y > 0).then(|| mask[(y - 1) * size + (x + 1)]).unwrap_or(false),
(x > 0 && y + 1 < size).then(|| mask[(y + 1) * size + (x - 1)]).unwrap_or(false),
(x + 1 < size && y + 1 < size).then(|| mask[(y + 1) * size + (x + 1)]).unwrap_or(false),
];
if adj_filled.iter().any(|&a| a) { aura1[y * size + x] = true; }
}}
for y in 0..size { for x in 0..size {
if mask[y * size + x] || aura1[y * size + x] { continue; }
let adj_aura = [
(x > 0).then(|| aura1[y * size + (x - 1)]).unwrap_or(false),
(x + 1 < size).then(|| aura1[y * size + (x + 1)]).unwrap_or(false),
(y > 0).then(|| aura1[(y - 1) * size + x]).unwrap_or(false),
(y + 1 < size).then(|| aura1[(y + 1) * size + x]).unwrap_or(false),
];
if adj_aura.iter().any(|&a| a) { aura2[y * size + x] = true; }
}}
for i in 0..size * size {
if aura1[i] {
let pi = i * 4;
rgba[pi] = vfx.glow_outer[0]; rgba[pi + 1] = vfx.glow_outer[1]; rgba[pi + 2] = vfx.glow_outer[2]; rgba[pi + 3] = 200;
} else if aura2[i] {
let pi = i * 4;
rgba[pi] = vfx.glow_far[0]; rgba[pi + 1] = vfx.glow_far[1]; rgba[pi + 2] = vfx.glow_far[2]; rgba[pi + 3] = 100;
}
}
}
pub fn downscale_mask(mask: &[bool], src_w: usize, src_h: usize, target: usize, canvas_fill: f32) -> (Vec<bool>, usize, usize, usize, usize) {
let mut min_x = src_w; let mut max_x = 0usize; let mut min_y = src_h; let mut max_y = 0usize;
for y in 0..src_h { for x in 0..src_w {
if mask[y * src_w + x] { min_x = min_x.min(x); max_x = max_x.max(x); min_y = min_y.min(y); max_y = max_y.max(y); }
}}
let bbox_w = (max_x - min_x + 1) as f32;
let bbox_h = (max_y - min_y + 1) as f32;
if bbox_w < 1.0 || bbox_h < 1.0 { return (vec![false; target * target], 0, 0, target, target); }
let usable = (target - 2) as f32;
let base_scale = (usable / bbox_w).min(usable / bbox_h);
let scale = base_scale * canvas_fill;
let sprite_w = (bbox_w * scale).ceil() as usize;
let sprite_h = (bbox_h * scale).ceil() as usize;
let ox = (target - sprite_w) / 2;
let oy = target - sprite_h - 1;
let mut result = vec![false; target * target];
for ty in 0..sprite_h {
for tx in 0..sprite_w {
let sx_start = min_x + (tx as f32 / scale) as usize;
let sy_start = min_y + (ty as f32 / scale) as usize;
let sx_end = min_x + ((tx + 1) as f32 / scale) as usize;
let sy_end = min_y + ((ty + 1) as f32 / scale) as usize;
let mut on = 0; let mut total = 0;
for sy in sy_start..=sy_end.min(src_h - 1) {
for sx in sx_start..=sx_end.min(src_w - 1) { total += 1; if mask[sy * src_w + sx] { on += 1; } }
}
let px = ox + tx; let py = oy + ty;
if px < target && py < target && total > 0 && on as f32 / total as f32 > 0.4 { result[py * target + px] = true; }
}
}
(result, ox, oy, sprite_w, sprite_h)
}
pub fn generate_sprite(
photo_rgba: &[u8], photo_w: usize, photo_h: usize, normals: Option<&[Vec3]>, style: &StyleSpec,
) -> Vec<u8> {
let ts = style.target_size as usize;
let mut rgba = vec![0u8; ts * ts * 4];
let silhouette = extract_silhouette(photo_rgba, photo_w, photo_h, style.bg_threshold);
let fg_count = silhouette.iter().filter(|&&m| m).count();
if fg_count == 0 { return rgba; }
let palette = extract_palette(photo_rgba, &silhouette, photo_w, photo_h);
let (mask, ox, oy, sw, sh) = downscale_mask(&silhouette, photo_w, photo_h, ts, style.canvas_fill);
for i in 0..ts * ts {
if mask[i] {
let pi = i * 4;
rgba[pi] = palette.body[0]; rgba[pi + 1] = palette.body[1]; rgba[pi + 2] = palette.body[2]; rgba[pi + 3] = 255;
}
}
if let Some(normals) = normals {
let light = style.light_dir.normalize();
let src_n = normals.len();
for ty in 0..sh { for tx in 0..sw {
let px = ox + tx; let py = oy + ty;
if px >= ts || py >= ts { continue; }
let ti = py * ts + px;
if !mask[ti] { continue; }
let sx = ((tx as f32 / sw as f32) * photo_w as f32) as usize;
let sy = ((ty as f32 / sh as f32) * photo_h as f32) as usize;
let ni = (sy * photo_w + sx).min(src_n - 1);
let normal = normals[ni];
let dot = normal.dot(light);
let pi = ti * 4;
if dot > style.highlight_threshold {
rgba[pi] = palette.highlight[0]; rgba[pi + 1] = palette.highlight[1]; rgba[pi + 2] = palette.highlight[2];
} else if dot < style.shadow_threshold {
rgba[pi] = palette.shadow[0]; rgba[pi + 1] = palette.shadow[1]; rgba[pi + 2] = palette.shadow[2];
}
}}
}
if sh > 4 && sw > 4 {
let eye_y = oy + (sh as f32 * style.eye_relative_y) as usize;
let eye_x = ox + (sw as f32 * style.eye_relative_x) as usize;
let es = style.eye_size as usize;
for dy in 0..es { for dx in 0..es {
let px = eye_x + dx; let py = eye_y + dy;
if px < ts && py < ts && mask[py * ts + px] {
let pi = (py * ts + px) * 4;
rgba[pi] = palette.eye[0]; rgba[pi + 1] = palette.eye[1]; rgba[pi + 2] = palette.eye[2];
}
}}
}
let body_copy: Vec<bool> = (0..ts * ts).map(|i| rgba[i * 4 + 3] > 0).collect();
for y in 1..ts - 1 { for x in 1..ts - 1 {
let i = y * ts + x;
if body_copy[i] { continue; }
let neighbors = [(y - 1) * ts + x, (y + 1) * ts + x, y * ts + x - 1, y * ts + x + 1];
if neighbors.iter().any(|&ni| body_copy[ni]) {
let pi = i * 4;
rgba[pi] = style.outline_color[0]; rgba[pi + 1] = style.outline_color[1]; rgba[pi + 2] = style.outline_color[2]; rgba[pi + 3] = 255;
}
}}
rgba
}