#![forbid(unsafe_code)]
mod badge;
mod crest;
mod draws;
mod grid;
mod monster;
mod nerd;
mod realm;
mod retro;
use draws::Draws;
pub type Rgb = [u8; 3];
const FACE_CONTEXT: &str = "gsichtl 2026-10-02 face v1";
const ICON_CONTEXT: &str = "gsichtl 2026-10-03 icon v1";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Kind {
Monster,
Nerd,
Crest,
Badge,
Realm,
Retro,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Avatar {
kind: Kind,
side: u8,
background: Rgb,
cells: Vec<Option<Rgb>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Rgba {
pub width: u32,
pub height: u32,
pub pixels: Vec<u8>,
}
pub fn face(seed: &[u8]) -> Avatar {
let mut draws = Draws::new(FACE_CONTEXT, seed);
if draws.pick(2) == 0 {
monster::build(monster::draw(&mut draws))
} else {
nerd::build(nerd::draw(&mut draws))
}
}
pub fn monster(seed: &[u8]) -> Avatar {
monster::build(monster::draw(&mut Draws::new(monster::CONTEXT, seed)))
}
pub fn nerd(seed: &[u8]) -> Avatar {
nerd::build(nerd::draw(&mut Draws::new(nerd::CONTEXT, seed)))
}
pub fn crest(seed: &[u8]) -> Avatar {
crest::build(crest::draw(&mut Draws::new(crest::CONTEXT, seed)))
}
pub fn badge(seed: &[u8]) -> Avatar {
badge::build(badge::draw(&mut Draws::new(badge::CONTEXT, seed)))
}
pub fn realm(seed: &[u8]) -> Avatar {
realm::build(realm::draw(&mut Draws::new(realm::CONTEXT, seed)))
}
pub fn retro(seed: &[u8]) -> Avatar {
retro::build(retro::draw(&mut Draws::new(retro::CONTEXT, seed)))
}
pub fn icon(seed: &[u8]) -> Avatar {
let mut draws = Draws::new(ICON_CONTEXT, seed);
if draws.pick(2) == 0 {
realm::build(realm::draw(&mut draws))
} else {
retro::build(retro::draw(&mut draws))
}
}
impl Avatar {
pub fn kind(&self) -> Kind {
self.kind
}
pub fn side(&self) -> u8 {
self.side
}
pub fn background(&self) -> Rgb {
self.background
}
pub fn cell(&self, x: u8, y: u8) -> Option<Rgb> {
if x >= self.side || y >= self.side {
return None;
}
let index = usize::from(y) * usize::from(self.side) + usize::from(x);
self.cells.get(index).copied().flatten()
}
pub fn to_rgba(&self, cell_px: u8, margin_px: u8) -> Rgba {
self.render(cell_px, margin_px, Some(self.background))
}
pub fn to_rgba_transparent(&self, cell_px: u8, margin_px: u8) -> Rgba {
self.render(cell_px, margin_px, None)
}
#[cfg(feature = "png")]
pub fn to_png(&self, cell_px: u8, margin_px: u8) -> Result<Vec<u8>, png::EncodingError> {
encode_png(&self.to_rgba(cell_px, margin_px))
}
#[cfg(feature = "png")]
pub fn to_png_transparent(
&self,
cell_px: u8,
margin_px: u8,
) -> Result<Vec<u8>, png::EncodingError> {
encode_png(&self.to_rgba_transparent(cell_px, margin_px))
}
fn render(&self, cell_px: u8, margin_px: u8, background: Option<Rgb>) -> Rgba {
let empty = match background {
Some([r, g, b]) => [r, g, b, u8::MAX],
None => [0; 4],
};
let cell = u32::from(cell_px);
let margin = u32::from(margin_px);
let grid = u32::from(self.side) * cell;
let side = grid + 2 * margin;
let mut pixels = Vec::with_capacity(side as usize * side as usize * 4);
for py in 0..side {
for px in 0..side {
let color = if (margin..margin + grid).contains(&px)
&& (margin..margin + grid).contains(&py)
{
let x = u8::try_from((px - margin) / cell).unwrap_or(u8::MAX);
let y = u8::try_from((py - margin) / cell).unwrap_or(u8::MAX);
self.cell(x, y)
} else {
None
};
match color {
Some([r, g, b]) => pixels.extend_from_slice(&[r, g, b, u8::MAX]),
None => pixels.extend_from_slice(&empty),
}
}
}
Rgba {
width: side,
height: side,
pixels,
}
}
}
#[cfg(feature = "png")]
fn encode_png(image: &Rgba) -> Result<Vec<u8>, png::EncodingError> {
let mut out = Vec::new();
let mut encoder = png::Encoder::new(&mut out, image.width, image.height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header()?;
writer.write_image_data(&image.pixels)?;
writer.finish()?;
Ok(out)
}