//! Procedural, asset-free avatar generation driven by stable identity hashes.
//!
//! The crate produces deterministic avatar images from an input identifier
//! without shipping image packs, sprites, or third-party artwork. All visual
//! output is drawn from code using geometric primitives.
//!
//! Typical usage:
//! ```no_run
//! use hashavatar::{
//! AvatarBackground, AvatarKind, AvatarOptions, AvatarOutputFormat, AvatarSpec,
//! encode_avatar_for_id,
//! };
//!
//! let spec = AvatarSpec::new(256, 256, 0)?;
//! let bytes = encode_avatar_for_id(
//! spec,
//! "robot@hashavatar.app",
//! AvatarOutputFormat::WebP,
//! AvatarOptions {
//! kind: AvatarKind::Robot,
//! background: AvatarBackground::Transparent,
//! },
//! )?;
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
#![forbid(unsafe_code)]
#[cfg(all(feature = "blake3", feature = "xxh3"))]
compile_error!(
"hashavatar features `blake3` and `xxh3` are mutually exclusive; choose one identity hash mode"
);
#[cfg(all(feature = "fuzzing", not(any(debug_assertions, fuzzing))))]
compile_error!(
"hashavatar's fuzzing feature exposes internal fuzz harness entry points and must not be enabled in non-fuzzing release builds"
);
use std::io::Cursor;
use std::mem::swap;
use std::str::FromStr;
#[cfg(feature = "gif")]
use image::codecs::gif::GifEncoder;
#[cfg(feature = "jpeg")]
use image::codecs::jpeg::JpegEncoder;
#[cfg(feature = "png")]
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::codecs::webp::WebPEncoder;
use image::error::{LimitError, LimitErrorKind};
use image::{
ExtendedColorType, ImageBuffer, ImageEncoder, ImageError, ImageResult, Rgba, RgbaImage,
};
use palette::{FromColor, Hsl, Srgb};
use rand::{RngExt, SeedableRng, rngs::StdRng};
#[cfg(not(any(feature = "blake3", feature = "xxh3")))]
use sha2::{Digest, Sha512};
use subtle::ConstantTimeEq;
use zeroize::{Zeroize, Zeroizing};
/// Rendering contract version for deterministic avatars.
///
/// Within a major crate release, the goal is to keep visuals stable for the
/// same `(namespace, id, kind, background, size)` tuple unless a documented bug
/// fix requires a targeted change.
pub const AVATAR_STYLE_VERSION: u32 = 2;
/// Smallest supported raster or SVG dimension.
pub const MIN_AVATAR_DIMENSION: u32 = 64;
/// Largest supported raster or SVG dimension.
///
/// This caps worst-case allocation and encoding work for callers that expose
/// avatar generation to untrusted input. A 2048 x 2048 RGBA buffer is 16 MiB
/// before encoder overhead.
pub const MAX_AVATAR_DIMENSION: u32 = 2048;
/// Number of bytes in one RGBA8 raster pixel.
pub const AVATAR_RGBA_BYTES_PER_PIXEL: usize = 4;
/// Largest supported raster pixel count.
pub const MAX_AVATAR_PIXELS: usize =
(MAX_AVATAR_DIMENSION as usize) * (MAX_AVATAR_DIMENSION as usize);
/// Largest supported raw RGBA8 image buffer size before encoder overhead.
///
/// Services should combine this bound with their own request concurrency
/// limits. The crate bounds each render request, but it cannot prevent process
/// memory pressure from many concurrent maximum-size renders.
pub const MAX_AVATAR_RGBA_BYTES: usize = MAX_AVATAR_PIXELS * AVATAR_RGBA_BYTES_PER_PIXEL;
/// Largest supported identity input in bytes.
///
/// This prevents applications from accidentally hashing attacker-controlled
/// request bodies or other unbounded byte strings as avatar identities.
pub const MAX_AVATAR_ID_BYTES: usize = 1024;
/// Largest supported namespace component in bytes.
pub const MAX_AVATAR_NAMESPACE_COMPONENT_BYTES: usize = 128;
/// Identity digest byte used for automatic avatar family selection.
pub const AVATAR_STYLE_KIND_BYTE: usize = 0;
/// Identity digest byte used for automatic background selection.
pub const AVATAR_STYLE_BACKGROUND_BYTE: usize = 1;
/// Identity digest byte used for automatic accessory selection.
pub const AVATAR_STYLE_ACCESSORY_BYTE: usize = 2;
/// Identity digest byte used for automatic color-palette selection.
pub const AVATAR_STYLE_COLOR_BYTE: usize = 3;
/// Identity digest byte used for automatic expression selection.
pub const AVATAR_STYLE_EXPRESSION_BYTE: usize = 4;
/// Identity digest byte used for automatic frame-shape selection.
pub const AVATAR_STYLE_SHAPE_BYTE: usize = 5;
const HASH_DOMAIN: &[u8] = b"hashavatar";
const HASH_DOMAIN_ALGORITHM_COMPONENT: &[u8] = b"identity-hash";
#[cfg(feature = "blake3")]
const ACTIVE_HASH_ALGORITHM_LABEL: &[u8] = b"blake3";
#[cfg(all(not(feature = "blake3"), feature = "xxh3"))]
const ACTIVE_HASH_ALGORITHM_LABEL: &[u8] = b"xxh3-128";
#[cfg(all(not(feature = "blake3"), not(feature = "xxh3")))]
const ACTIVE_HASH_ALGORITHM_LABEL: &[u8] = b"sha512";
#[cfg(feature = "xxh3")]
const HASH_XOF_CHUNK_COMPONENT: &[u8] = b"digest-chunk";
/// RGBA color helper for concise shape drawing.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Color(pub [u8; 4]);
impl Color {
pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
Self([r, g, b, a])
}
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self::rgba(r, g, b, 255)
}
}
impl From<Color> for Rgba<u8> {
fn from(value: Color) -> Self {
Rgba(value.0)
}
}
// Raster primitive implementations adapted from imageproc's MIT-licensed
// drawing modules. See THIRD_PARTY_NOTICES.md.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
const fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Rect {
left: i32,
top: i32,
width: u32,
height: u32,
}
impl Rect {
const fn at(left: i32, top: i32) -> RectPosition {
RectPosition { left, top }
}
const fn left(self) -> i32 {
self.left
}
const fn top(self) -> i32 {
self.top
}
fn right(self) -> i32 {
let width_offset = self.width.saturating_sub(1).min(i32::MAX as u32) as i32;
self.left.saturating_add(width_offset)
}
fn bottom(self) -> i32 {
let height_offset = self.height.saturating_sub(1).min(i32::MAX as u32) as i32;
self.top.saturating_add(height_offset)
}
const fn width(self) -> u32 {
self.width
}
const fn height(self) -> u32 {
self.height
}
fn intersect(self, other: Self) -> Option<Self> {
let left = self.left.max(other.left);
let top = self.top.max(other.top);
let right = self.right().min(other.right());
let bottom = self.bottom().min(other.bottom());
if right < left || bottom < top {
return None;
}
Some(Self::at(left, top).of_size(
right.saturating_sub(left).saturating_add(1) as u32,
bottom.saturating_sub(top).saturating_add(1) as u32,
))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct RectPosition {
left: i32,
top: i32,
}
impl RectPosition {
const fn of_size(self, width: u32, height: u32) -> Rect {
Rect {
left: self.left,
top: self.top,
width: if width == 0 { 1 } else { width },
height: if height == 0 { 1 } else { height },
}
}
}
fn draw_filled_rect_mut(image: &mut RgbaImage, rect: Rect, color: Rgba<u8>) {
if image.width() == 0 || image.height() == 0 {
return;
}
let bounds = Rect::at(0, 0).of_size(image.width(), image.height());
if let Some(intersection) = bounds.intersect(rect) {
for dy in 0..intersection.height() {
for dx in 0..intersection.width() {
let x = intersection.left() as u32 + dx;
let y = intersection.top() as u32 + dy;
image.put_pixel(x, y, color);
}
}
}
}
fn draw_line_segment_mut(
image: &mut RgbaImage,
start: (f32, f32),
end: (f32, f32),
color: Rgba<u8>,
) {
for (x, y) in BresenhamLineIter::new(start, end) {
draw_if_in_bounds(image, x, y, color);
}
}
struct BresenhamLineIter {
dx: f32,
dy: f32,
x: i32,
y: i32,
error: f32,
end_x: i32,
is_steep: bool,
y_step: i32,
}
impl BresenhamLineIter {
fn new(start: (f32, f32), end: (f32, f32)) -> Self {
let (mut x0, mut y0) = (start.0, start.1);
let (mut x1, mut y1) = (end.0, end.1);
let is_steep = (y1 - y0).abs() > (x1 - x0).abs();
if is_steep {
swap(&mut x0, &mut y0);
swap(&mut x1, &mut y1);
}
if x0 > x1 {
swap(&mut x0, &mut x1);
swap(&mut y0, &mut y1);
}
let dx = x1 - x0;
Self {
dx,
dy: (y1 - y0).abs(),
x: x0 as i32,
y: y0 as i32,
error: dx / 2.0,
end_x: x1 as i32,
is_steep,
y_step: if y0 < y1 { 1 } else { -1 },
}
}
}
impl Iterator for BresenhamLineIter {
type Item = (i32, i32);
fn next(&mut self) -> Option<Self::Item> {
if self.x > self.end_x {
return None;
}
let point = if self.is_steep {
(self.y, self.x)
} else {
(self.x, self.y)
};
self.x += 1;
self.error -= self.dy;
if self.error < 0.0 {
self.y += self.y_step;
self.error += self.dx;
}
Some(point)
}
}
fn draw_antialiased_line_segment_mut<B>(
image: &mut RgbaImage,
start: (i32, i32),
end: (i32, i32),
color: Rgba<u8>,
blend: B,
) where
B: Fn(Rgba<u8>, Rgba<u8>, f32) -> Rgba<u8>,
{
let (mut x0, mut y0) = (start.0, start.1);
let (mut x1, mut y1) = (end.0, end.1);
let is_steep = (y1 - y0).abs() > (x1 - x0).abs();
if is_steep {
if y0 > y1 {
swap(&mut x0, &mut x1);
swap(&mut y0, &mut y1);
}
plot_wu_line(image, (y0, x0), (y1, x1), color, |x, y| (y, x), blend);
} else {
if x0 > x1 {
swap(&mut x0, &mut x1);
swap(&mut y0, &mut y1);
}
plot_wu_line(image, (x0, y0), (x1, y1), color, |x, y| (x, y), blend);
}
}
fn plot_wu_line<T, B>(
image: &mut RgbaImage,
start: (i32, i32),
end: (i32, i32),
color: Rgba<u8>,
transform: T,
blend: B,
) where
T: Fn(i32, i32) -> (i32, i32),
B: Fn(Rgba<u8>, Rgba<u8>, f32) -> Rgba<u8>,
{
let dx = end.0 - start.0;
let dy = end.1 - start.1;
if dx == 0 {
plot_antialiased_pixel(image, transform(start.0, start.1), color, 1.0, &blend);
return;
}
let gradient = dy as f32 / dx as f32;
let mut fy = start.1 as f32;
for x in start.0..=end.0 {
plot_antialiased_pixel(
image,
transform(x, fy as i32),
color,
1.0 - fy.fract(),
&blend,
);
plot_antialiased_pixel(
image,
transform(x, fy as i32 + 1),
color,
fy.fract(),
&blend,
);
fy += gradient;
}
}
fn plot_antialiased_pixel<B>(
image: &mut RgbaImage,
(x, y): (i32, i32),
color: Rgba<u8>,
weight: f32,
blend: &B,
) where
B: Fn(Rgba<u8>, Rgba<u8>, f32) -> Rgba<u8>,
{
if in_bounds(image, x, y) {
let original = *image.get_pixel(x as u32, y as u32);
image.put_pixel(x as u32, y as u32, blend(color, original, weight));
}
}
fn draw_filled_ellipse_mut(
image: &mut RgbaImage,
center: (i32, i32),
width_radius: i32,
height_radius: i32,
color: Rgba<u8>,
) {
if width_radius == height_radius {
draw_filled_circle_mut(image, center, width_radius, color);
return;
}
draw_ellipse(
|image, x0, y0, x, y| {
draw_line_segment_mut(
image,
((x0 - x) as f32, (y0 + y) as f32),
((x0 + x) as f32, (y0 + y) as f32),
color,
);
draw_line_segment_mut(
image,
((x0 - x) as f32, (y0 - y) as f32),
((x0 + x) as f32, (y0 - y) as f32),
color,
);
},
image,
center,
width_radius,
height_radius,
);
}
fn draw_ellipse<F>(
mut render: F,
image: &mut RgbaImage,
center: (i32, i32),
width_radius: i32,
height_radius: i32,
) where
F: FnMut(&mut RgbaImage, i32, i32, i32, i32),
{
let (x0, y0) = center;
let w2 = (width_radius as f64).powi(2);
let h2 = (height_radius as f64).powi(2);
let mut x = 0;
let mut y = height_radius;
let mut px = 0.0;
let mut py = 2.0 * w2 * y as f64;
render(image, x0, y0, x, y);
let mut p = h2 - (w2 * height_radius as f64) + (0.25 * w2);
while px < py {
x += 1;
px += 2.0 * h2;
if p < 0.0 {
p += h2 + px;
} else {
y -= 1;
py += -2.0 * w2;
p += h2 + px - py;
}
render(image, x0, y0, x, y);
}
p = h2 * (x as f64 + 0.5).powi(2) + w2 * ((y - 1) as f64).powi(2) - w2 * h2;
while y > 0 {
y -= 1;
py += -2.0 * w2;
if p > 0.0 {
p += w2 - py;
} else {
x += 1;
px += 2.0 * h2;
p += w2 - py + px;
}
render(image, x0, y0, x, y);
}
}
fn draw_hollow_circle_mut(image: &mut RgbaImage, center: (i32, i32), radius: i32, color: Rgba<u8>) {
let mut x = 0;
let mut y = radius;
let mut p = 1 - radius;
let (x0, y0) = center;
while x <= y {
draw_if_in_bounds(image, x0 + x, y0 + y, color);
draw_if_in_bounds(image, x0 + y, y0 + x, color);
draw_if_in_bounds(image, x0 - y, y0 + x, color);
draw_if_in_bounds(image, x0 - x, y0 + y, color);
draw_if_in_bounds(image, x0 - x, y0 - y, color);
draw_if_in_bounds(image, x0 - y, y0 - x, color);
draw_if_in_bounds(image, x0 + y, y0 - x, color);
draw_if_in_bounds(image, x0 + x, y0 - y, color);
x += 1;
if p < 0 {
p += 2 * x + 1;
} else {
y -= 1;
p += 2 * (x - y) + 1;
}
}
}
fn draw_filled_circle_mut(image: &mut RgbaImage, center: (i32, i32), radius: i32, color: Rgba<u8>) {
let mut x = 0;
let mut y = radius;
let mut p = 1 - radius;
let (x0, y0) = center;
while x <= y {
draw_line_segment_mut(
image,
((x0 - x) as f32, (y0 + y) as f32),
((x0 + x) as f32, (y0 + y) as f32),
color,
);
draw_line_segment_mut(
image,
((x0 - y) as f32, (y0 + x) as f32),
((x0 + y) as f32, (y0 + x) as f32),
color,
);
draw_line_segment_mut(
image,
((x0 - x) as f32, (y0 - y) as f32),
((x0 + x) as f32, (y0 - y) as f32),
color,
);
draw_line_segment_mut(
image,
((x0 - y) as f32, (y0 - x) as f32),
((x0 + y) as f32, (y0 - x) as f32),
color,
);
x += 1;
if p < 0 {
p += 2 * x + 1;
} else {
y -= 1;
p += 2 * (x - y) + 1;
}
}
}
fn draw_polygon_mut(image: &mut RgbaImage, poly: &[Point<i32>], color: Rgba<u8>) {
if poly.is_empty() || image.width() == 0 || image.height() == 0 {
return;
}
let mut y_min = i32::MAX;
let mut y_max = i32::MIN;
for point in poly {
y_min = y_min.min(point.y);
y_max = y_max.max(point.y);
}
y_min = 0.max(y_min.min(image.height() as i32 - 1));
y_max = 0.max(y_max.min(image.height() as i32 - 1));
let mut closed = poly.to_vec();
let first = poly[0];
let last = poly[poly.len() - 1];
if first != last {
closed.push(first);
}
let edges: Vec<&[Point<i32>]> = closed.windows(2).collect();
let mut intersections = Vec::new();
for y in y_min..=y_max {
for edge in &edges {
let p0 = edge[0];
let p1 = edge[1];
if p0.y <= y && p1.y >= y || p1.y <= y && p0.y >= y {
if p0.y == p1.y {
intersections.push(p0.x);
intersections.push(p1.x);
} else if p0.y == y || p1.y == y {
if p1.y > y {
intersections.push(p0.x);
}
if p0.y > y {
intersections.push(p1.x);
}
} else {
let dy = i64::from(p1.y) - i64::from(p0.y);
let dx = i64::from(p1.x) - i64::from(p0.x);
let fraction = (i64::from(y) - i64::from(p0.y)) as f64 / dy as f64;
let inter = p0.x as f64 + fraction * dx as f64;
intersections.push(round_f64_to_i32_saturating(inter));
}
}
}
intersections.sort_unstable();
for range in intersections.chunks(2) {
if range.len() < 2 {
continue;
}
let mut from = range[0].min(image.width() as i32);
let mut to = range[1].min(image.width() as i32 - 1);
if from < image.width() as i32 && to >= 0 {
from = from.max(0);
to = to.max(0);
for x in from..=to {
image.put_pixel(x as u32, y as u32, color);
}
}
}
intersections.clear();
}
for edge in edges {
draw_line_segment_mut(
image,
(edge[0].x as f32, edge[0].y as f32),
(edge[1].x as f32, edge[1].y as f32),
color,
);
}
}
fn round_f64_to_i32_saturating(value: f64) -> i32 {
if !value.is_finite() {
0
} else if value <= i32::MIN as f64 {
i32::MIN
} else if value >= i32::MAX as f64 {
i32::MAX
} else {
value.round() as i32
}
}
#[cfg(feature = "fuzzing")]
#[doc(hidden)]
/// Internal fuzz harness entry point.
///
/// This is not a stable consumer API. The crate refuses non-fuzzing release
/// builds with the `fuzzing` feature enabled.
pub fn fuzz_draw_polygon_rgba(width: u32, height: u32, points: &[(i32, i32)], color: [u8; 4]) {
let mut image = RgbaImage::new(width, height);
let polygon: Vec<_> = points.iter().map(|&(x, y)| Point::new(x, y)).collect();
draw_polygon_mut(&mut image, &polygon, Rgba(color));
}
fn interpolate(left: Rgba<u8>, right: Rgba<u8>, left_weight: f32) -> Rgba<u8> {
let right_weight = 1.0 - left_weight;
Rgba([
weighted_channel_sum(left.0[0], right.0[0], left_weight, right_weight),
weighted_channel_sum(left.0[1], right.0[1], left_weight, right_weight),
weighted_channel_sum(left.0[2], right.0[2], left_weight, right_weight),
weighted_channel_sum(left.0[3], right.0[3], left_weight, right_weight),
])
}
fn weighted_channel_sum(left: u8, right: u8, left_weight: f32, right_weight: f32) -> u8 {
let total_weight = left_weight + right_weight;
if !total_weight.is_finite() || total_weight <= 0.0 {
return u8::MIN;
}
let value = (left as f32 * left_weight + right as f32 * right_weight) / total_weight;
if value.is_finite() {
value.clamp(u8::MIN as f32, u8::MAX as f32) as u8
} else {
u8::MIN
}
}
fn draw_if_in_bounds(image: &mut RgbaImage, x: i32, y: i32, color: Rgba<u8>) {
if in_bounds(image, x, y) {
image.put_pixel(x as u32, y as u32, color);
}
}
fn in_bounds(image: &RgbaImage, x: i32, y: i32) -> bool {
x >= 0 && x < image.width() as i32 && y >= 0 && y < image.height() as i32
}
/// Input parameters for a generated avatar image.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AvatarSpec {
width: u32,
height: u32,
seed: u64,
}
impl AvatarSpec {
pub const fn new(width: u32, height: u32, seed: u64) -> Result<Self, AvatarSpecError> {
if Self::dimensions_are_supported(width, height) {
Ok(Self {
width,
height,
seed,
})
} else {
Err(AvatarSpecError { width, height })
}
}
const fn new_unchecked(width: u32, height: u32, seed: u64) -> Self {
Self {
width,
height,
seed,
}
}
pub const fn width(self) -> u32 {
self.width
}
pub const fn height(self) -> u32 {
self.height
}
pub const fn seed(self) -> u64 {
self.seed
}
pub const fn pixel_count(self) -> usize {
(self.width as usize) * (self.height as usize)
}
pub const fn rgba_buffer_len(self) -> usize {
self.pixel_count() * AVATAR_RGBA_BYTES_PER_PIXEL
}
pub const fn render_resource_budget(
self,
concurrent_renders: usize,
) -> AvatarRenderResourceBudget {
AvatarRenderResourceBudget::new(self, concurrent_renders)
}
pub const fn is_supported(self) -> bool {
Self::dimensions_are_supported(self.width, self.height)
}
const fn dimensions_are_supported(width: u32, height: u32) -> bool {
width >= MIN_AVATAR_DIMENSION
&& height >= MIN_AVATAR_DIMENSION
&& width <= MAX_AVATAR_DIMENSION
&& height <= MAX_AVATAR_DIMENSION
}
pub fn validate(self) -> Result<(), AvatarSpecError> {
if self.is_supported() {
Ok(())
} else {
Err(AvatarSpecError {
width: self.width,
height: self.height,
})
}
}
}
impl Default for AvatarSpec {
fn default() -> Self {
Self::new_unchecked(256, 256, 1)
}
}
/// Resource budget estimate for raster rendering.
///
/// This type intentionally models the raw RGBA buffer only. Encoders may need
/// additional temporary memory, so services should leave headroom above this
/// estimate when sizing request concurrency limits.
#[must_use = "use AvatarRenderResourceBudget to size service-level render concurrency limits"]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AvatarRenderResourceBudget {
spec: AvatarSpec,
concurrent_renders: usize,
}
impl AvatarRenderResourceBudget {
pub const fn new(spec: AvatarSpec, concurrent_renders: usize) -> Self {
Self {
spec,
concurrent_renders,
}
}
pub const fn spec(self) -> AvatarSpec {
self.spec
}
pub const fn concurrent_renders(self) -> usize {
self.concurrent_renders
}
pub const fn raw_rgba_bytes_per_render(self) -> usize {
self.spec.rgba_buffer_len()
}
pub const fn raw_rgba_bytes_for_concurrent_renders(self) -> usize {
self.raw_rgba_bytes_per_render()
.saturating_mul(self.concurrent_renders)
}
pub const fn max_supported_raw_rgba_bytes_for_concurrent_renders(
concurrent_renders: usize,
) -> usize {
MAX_AVATAR_RGBA_BYTES.saturating_mul(concurrent_renders)
}
pub const fn max_concurrent_renders_for_memory_budget(
spec: AvatarSpec,
memory_budget_bytes: usize,
) -> usize {
let per_render = spec.rgba_buffer_len();
match memory_budget_bytes.checked_div(per_render) {
Some(value) => value,
None => 0,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AvatarSpecError {
width: u32,
height: u32,
}
impl AvatarSpecError {
pub const fn width(self) -> u32 {
self.width
}
pub const fn height(self) -> u32 {
self.height
}
}
impl std::fmt::Display for AvatarSpecError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"avatar dimensions must be between {MIN_AVATAR_DIMENSION} and {MAX_AVATAR_DIMENSION} pixels per side, got {}x{}",
self.width, self.height
)
}
}
impl std::error::Error for AvatarSpecError {}
fn validate_image_avatar_spec(spec: AvatarSpec) -> ImageResult<()> {
spec.validate().map_err(avatar_spec_error_to_image_error)
}
fn avatar_spec_error_to_image_error(_: AvatarSpecError) -> ImageError {
ImageError::Limits(LimitError::from_kind(LimitErrorKind::DimensionError))
}
fn avatar_identity_error_to_image_error(error: AvatarIdentityError) -> ImageError {
ImageError::IoError(std::io::Error::new(std::io::ErrorKind::InvalidInput, error))
}
fn avatar_render_error_to_image_error(error: AvatarRenderError) -> ImageError {
match error {
AvatarRenderError::Spec(error) => avatar_spec_error_to_image_error(error),
AvatarRenderError::Identity(error) => avatar_identity_error_to_image_error(error),
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AvatarIdentityComponent {
Input,
Tenant,
StyleVersion,
}
impl AvatarIdentityComponent {
pub const fn as_str(self) -> &'static str {
match self {
Self::Input => "identity input",
Self::Tenant => "namespace tenant",
Self::StyleVersion => "namespace style version",
}
}
}
impl std::fmt::Display for AvatarIdentityComponent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AvatarIdentityError {
component: AvatarIdentityComponent,
length: usize,
max: usize,
}
impl AvatarIdentityError {
pub const fn component(self) -> AvatarIdentityComponent {
self.component
}
pub const fn length(self) -> usize {
self.length
}
pub const fn max(self) -> usize {
self.max
}
}
impl std::fmt::Display for AvatarIdentityError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} must be at most {} bytes, got {} bytes",
self.component, self.max, self.length
)
}
}
impl std::error::Error for AvatarIdentityError {}
#[derive(Debug)]
pub enum AvatarRenderError {
Spec(AvatarSpecError),
Identity(AvatarIdentityError),
}
impl From<AvatarSpecError> for AvatarRenderError {
fn from(error: AvatarSpecError) -> Self {
Self::Spec(error)
}
}
impl From<AvatarIdentityError> for AvatarRenderError {
fn from(error: AvatarIdentityError) -> Self {
Self::Identity(error)
}
}
impl std::fmt::Display for AvatarRenderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Spec(error) => error.fmt(f),
Self::Identity(error) => error.fmt(f),
}
}
}
impl std::error::Error for AvatarRenderError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Spec(error) => Some(error),
Self::Identity(error) => Some(error),
}
}
}
/// Options for deriving a stable avatar identity.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AvatarIdentityOptions<'a> {
namespace: AvatarNamespace<'a>,
}
impl<'a> AvatarIdentityOptions<'a> {
pub const fn new(namespace: AvatarNamespace<'a>) -> Self {
Self { namespace }
}
pub const fn namespace(self) -> AvatarNamespace<'a> {
self.namespace
}
}
impl Default for AvatarIdentityOptions<'static> {
fn default() -> Self {
Self::new(AvatarNamespace::DEFAULT)
}
}
/// A stable avatar identity derived from a fixed 64-byte digest.
///
/// This is intended for Robohash-style uniqueness: the same input always maps
/// to the same visual genome, while different inputs produce different shape
/// and palette parameters with negligible collision risk.
///
/// # Security
///
/// `AvatarIdentity` implements `Clone`. Each clone is independently zeroized
/// on drop. Callers operating in high-assurance environments should keep clones
/// as short-lived as possible to reduce the window during which digest bytes
/// exist in multiple memory locations.
#[derive(Clone, Eq)]
pub struct AvatarIdentity {
digest: [u8; 64],
}
impl AvatarIdentity {
pub fn new<T: AsRef<[u8]>>(input: T) -> Result<Self, AvatarIdentityError> {
Self::new_with_namespace(AvatarNamespace::default(), input)
}
pub fn new_with_namespace<T: AsRef<[u8]>>(
namespace: AvatarNamespace<'_>,
input: T,
) -> Result<Self, AvatarIdentityError> {
Self::new_with_options(AvatarIdentityOptions::new(namespace), input)
}
pub fn new_with_options<T: AsRef<[u8]>>(
options: AvatarIdentityOptions<'_>,
input: T,
) -> Result<Self, AvatarIdentityError> {
let input = input.as_ref();
validate_identity_component(
AvatarIdentityComponent::Input,
input.len(),
MAX_AVATAR_ID_BYTES,
)?;
validate_identity_component(
AvatarIdentityComponent::Tenant,
options.namespace.tenant.len(),
MAX_AVATAR_NAMESPACE_COMPONENT_BYTES,
)?;
validate_identity_component(
AvatarIdentityComponent::StyleVersion,
options.namespace.style_version.len(),
MAX_AVATAR_NAMESPACE_COMPONENT_BYTES,
)?;
Ok(Self::new_unchecked(options, input))
}
fn new_unchecked(options: AvatarIdentityOptions<'_>, input: &[u8]) -> Self {
Self {
digest: derive_identity_digest(options, input),
}
}
fn rng_seed(&self) -> Zeroizing<[u8; 32]> {
let mut seed = Zeroizing::new([0u8; 32]);
seed.copy_from_slice(&self.digest[32..64]);
seed
}
fn byte(&self, index: usize) -> u8 {
self.digest[index]
}
fn unit_f32(&self, index: usize) -> f32 {
self.byte(index) as f32 / 255.0
}
}
impl Zeroize for AvatarIdentity {
fn zeroize(&mut self) {
self.digest.zeroize();
}
}
impl std::fmt::Debug for AvatarIdentity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AvatarIdentity")
.field("digest", &"[REDACTED]")
.finish()
}
}
impl PartialEq for AvatarIdentity {
fn eq(&self, other: &Self) -> bool {
self.digest.ct_eq(&other.digest).into()
}
}
impl Drop for AvatarIdentity {
fn drop(&mut self) {
self.zeroize();
}
}
fn validate_identity_component(
component: AvatarIdentityComponent,
length: usize,
max: usize,
) -> Result<(), AvatarIdentityError> {
if length <= max {
Ok(())
} else {
Err(AvatarIdentityError {
component,
length,
max,
})
}
}
fn derive_identity_digest(options: AvatarIdentityOptions<'_>, input: &[u8]) -> [u8; 64] {
let mut preimage = identity_hash_preimage(options, input);
let digest = active_identity_digest(&preimage);
preimage.zeroize();
digest
}
fn identity_hash_preimage(options: AvatarIdentityOptions<'_>, input: &[u8]) -> Vec<u8> {
let algorithm_overhead = if active_hash_algorithm_is_domain_separated() {
length_prefixed_component_size(HASH_DOMAIN_ALGORITHM_COMPONENT)
+ length_prefixed_component_size(ACTIVE_HASH_ALGORITHM_LABEL)
} else {
0
};
let mut preimage = Vec::with_capacity(
length_prefixed_component_size(HASH_DOMAIN)
+ algorithm_overhead
+ length_prefixed_component_size(options.namespace.tenant.as_bytes())
+ length_prefixed_component_size(options.namespace.style_version.as_bytes())
+ length_prefixed_component_size(input),
);
update_hash_input_component(&mut preimage, HASH_DOMAIN);
if active_hash_algorithm_is_domain_separated() {
update_hash_input_component(&mut preimage, HASH_DOMAIN_ALGORITHM_COMPONENT);
update_hash_input_component(&mut preimage, ACTIVE_HASH_ALGORITHM_LABEL);
}
update_hash_input_component(&mut preimage, options.namespace.tenant.as_bytes());
update_hash_input_component(&mut preimage, options.namespace.style_version.as_bytes());
update_hash_input_component(&mut preimage, input);
preimage
}
const fn active_hash_algorithm_is_domain_separated() -> bool {
cfg!(any(feature = "blake3", feature = "xxh3"))
}
#[cfg(feature = "blake3")]
fn active_identity_digest(preimage: &[u8]) -> [u8; 64] {
blake3_digest(preimage)
}
#[cfg(all(not(feature = "blake3"), feature = "xxh3"))]
fn active_identity_digest(preimage: &[u8]) -> [u8; 64] {
xxh3_128_digest(preimage)
}
#[cfg(all(not(feature = "blake3"), not(feature = "xxh3")))]
fn active_identity_digest(preimage: &[u8]) -> [u8; 64] {
sha512_digest(preimage)
}
const fn length_prefixed_component_size(bytes: &[u8]) -> usize {
std::mem::size_of::<u64>() + bytes.len()
}
fn update_hash_input_component(preimage: &mut Vec<u8>, bytes: &[u8]) {
preimage.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
preimage.extend_from_slice(bytes);
}
#[cfg(not(any(feature = "blake3", feature = "xxh3")))]
fn sha512_digest(preimage: &[u8]) -> [u8; 64] {
let mut hasher = Sha512::new();
hasher.update(preimage);
hasher.finalize().into()
}
#[cfg(feature = "blake3")]
fn blake3_digest(preimage: &[u8]) -> [u8; 64] {
let mut hasher = blake3::Hasher::new();
hasher.update(preimage);
let mut digest = [0u8; 64];
let mut reader = hasher.finalize_xof();
reader.fill(&mut digest);
reader.zeroize();
hasher.zeroize();
digest
}
#[cfg(feature = "xxh3")]
fn xxh3_128_digest(preimage: &[u8]) -> [u8; 64] {
let mut digest = [0u8; 64];
for chunk in 0..4 {
let mut chunk_input = Vec::with_capacity(
preimage.len()
+ length_prefixed_component_size(HASH_XOF_CHUNK_COMPONENT)
+ length_prefixed_component_size(&[chunk as u8]),
);
chunk_input.extend_from_slice(preimage);
update_hash_input_component(&mut chunk_input, HASH_XOF_CHUNK_COMPONENT);
update_hash_input_component(&mut chunk_input, &[chunk as u8]);
let mut chunk_digest = xxhash_rust::xxh3::xxh3_128(&chunk_input).to_le_bytes();
let offset = chunk * chunk_digest.len();
digest[offset..offset + chunk_digest.len()].copy_from_slice(&chunk_digest);
chunk_digest.zeroize();
chunk_input.zeroize();
}
digest
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AvatarNamespace<'a> {
tenant: &'a str,
style_version: &'a str,
}
impl<'a> AvatarNamespace<'a> {
pub const DEFAULT: Self = Self {
tenant: "public",
style_version: "v2",
};
pub fn new(tenant: &'a str, style_version: &'a str) -> Result<Self, AvatarIdentityError> {
validate_identity_component(
AvatarIdentityComponent::Tenant,
tenant.len(),
MAX_AVATAR_NAMESPACE_COMPONENT_BYTES,
)?;
validate_identity_component(
AvatarIdentityComponent::StyleVersion,
style_version.len(),
MAX_AVATAR_NAMESPACE_COMPONENT_BYTES,
)?;
Ok(Self {
tenant,
style_version,
})
}
pub const fn tenant(self) -> &'a str {
self.tenant
}
pub const fn style_version(self) -> &'a str {
self.style_version
}
}
impl Default for AvatarNamespace<'_> {
fn default() -> Self {
Self::DEFAULT
}
}
/// Trait for renderers that can draw reusable avatar styles onto an image buffer.
pub trait AvatarRenderer {
fn render(&self, spec: AvatarSpec) -> Result<RgbaImage, AvatarSpecError>;
}
/// Export formats for encoded avatar assets.
///
/// `WebP` is the default because it is the more modern distribution format and
/// is usually smaller than PNG for generated avatar art.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum AvatarOutputFormat {
/// Lossless WebP output.
#[default]
WebP,
/// Optional lossless PNG output.
#[cfg(feature = "png")]
Png,
/// Optional JPEG output with transparent pixels composited over white.
#[cfg(feature = "jpeg")]
Jpeg,
/// Optional GIF output.
///
/// # Warning
///
/// GIF encoding performs 256-color quantization inside the `image` crate.
/// Those internal quantization buffers are not accessible to `hashavatar`
/// and are not zeroized by this crate. For high-assurance deployments,
/// prefer `AvatarOutputFormat::WebP` or PNG output.
#[cfg(feature = "gif")]
Gif,
}
impl AvatarOutputFormat {
pub const ALL: &'static [Self] = &[
Self::WebP,
#[cfg(feature = "png")]
Self::Png,
#[cfg(feature = "jpeg")]
Self::Jpeg,
#[cfg(feature = "gif")]
Self::Gif,
];
pub fn from_byte(value: u8) -> Self {
Self::ALL[usize::from(value) % Self::ALL.len()]
}
pub const fn as_str(self) -> &'static str {
match self {
Self::WebP => "webp",
#[cfg(feature = "png")]
Self::Png => "png",
#[cfg(feature = "jpeg")]
Self::Jpeg => "jpg",
#[cfg(feature = "gif")]
Self::Gif => "gif",
}
}
}
impl FromStr for AvatarOutputFormat {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"webp" => Ok(Self::WebP),
#[cfg(feature = "png")]
"png" => Ok(Self::Png),
#[cfg(feature = "jpeg")]
"jpg" | "jpeg" => Ok(Self::Jpeg),
#[cfg(feature = "gif")]
"gif" => Ok(Self::Gif),
_ => Err("unsupported avatar output format"),
}
}
}
impl std::fmt::Display for AvatarOutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
/// Avatar family selection.
///
/// Values can be round-tripped through [`AvatarKind::as_str`] and
/// [`FromStr`]. `Icecream` also accepts `ice-cream` and `ice_cream` when
/// parsing user input.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum AvatarKind {
/// Cat face avatar.
#[default]
Cat,
/// Dog face avatar.
Dog,
/// Robot head avatar.
Robot,
/// Fox face avatar.
Fox,
/// Alien face avatar.
Alien,
/// Monster face avatar.
Monster,
/// Ghost avatar.
Ghost,
/// Slime creature avatar.
Slime,
/// Bird avatar.
Bird,
/// Wizard avatar.
Wizard,
/// Skull avatar.
Skull,
/// Paw-print avatar.
Paws,
/// Ringed planet avatar.
Planet,
/// Rocket avatar.
Rocket,
/// Mushroom avatar.
Mushroom,
/// Cactus avatar.
Cactus,
/// Frog face avatar.
Frog,
/// Panda face avatar.
Panda,
/// Cupcake avatar.
Cupcake,
/// Pizza slice avatar.
Pizza,
/// Ice cream cone avatar.
Icecream,
/// Octopus avatar.
Octopus,
/// Knight helmet avatar.
Knight,
/// Bear face avatar.
Bear,
/// Penguin avatar.
Penguin,
/// Dragon avatar.
Dragon,
/// Ninja avatar.
Ninja,
/// Astronaut avatar.
Astronaut,
/// Diamond avatar.
Diamond,
/// Coffee cup avatar.
CoffeeCup,
/// Shield avatar.
Shield,
}
impl AvatarKind {
pub const ALL: &'static [Self] = &[
Self::Cat,
Self::Dog,
Self::Robot,
Self::Fox,
Self::Alien,
Self::Monster,
Self::Ghost,
Self::Slime,
Self::Bird,
Self::Wizard,
Self::Skull,
Self::Paws,
Self::Planet,
Self::Rocket,
Self::Mushroom,
Self::Cactus,
Self::Frog,
Self::Panda,
Self::Cupcake,
Self::Pizza,
Self::Icecream,
Self::Octopus,
Self::Knight,
Self::Bear,
Self::Penguin,
Self::Dragon,
Self::Ninja,
Self::Astronaut,
Self::Diamond,
Self::CoffeeCup,
Self::Shield,
];
pub fn from_byte(value: u8) -> Self {
Self::ALL[usize::from(value) % Self::ALL.len()]
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Cat => "cat",
Self::Dog => "dog",
Self::Robot => "robot",
Self::Fox => "fox",
Self::Alien => "alien",
Self::Monster => "monster",
Self::Ghost => "ghost",
Self::Slime => "slime",
Self::Bird => "bird",
Self::Wizard => "wizard",
Self::Skull => "skull",
Self::Paws => "paws",
Self::Planet => "planet",
Self::Rocket => "rocket",
Self::Mushroom => "mushroom",
Self::Cactus => "cactus",
Self::Frog => "frog",
Self::Panda => "panda",
Self::Cupcake => "cupcake",
Self::Pizza => "pizza",
Self::Icecream => "icecream",
Self::Octopus => "octopus",
Self::Knight => "knight",
Self::Bear => "bear",
Self::Penguin => "penguin",
Self::Dragon => "dragon",
Self::Ninja => "ninja",
Self::Astronaut => "astronaut",
Self::Diamond => "diamond",
Self::CoffeeCup => "coffee-cup",
Self::Shield => "shield",
}
}
/// Returns whether this family has face anchors for accessories and
/// expressions.
///
/// Families without face anchors still support canvas-level color and
/// frame-shape layers. Accessory and expression choices are accepted but
/// skipped deterministically for those families.
pub const fn supports_face_layers(self) -> bool {
!matches!(
self,
Self::Paws
| Self::Planet
| Self::Rocket
| Self::Mushroom
| Self::Cactus
| Self::Cupcake
| Self::Pizza
| Self::Icecream
| Self::Diamond
| Self::CoffeeCup
| Self::Shield
)
}
}
impl FromStr for AvatarKind {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"cat" => Ok(Self::Cat),
"dog" => Ok(Self::Dog),
"robot" => Ok(Self::Robot),
"fox" => Ok(Self::Fox),
"alien" => Ok(Self::Alien),
"monster" => Ok(Self::Monster),
"ghost" => Ok(Self::Ghost),
"slime" => Ok(Self::Slime),
"bird" => Ok(Self::Bird),
"wizard" => Ok(Self::Wizard),
"skull" => Ok(Self::Skull),
"paws" => Ok(Self::Paws),
"planet" => Ok(Self::Planet),
"rocket" => Ok(Self::Rocket),
"mushroom" => Ok(Self::Mushroom),
"cactus" => Ok(Self::Cactus),
"frog" => Ok(Self::Frog),
"panda" => Ok(Self::Panda),
"cupcake" => Ok(Self::Cupcake),
"pizza" => Ok(Self::Pizza),
"icecream" | "ice-cream" | "ice_cream" => Ok(Self::Icecream),
"octopus" => Ok(Self::Octopus),
"knight" => Ok(Self::Knight),
"bear" => Ok(Self::Bear),
"penguin" => Ok(Self::Penguin),
"dragon" => Ok(Self::Dragon),
"ninja" => Ok(Self::Ninja),
"astronaut" => Ok(Self::Astronaut),
"diamond" => Ok(Self::Diamond),
"coffee-cup" | "coffee_cup" | "coffeecup" => Ok(Self::CoffeeCup),
"shield" => Ok(Self::Shield),
_ => Err("unsupported avatar kind"),
}
}
}
impl std::fmt::Display for AvatarKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
/// Canvas background mode for raster and SVG avatar output.
///
/// `Themed` is identity and family aware. The fixed modes are useful for
/// predictable compositing, while `Transparent` leaves the SVG background out
/// and uses a fully transparent raster canvas.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum AvatarBackground {
/// Identity-derived background chosen by the selected avatar family.
#[default]
Themed,
/// Pure white background.
White,
/// Pure black background.
Black,
/// Charcoal background, useful for dark UI previews.
Dark,
/// Subtle off-white background.
Light,
/// Fully transparent background.
Transparent,
/// Light dotted pattern.
PolkaDot,
/// Subtle diagonal stripe pattern.
Striped,
/// Small checkerboard pattern.
Checkerboard,
/// Fine grid pattern.
Grid,
/// Warm sunrise gradient.
Sunrise,
/// Cool ocean gradient.
Ocean,
/// Dark star-field background.
Starry,
}
impl AvatarBackground {
pub const ALL: &'static [Self] = &[
Self::Themed,
Self::White,
Self::Black,
Self::Dark,
Self::Light,
Self::Transparent,
Self::PolkaDot,
Self::Striped,
Self::Checkerboard,
Self::Grid,
Self::Sunrise,
Self::Ocean,
Self::Starry,
];
pub fn from_byte(value: u8) -> Self {
Self::ALL[usize::from(value) % Self::ALL.len()]
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Themed => "themed",
Self::White => "white",
Self::Black => "black",
Self::Dark => "dark",
Self::Light => "light",
Self::Transparent => "transparent",
Self::PolkaDot => "polka-dot",
Self::Striped => "striped",
Self::Checkerboard => "checkerboard",
Self::Grid => "grid",
Self::Sunrise => "sunrise",
Self::Ocean => "ocean",
Self::Starry => "starry",
}
}
}
impl FromStr for AvatarBackground {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"themed" => Ok(Self::Themed),
"white" => Ok(Self::White),
"black" => Ok(Self::Black),
"dark" => Ok(Self::Dark),
"light" => Ok(Self::Light),
"transparent" => Ok(Self::Transparent),
"polka-dot" | "polka_dot" | "polkadot" => Ok(Self::PolkaDot),
"striped" | "stripes" => Ok(Self::Striped),
"checkerboard" | "checker-board" | "checker_board" => Ok(Self::Checkerboard),
"grid" => Ok(Self::Grid),
"sunrise" => Ok(Self::Sunrise),
"ocean" => Ok(Self::Ocean),
"starry" | "stars" => Ok(Self::Starry),
_ => Err("unsupported avatar background"),
}
}
}
impl std::fmt::Display for AvatarBackground {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
/// Optional avatar accessory layer.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum AvatarAccessory {
/// No accessory layer.
#[default]
None,
/// Simple glasses overlay.
Glasses,
/// Hat overlay.
Hat,
/// Headphones overlay.
Headphones,
/// Crown overlay.
Crown,
/// Bowtie overlay.
Bowtie,
/// Eyepatch overlay.
Eyepatch,
/// Scarf overlay.
Scarf,
/// Halo overlay.
Halo,
/// Horn overlay.
Horns,
}
impl AvatarAccessory {
pub const ALL: &'static [Self] = &[
Self::None,
Self::Glasses,
Self::Hat,
Self::Headphones,
Self::Crown,
Self::Bowtie,
Self::Eyepatch,
Self::Scarf,
Self::Halo,
Self::Horns,
];
pub fn from_byte(value: u8) -> Self {
Self::ALL[usize::from(value) % Self::ALL.len()]
}
pub const fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::Glasses => "glasses",
Self::Hat => "hat",
Self::Headphones => "headphones",
Self::Crown => "crown",
Self::Bowtie => "bowtie",
Self::Eyepatch => "eyepatch",
Self::Scarf => "scarf",
Self::Halo => "halo",
Self::Horns => "horns",
}
}
}
impl FromStr for AvatarAccessory {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"none" => Ok(Self::None),
"glasses" => Ok(Self::Glasses),
"hat" => Ok(Self::Hat),
"headphones" => Ok(Self::Headphones),
"crown" => Ok(Self::Crown),
"bowtie" | "bow-tie" | "bow_tie" => Ok(Self::Bowtie),
"eyepatch" | "eye-patch" | "eye_patch" => Ok(Self::Eyepatch),
"scarf" => Ok(Self::Scarf),
"halo" => Ok(Self::Halo),
"horns" => Ok(Self::Horns),
_ => Err("unsupported avatar accessory"),
}
}
}
impl std::fmt::Display for AvatarAccessory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
/// Optional avatar accent color palette.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum AvatarColor {
/// Family default colors.
#[default]
Default,
/// Bright mint accent.
NeonMint,
/// Soft pink accent.
PastelPink,
/// Deep red accent.
Crimson,
/// Warm gold accent.
Gold,
/// Blue-green accent.
DeepSeaBlue,
}
impl AvatarColor {
pub const ALL: &'static [Self] = &[
Self::Default,
Self::NeonMint,
Self::PastelPink,
Self::Crimson,
Self::Gold,
Self::DeepSeaBlue,
];
pub fn from_byte(value: u8) -> Self {
Self::ALL[usize::from(value) % Self::ALL.len()]
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Default => "default",
Self::NeonMint => "neon-mint",
Self::PastelPink => "pastel-pink",
Self::Crimson => "crimson",
Self::Gold => "gold",
Self::DeepSeaBlue => "deep-sea-blue",
}
}
}
impl FromStr for AvatarColor {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"default" => Ok(Self::Default),
"neon-mint" | "neon_mint" | "neonmint" => Ok(Self::NeonMint),
"pastel-pink" | "pastel_pink" | "pastelpink" => Ok(Self::PastelPink),
"crimson" => Ok(Self::Crimson),
"gold" => Ok(Self::Gold),
"deep-sea-blue" | "deep_sea_blue" | "deepseablue" => Ok(Self::DeepSeaBlue),
_ => Err("unsupported avatar color"),
}
}
}
impl std::fmt::Display for AvatarColor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
/// Optional avatar expression layer.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum AvatarExpression {
/// Family default expression.
#[default]
Default,
/// Happy expression overlay.
Happy,
/// Grumpy expression overlay.
Grumpy,
/// Surprised expression overlay.
Surprised,
/// Sleepy expression overlay.
Sleepy,
/// Winking expression overlay.
Winking,
/// Cool expression overlay.
Cool,
/// Crying expression overlay.
Crying,
}
impl AvatarExpression {
pub const ALL: &'static [Self] = &[
Self::Default,
Self::Happy,
Self::Grumpy,
Self::Surprised,
Self::Sleepy,
Self::Winking,
Self::Cool,
Self::Crying,
];
pub fn from_byte(value: u8) -> Self {
Self::ALL[usize::from(value) % Self::ALL.len()]
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Default => "default",
Self::Happy => "happy",
Self::Grumpy => "grumpy",
Self::Surprised => "surprised",
Self::Sleepy => "sleepy",
Self::Winking => "winking",
Self::Cool => "cool",
Self::Crying => "crying",
}
}
}
impl FromStr for AvatarExpression {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"default" => Ok(Self::Default),
"happy" => Ok(Self::Happy),
"grumpy" => Ok(Self::Grumpy),
"surprised" => Ok(Self::Surprised),
"sleepy" => Ok(Self::Sleepy),
"winking" | "wink" => Ok(Self::Winking),
"cool" => Ok(Self::Cool),
"crying" => Ok(Self::Crying),
_ => Err("unsupported avatar expression"),
}
}
}
impl std::fmt::Display for AvatarExpression {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
/// Optional frame shape for the generated avatar.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum AvatarShape {
/// Default square canvas.
#[default]
Square,
/// Circular frame.
Circle,
/// Rounded rectangle frame.
Squircle,
/// Hexagonal frame.
Hexagon,
/// Octagonal frame.
Octagon,
}
impl AvatarShape {
pub const ALL: &'static [Self] = &[
Self::Square,
Self::Circle,
Self::Squircle,
Self::Hexagon,
Self::Octagon,
];
pub fn from_byte(value: u8) -> Self {
Self::ALL[usize::from(value) % Self::ALL.len()]
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Square => "square",
Self::Circle => "circle",
Self::Squircle => "squircle",
Self::Hexagon => "hexagon",
Self::Octagon => "octagon",
}
}
}
impl FromStr for AvatarShape {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"square" => Ok(Self::Square),
"circle" => Ok(Self::Circle),
"squircle" => Ok(Self::Squircle),
"hexagon" => Ok(Self::Hexagon),
"octagon" => Ok(Self::Octagon),
_ => Err("unsupported avatar shape"),
}
}
}
impl std::fmt::Display for AvatarShape {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct AvatarOptions {
pub kind: AvatarKind,
pub background: AvatarBackground,
}
impl AvatarOptions {
pub const fn new(kind: AvatarKind, background: AvatarBackground) -> Self {
Self { kind, background }
}
}
/// Full avatar style selection including the baseline kind/background and
/// optional visual layers.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct AvatarStyleOptions {
pub kind: AvatarKind,
pub background: AvatarBackground,
pub accessory: AvatarAccessory,
pub color: AvatarColor,
pub expression: AvatarExpression,
pub shape: AvatarShape,
}
impl AvatarStyleOptions {
pub const fn new(
kind: AvatarKind,
background: AvatarBackground,
accessory: AvatarAccessory,
color: AvatarColor,
expression: AvatarExpression,
shape: AvatarShape,
) -> Self {
Self {
kind,
background,
accessory,
color,
expression,
shape,
}
}
pub const fn from_options(options: AvatarOptions) -> Self {
Self {
kind: options.kind,
background: options.background,
accessory: AvatarAccessory::None,
color: AvatarColor::Default,
expression: AvatarExpression::Default,
shape: AvatarShape::Square,
}
}
pub fn from_identity(identity: &AvatarIdentity) -> Self {
Self {
kind: AvatarKind::from_byte(identity.byte(AVATAR_STYLE_KIND_BYTE)),
background: AvatarBackground::from_byte(identity.byte(AVATAR_STYLE_BACKGROUND_BYTE)),
accessory: AvatarAccessory::from_byte(identity.byte(AVATAR_STYLE_ACCESSORY_BYTE)),
color: AvatarColor::from_byte(identity.byte(AVATAR_STYLE_COLOR_BYTE)),
expression: AvatarExpression::from_byte(identity.byte(AVATAR_STYLE_EXPRESSION_BYTE)),
shape: AvatarShape::from_byte(identity.byte(AVATAR_STYLE_SHAPE_BYTE)),
}
}
pub const fn legacy_options(self) -> AvatarOptions {
AvatarOptions::new(self.kind, self.background)
}
const fn has_extra_layers(self) -> bool {
!matches!(self.accessory, AvatarAccessory::None)
|| !matches!(self.color, AvatarColor::Default)
|| !matches!(self.expression, AvatarExpression::Default)
|| !matches!(self.shape, AvatarShape::Square)
}
}
impl From<AvatarOptions> for AvatarStyleOptions {
fn from(options: AvatarOptions) -> Self {
Self::from_options(options)
}
}
impl From<AvatarStyleOptions> for AvatarOptions {
fn from(options: AvatarStyleOptions) -> Self {
options.legacy_options()
}
}
/// Cat-face avatar renderer built from simple geometric primitives.
///
/// The face is intentionally stylized:
/// - a rounded head ellipse defines the main silhouette
/// - two ear polygons make the head read as feline rather than circular
/// - wide-set eyes, a small triangular nose, whiskers, and a curved smile complete the expression
#[derive(Clone, Copy, Debug, Default)]
pub struct CatAvatar;
impl AvatarRenderer for CatAvatar {
fn render(&self, spec: AvatarSpec) -> Result<RgbaImage, AvatarSpecError> {
render_cat_avatar(spec)
}
}
/// Cat-face avatar renderer driven by a stable identity digest.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HashedCatAvatar {
identity: AvatarIdentity,
}
impl HashedCatAvatar {
pub fn new<T: AsRef<[u8]>>(input: T) -> Result<Self, AvatarIdentityError> {
Self::new_with_namespace(AvatarNamespace::default(), input)
}
pub fn new_with_namespace<T: AsRef<[u8]>>(
namespace: AvatarNamespace<'_>,
input: T,
) -> Result<Self, AvatarIdentityError> {
Ok(Self {
identity: AvatarIdentity::new_with_namespace(namespace, input)?,
})
}
pub fn new_with_identity_options<T: AsRef<[u8]>>(
options: AvatarIdentityOptions<'_>,
input: T,
) -> Result<Self, AvatarIdentityError> {
Ok(Self {
identity: AvatarIdentity::new_with_options(options, input)?,
})
}
pub fn identity(&self) -> &AvatarIdentity {
&self.identity
}
}
impl AvatarRenderer for HashedCatAvatar {
fn render(&self, spec: AvatarSpec) -> Result<RgbaImage, AvatarSpecError> {
render_cat_avatar_for_identity(spec, &self.identity)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HashedDogAvatar {
identity: AvatarIdentity,
background: AvatarBackground,
}
impl HashedDogAvatar {
pub fn new<T: AsRef<[u8]>>(
input: T,
background: AvatarBackground,
) -> Result<Self, AvatarIdentityError> {
Self::new_with_namespace(AvatarNamespace::default(), input, background)
}
pub fn new_with_namespace<T: AsRef<[u8]>>(
namespace: AvatarNamespace<'_>,
input: T,
background: AvatarBackground,
) -> Result<Self, AvatarIdentityError> {
Ok(Self {
identity: AvatarIdentity::new_with_namespace(namespace, input)?,
background,
})
}
pub fn new_with_identity_options<T: AsRef<[u8]>>(
options: AvatarIdentityOptions<'_>,
input: T,
background: AvatarBackground,
) -> Result<Self, AvatarIdentityError> {
Ok(Self {
identity: AvatarIdentity::new_with_options(options, input)?,
background,
})
}
}
impl AvatarRenderer for HashedDogAvatar {
fn render(&self, spec: AvatarSpec) -> Result<RgbaImage, AvatarSpecError> {
render_dog_avatar_for_identity(spec, &self.identity, self.background)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HashedRobotAvatar {
identity: AvatarIdentity,
background: AvatarBackground,
}
impl HashedRobotAvatar {
pub fn new<T: AsRef<[u8]>>(
input: T,
background: AvatarBackground,
) -> Result<Self, AvatarIdentityError> {
Self::new_with_namespace(AvatarNamespace::default(), input, background)
}
pub fn new_with_namespace<T: AsRef<[u8]>>(
namespace: AvatarNamespace<'_>,
input: T,
background: AvatarBackground,
) -> Result<Self, AvatarIdentityError> {
Ok(Self {
identity: AvatarIdentity::new_with_namespace(namespace, input)?,
background,
})
}
pub fn new_with_identity_options<T: AsRef<[u8]>>(
options: AvatarIdentityOptions<'_>,
input: T,
background: AvatarBackground,
) -> Result<Self, AvatarIdentityError> {
Ok(Self {
identity: AvatarIdentity::new_with_options(options, input)?,
background,
})
}
}
impl AvatarRenderer for HashedRobotAvatar {
fn render(&self, spec: AvatarSpec) -> Result<RgbaImage, AvatarSpecError> {
render_robot_avatar_for_identity(spec, &self.identity, self.background)
}
}
/// Render and encode an avatar into memory.
pub fn encode_avatar<R: AvatarRenderer>(
renderer: &R,
spec: AvatarSpec,
format: AvatarOutputFormat,
) -> ImageResult<Vec<u8>> {
validate_image_avatar_spec(spec)?;
let image = renderer
.render(spec)
.map_err(avatar_spec_error_to_image_error)?;
encode_owned_rgba_image(image, format)
}
/// Render and encode a cat avatar into memory.
pub fn encode_cat_avatar(spec: AvatarSpec, format: AvatarOutputFormat) -> ImageResult<Vec<u8>> {
encode_avatar(&CatAvatar, spec, format)
}
/// Render and encode a cat avatar for a stable identity string.
pub fn encode_cat_avatar_for_id<T: AsRef<[u8]>>(
spec: AvatarSpec,
id: T,
format: AvatarOutputFormat,
) -> ImageResult<Vec<u8>> {
let renderer = HashedCatAvatar::new(id).map_err(avatar_identity_error_to_image_error)?;
encode_avatar(&renderer, spec, format)
}
pub fn encode_avatar_for_id<T: AsRef<[u8]>>(
spec: AvatarSpec,
id: T,
format: AvatarOutputFormat,
options: AvatarOptions,
) -> ImageResult<Vec<u8>> {
encode_avatar_for_namespace(spec, AvatarNamespace::default(), id, format, options)
}
pub fn encode_avatar_for_namespace<T: AsRef<[u8]>>(
spec: AvatarSpec,
namespace: AvatarNamespace<'_>,
id: T,
format: AvatarOutputFormat,
options: AvatarOptions,
) -> ImageResult<Vec<u8>> {
encode_avatar_with_identity_options(
spec,
AvatarIdentityOptions::new(namespace),
id,
format,
options,
)
}
pub fn encode_avatar_with_identity_options<T: AsRef<[u8]>>(
spec: AvatarSpec,
identity_options: AvatarIdentityOptions<'_>,
id: T,
format: AvatarOutputFormat,
options: AvatarOptions,
) -> ImageResult<Vec<u8>> {
let image = render_avatar_with_identity_options(spec, identity_options, id, options)
.map_err(avatar_render_error_to_image_error)?;
encode_owned_rgba_image(image, format)
}
pub fn encode_avatar_style_for_id<T: AsRef<[u8]>>(
spec: AvatarSpec,
id: T,
format: AvatarOutputFormat,
style: AvatarStyleOptions,
) -> ImageResult<Vec<u8>> {
encode_avatar_style_for_namespace(spec, AvatarNamespace::default(), id, format, style)
}
pub fn encode_avatar_style_for_namespace<T: AsRef<[u8]>>(
spec: AvatarSpec,
namespace: AvatarNamespace<'_>,
id: T,
format: AvatarOutputFormat,
style: AvatarStyleOptions,
) -> ImageResult<Vec<u8>> {
encode_avatar_style_with_identity_options(
spec,
AvatarIdentityOptions::new(namespace),
id,
format,
style,
)
}
pub fn encode_avatar_style_with_identity_options<T: AsRef<[u8]>>(
spec: AvatarSpec,
identity_options: AvatarIdentityOptions<'_>,
id: T,
format: AvatarOutputFormat,
style: AvatarStyleOptions,
) -> ImageResult<Vec<u8>> {
let image = render_avatar_style_with_identity_options(spec, identity_options, id, style)
.map_err(avatar_render_error_to_image_error)?;
encode_owned_rgba_image(image, format)
}
pub fn encode_avatar_auto_for_id<T: AsRef<[u8]>>(
spec: AvatarSpec,
id: T,
format: AvatarOutputFormat,
) -> ImageResult<Vec<u8>> {
encode_avatar_auto_for_namespace(spec, AvatarNamespace::default(), id, format)
}
pub fn encode_avatar_auto_for_namespace<T: AsRef<[u8]>>(
spec: AvatarSpec,
namespace: AvatarNamespace<'_>,
id: T,
format: AvatarOutputFormat,
) -> ImageResult<Vec<u8>> {
encode_avatar_auto_with_identity_options(
spec,
AvatarIdentityOptions::new(namespace),
id,
format,
)
}
pub fn encode_avatar_auto_with_identity_options<T: AsRef<[u8]>>(
spec: AvatarSpec,
identity_options: AvatarIdentityOptions<'_>,
id: T,
format: AvatarOutputFormat,
) -> ImageResult<Vec<u8>> {
let image = render_avatar_auto_with_identity_options(spec, identity_options, id)
.map_err(avatar_render_error_to_image_error)?;
encode_owned_rgba_image(image, format)
}
#[derive(Clone, Debug)]
struct AvatarRenderPlan {
spec: AvatarSpec,
identity: AvatarIdentity,
style: AvatarStyleOptions,
}
impl AvatarRenderPlan {
fn new<T: AsRef<[u8]>>(
spec: AvatarSpec,
identity_options: AvatarIdentityOptions<'_>,
id: T,
options: AvatarOptions,
) -> Result<Self, AvatarRenderError> {
Self::new_with_style(
spec,
identity_options,
id,
AvatarStyleOptions::from(options),
)
}
fn new_with_style<T: AsRef<[u8]>>(
spec: AvatarSpec,
identity_options: AvatarIdentityOptions<'_>,
id: T,
style: AvatarStyleOptions,
) -> Result<Self, AvatarRenderError> {
spec.validate()?;
let identity = AvatarIdentity::new_with_options(identity_options, id)?;
Ok(Self {
spec,
identity,
style,
})
}
fn new_auto<T: AsRef<[u8]>>(
spec: AvatarSpec,
identity_options: AvatarIdentityOptions<'_>,
id: T,
) -> Result<Self, AvatarRenderError> {
spec.validate()?;
let identity = AvatarIdentity::new_with_options(identity_options, id)?;
let style = AvatarStyleOptions::from_identity(&identity);
Ok(Self {
spec,
identity,
style,
})
}
fn render_rgba(&self) -> Result<RgbaImage, AvatarSpecError> {
let mut image = match self.style.kind {
AvatarKind::Cat => render_cat_avatar_for_identity_with_background(
self.spec,
&self.identity,
self.style.background,
),
AvatarKind::Dog => {
render_dog_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Robot => {
render_robot_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Fox => {
render_fox_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Alien => {
render_alien_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Monster => {
render_monster_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Ghost => {
render_ghost_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Slime => {
render_slime_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Bird => {
render_bird_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Wizard => {
render_wizard_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Skull => {
render_skull_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Paws => {
render_paws_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Planet => {
render_planet_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Rocket => {
render_rocket_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Mushroom => render_mushroom_avatar_for_identity(
self.spec,
&self.identity,
self.style.background,
),
AvatarKind::Cactus => {
render_cactus_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Frog => {
render_frog_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Panda => {
render_panda_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Cupcake => {
render_cupcake_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Pizza => {
render_pizza_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Icecream => render_icecream_avatar_for_identity(
self.spec,
&self.identity,
self.style.background,
),
AvatarKind::Octopus => {
render_octopus_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Knight => {
render_knight_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Bear => {
render_bear_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Penguin => {
render_penguin_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Dragon => {
render_dragon_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Ninja => {
render_ninja_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::Astronaut => render_astronaut_avatar_for_identity(
self.spec,
&self.identity,
self.style.background,
),
AvatarKind::Diamond => {
render_diamond_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
AvatarKind::CoffeeCup => render_coffee_cup_avatar_for_identity(
self.spec,
&self.identity,
self.style.background,
),
AvatarKind::Shield => {
render_shield_avatar_for_identity(self.spec, &self.identity, self.style.background)
}
}?;
apply_style_layers(&mut image, self.spec, self.style, &self.identity);
Ok(image)
}
fn svg_background_color(&self) -> Color {
match self.style.background {
AvatarBackground::Themed => match self.style.kind {
AvatarKind::Cat => {
hsl_to_color(28.0 + self.identity.unit_f32(2) * 40.0, 0.25, 0.92)
}
AvatarKind::Dog => {
hsl_to_color(200.0 + self.identity.unit_f32(3) * 60.0, 0.20, 0.92)
}
AvatarKind::Robot => {
hsl_to_color(220.0 + self.identity.unit_f32(4) * 50.0, 0.18, 0.93)
}
AvatarKind::Fox => {
hsl_to_color(18.0 + self.identity.unit_f32(5) * 30.0, 0.28, 0.93)
}
AvatarKind::Alien => {
hsl_to_color(260.0 + self.identity.unit_f32(6) * 60.0, 0.20, 0.93)
}
AvatarKind::Monster => {
hsl_to_color(300.0 + self.identity.unit_f32(7) * 45.0, 0.24, 0.92)
}
AvatarKind::Ghost => {
hsl_to_color(220.0 + self.identity.unit_f32(8) * 35.0, 0.18, 0.95)
}
AvatarKind::Slime => {
hsl_to_color(120.0 + self.identity.unit_f32(9) * 70.0, 0.24, 0.92)
}
AvatarKind::Bird => {
hsl_to_color(180.0 + self.identity.unit_f32(10) * 40.0, 0.22, 0.93)
}
AvatarKind::Wizard => {
hsl_to_color(250.0 + self.identity.unit_f32(11) * 40.0, 0.24, 0.92)
}
AvatarKind::Skull => {
hsl_to_color(210.0 + self.identity.unit_f32(12) * 20.0, 0.08, 0.94)
}
AvatarKind::Paws => {
hsl_to_color(28.0 + self.identity.unit_f32(13) * 30.0, 0.22, 0.94)
}
AvatarKind::Planet => {
hsl_to_color(215.0 + self.identity.unit_f32(14) * 90.0, 0.24, 0.91)
}
AvatarKind::Rocket => {
hsl_to_color(205.0 + self.identity.unit_f32(15) * 70.0, 0.22, 0.92)
}
AvatarKind::Mushroom => {
hsl_to_color(18.0 + self.identity.unit_f32(16) * 35.0, 0.20, 0.93)
}
AvatarKind::Cactus => {
hsl_to_color(80.0 + self.identity.unit_f32(17) * 55.0, 0.20, 0.92)
}
AvatarKind::Frog => {
hsl_to_color(95.0 + self.identity.unit_f32(18) * 65.0, 0.23, 0.92)
}
AvatarKind::Panda => {
hsl_to_color(200.0 + self.identity.unit_f32(19) * 45.0, 0.08, 0.94)
}
AvatarKind::Cupcake => {
hsl_to_color(320.0 + self.identity.unit_f32(20) * 45.0, 0.22, 0.94)
}
AvatarKind::Pizza => {
hsl_to_color(36.0 + self.identity.unit_f32(21) * 30.0, 0.24, 0.93)
}
AvatarKind::Icecream => {
hsl_to_color(190.0 + self.identity.unit_f32(22) * 95.0, 0.18, 0.94)
}
AvatarKind::Octopus => {
hsl_to_color(185.0 + self.identity.unit_f32(23) * 70.0, 0.22, 0.92)
}
AvatarKind::Knight => {
hsl_to_color(215.0 + self.identity.unit_f32(24) * 30.0, 0.12, 0.92)
}
AvatarKind::Bear => {
hsl_to_color(30.0 + self.identity.unit_f32(25) * 28.0, 0.20, 0.92)
}
AvatarKind::Penguin => {
hsl_to_color(200.0 + self.identity.unit_f32(26) * 35.0, 0.18, 0.93)
}
AvatarKind::Dragon => {
hsl_to_color(105.0 + self.identity.unit_f32(27) * 45.0, 0.22, 0.91)
}
AvatarKind::Ninja => {
hsl_to_color(225.0 + self.identity.unit_f32(28) * 35.0, 0.12, 0.91)
}
AvatarKind::Astronaut => {
hsl_to_color(215.0 + self.identity.unit_f32(29) * 60.0, 0.16, 0.92)
}
AvatarKind::Diamond => {
hsl_to_color(185.0 + self.identity.unit_f32(30) * 50.0, 0.20, 0.93)
}
AvatarKind::CoffeeCup => {
hsl_to_color(32.0 + self.identity.unit_f32(31) * 28.0, 0.18, 0.93)
}
AvatarKind::Shield => {
hsl_to_color(215.0 + self.identity.unit_f32(32) * 40.0, 0.15, 0.92)
}
},
AvatarBackground::White => Color::rgb(255, 255, 255),
AvatarBackground::Black => Color::rgb(0, 0, 0),
AvatarBackground::Dark => Color::rgb(17, 24, 39),
AvatarBackground::Light => Color::rgb(248, 250, 247),
AvatarBackground::Transparent => Color::rgba(255, 255, 255, 0),
AvatarBackground::PolkaDot
| AvatarBackground::Striped
| AvatarBackground::Checkerboard
| AvatarBackground::Grid => Color::rgb(248, 250, 247),
AvatarBackground::Sunrise => Color::rgb(255, 247, 212),
AvatarBackground::Ocean => Color::rgb(220, 248, 252),
AvatarBackground::Starry => Color::rgb(17, 24, 39),
}
}
fn render_svg_body(&self) -> String {
match self.style.kind {
AvatarKind::Cat => render_cat_svg(self.spec, &self.identity),
AvatarKind::Dog => render_dog_svg(self.spec, &self.identity),
AvatarKind::Robot => render_robot_svg(self.spec, &self.identity),
AvatarKind::Fox => render_fox_svg(self.spec, &self.identity),
AvatarKind::Alien => render_alien_svg(self.spec, &self.identity),
AvatarKind::Monster => render_monster_svg(self.spec, &self.identity),
AvatarKind::Ghost => render_ghost_svg(self.spec, &self.identity),
AvatarKind::Slime => render_slime_svg(self.spec, &self.identity),
AvatarKind::Bird => render_bird_svg(self.spec, &self.identity),
AvatarKind::Wizard => render_wizard_svg(self.spec, &self.identity),
AvatarKind::Skull => render_skull_svg(self.spec, &self.identity),
AvatarKind::Paws => render_paws_svg(self.spec, &self.identity),
AvatarKind::Planet => render_planet_svg(self.spec, &self.identity),
AvatarKind::Rocket => render_rocket_svg(self.spec, &self.identity),
AvatarKind::Mushroom => render_mushroom_svg(self.spec, &self.identity),
AvatarKind::Cactus => render_cactus_svg(self.spec, &self.identity),
AvatarKind::Frog => render_frog_svg(self.spec, &self.identity),
AvatarKind::Panda => render_panda_svg(self.spec, &self.identity),
AvatarKind::Cupcake => render_cupcake_svg(self.spec, &self.identity),
AvatarKind::Pizza => render_pizza_svg(self.spec, &self.identity),
AvatarKind::Icecream => render_icecream_svg(self.spec, &self.identity),
AvatarKind::Octopus => render_octopus_svg(self.spec, &self.identity),
AvatarKind::Knight => render_knight_svg(self.spec, &self.identity),
AvatarKind::Bear => render_bear_svg(self.spec, &self.identity),
AvatarKind::Penguin => render_penguin_svg(self.spec, &self.identity),
AvatarKind::Dragon => render_dragon_svg(self.spec, &self.identity),
AvatarKind::Ninja => render_ninja_svg(self.spec, &self.identity),
AvatarKind::Astronaut => render_astronaut_svg(self.spec, &self.identity),
AvatarKind::Diamond => render_diamond_svg(self.spec, &self.identity),
AvatarKind::CoffeeCup => render_coffee_cup_svg(self.spec, &self.identity),
AvatarKind::Shield => render_shield_svg(self.spec, &self.identity),
}
}
fn render_svg(&self) -> String {
let background = self.render_svg_background();
let content = format!(
"{}{}{}",
background,
self.render_svg_body(),
render_style_svg_layers(self.spec, self.style, &self.identity)
);
let (clip_defs, clipped_content) =
render_shape_svg_clip(self.spec, self.style.shape, &content);
let shape_layer = render_shape_svg_layer(
self.spec,
self.style.shape,
style_accent_color(self.style.color, &self.identity),
);
format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" width="{w}" height="{h}" role="img" aria-label="{label} avatar">{clip_defs}{body}{shape_layer}</svg>"#,
w = self.spec.width,
h = self.spec.height,
label = self.style.kind.as_str(),
clip_defs = clip_defs,
body = clipped_content,
shape_layer = shape_layer,
)
}
fn render_svg_background(&self) -> String {
match self.style.background {
AvatarBackground::Transparent => String::new(),
AvatarBackground::Themed
| AvatarBackground::White
| AvatarBackground::Black
| AvatarBackground::Dark
| AvatarBackground::Light => {
format!(
r#"<rect width="100%" height="100%" fill="{}"/>"#,
color_hex(self.svg_background_color())
)
}
AvatarBackground::PolkaDot => {
r##"<defs><pattern id="hashavatar-bg-polka-dot" width="16" height="16" patternUnits="userSpaceOnUse"><rect width="16" height="16" fill="#f8faf7"/><circle cx="8" cy="8" r="2" fill="#d1d5db"/></pattern></defs><rect width="100%" height="100%" fill="url(#hashavatar-bg-polka-dot)"/>"##.to_owned()
}
AvatarBackground::Striped => {
r##"<defs><pattern id="hashavatar-bg-striped" width="18" height="18" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="18" height="18" fill="#f8faf7"/><rect width="9" height="18" fill="#e5e7eb"/></pattern></defs><rect width="100%" height="100%" fill="url(#hashavatar-bg-striped)"/>"##.to_owned()
}
AvatarBackground::Checkerboard => {
r##"<defs><pattern id="hashavatar-bg-checkerboard" width="24" height="24" patternUnits="userSpaceOnUse"><rect width="24" height="24" fill="#f8faf7"/><rect width="12" height="12" fill="#e8ece7"/><rect x="12" y="12" width="12" height="12" fill="#e8ece7"/></pattern></defs><rect width="100%" height="100%" fill="url(#hashavatar-bg-checkerboard)"/>"##.to_owned()
}
AvatarBackground::Grid => {
r##"<defs><pattern id="hashavatar-bg-grid" width="16" height="16" patternUnits="userSpaceOnUse"><rect width="16" height="16" fill="#f8faf7"/><path d="M 16 0 L 0 0 0 16" fill="none" stroke="#dde2dd" stroke-width="1"/></pattern></defs><rect width="100%" height="100%" fill="url(#hashavatar-bg-grid)"/>"##.to_owned()
}
AvatarBackground::Sunrise => {
r##"<defs><linearGradient id="hashavatar-bg-sunrise" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#fff7d4"/><stop offset="100%" stop-color="#ffb86b"/></linearGradient></defs><rect width="100%" height="100%" fill="url(#hashavatar-bg-sunrise)"/>"##.to_owned()
}
AvatarBackground::Ocean => {
r##"<defs><linearGradient id="hashavatar-bg-ocean" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#dcf8fc"/><stop offset="100%" stop-color="#4b91be"/></linearGradient></defs><rect width="100%" height="100%" fill="url(#hashavatar-bg-ocean)"/>"##.to_owned()
}
AvatarBackground::Starry => {
r##"<defs><pattern id="hashavatar-bg-starry" width="40" height="40" patternUnits="userSpaceOnUse"><rect width="40" height="40" fill="#111827"/><circle cx="8" cy="9" r="1.2" fill="#ffffff" opacity="0.7"/><circle cx="28" cy="14" r="1" fill="#ffffff" opacity="0.55"/><circle cx="18" cy="31" r="1.4" fill="#ffffff" opacity="0.65"/></pattern></defs><rect width="100%" height="100%" fill="url(#hashavatar-bg-starry)"/>"##.to_owned()
}
}
}
}
/// Render an avatar image directly without encoding it.
pub fn render_avatar_for_id<T: AsRef<[u8]>>(
spec: AvatarSpec,
id: T,
options: AvatarOptions,
) -> Result<RgbaImage, AvatarRenderError> {
render_avatar_for_namespace(spec, AvatarNamespace::default(), id, options)
}
pub fn render_avatar_for_namespace<T: AsRef<[u8]>>(
spec: AvatarSpec,
namespace: AvatarNamespace<'_>,
id: T,
options: AvatarOptions,
) -> Result<RgbaImage, AvatarRenderError> {
render_avatar_with_identity_options(spec, AvatarIdentityOptions::new(namespace), id, options)
}
pub fn render_avatar_with_identity_options<T: AsRef<[u8]>>(
spec: AvatarSpec,
identity_options: AvatarIdentityOptions<'_>,
id: T,
options: AvatarOptions,
) -> Result<RgbaImage, AvatarRenderError> {
AvatarRenderPlan::new(spec, identity_options, id, options)?
.render_rgba()
.map_err(AvatarRenderError::from)
}
/// Render an avatar image with explicit visual layer style options.
pub fn render_avatar_style_for_id<T: AsRef<[u8]>>(
spec: AvatarSpec,
id: T,
style: AvatarStyleOptions,
) -> Result<RgbaImage, AvatarRenderError> {
render_avatar_style_for_namespace(spec, AvatarNamespace::default(), id, style)
}
pub fn render_avatar_style_for_namespace<T: AsRef<[u8]>>(
spec: AvatarSpec,
namespace: AvatarNamespace<'_>,
id: T,
style: AvatarStyleOptions,
) -> Result<RgbaImage, AvatarRenderError> {
render_avatar_style_with_identity_options(
spec,
AvatarIdentityOptions::new(namespace),
id,
style,
)
}
pub fn render_avatar_style_with_identity_options<T: AsRef<[u8]>>(
spec: AvatarSpec,
identity_options: AvatarIdentityOptions<'_>,
id: T,
style: AvatarStyleOptions,
) -> Result<RgbaImage, AvatarRenderError> {
AvatarRenderPlan::new_with_style(spec, identity_options, id, style)?
.render_rgba()
.map_err(AvatarRenderError::from)
}
/// Render an avatar image with all public style choices derived from distinct
/// identity digest bytes.
pub fn render_avatar_auto_for_id<T: AsRef<[u8]>>(
spec: AvatarSpec,
id: T,
) -> Result<RgbaImage, AvatarRenderError> {
render_avatar_auto_for_namespace(spec, AvatarNamespace::default(), id)
}
pub fn render_avatar_auto_for_namespace<T: AsRef<[u8]>>(
spec: AvatarSpec,
namespace: AvatarNamespace<'_>,
id: T,
) -> Result<RgbaImage, AvatarRenderError> {
render_avatar_auto_with_identity_options(spec, AvatarIdentityOptions::new(namespace), id)
}
pub fn render_avatar_auto_with_identity_options<T: AsRef<[u8]>>(
spec: AvatarSpec,
identity_options: AvatarIdentityOptions<'_>,
id: T,
) -> Result<RgbaImage, AvatarRenderError> {
AvatarRenderPlan::new_auto(spec, identity_options, id)?
.render_rgba()
.map_err(AvatarRenderError::from)
}
/// Render an avatar as a compact SVG string.
pub fn render_avatar_svg_for_id<T: AsRef<[u8]>>(
spec: AvatarSpec,
id: T,
options: AvatarOptions,
) -> Result<String, AvatarRenderError> {
render_avatar_svg_for_namespace(spec, AvatarNamespace::default(), id, options)
}
pub fn render_avatar_svg_for_namespace<T: AsRef<[u8]>>(
spec: AvatarSpec,
namespace: AvatarNamespace<'_>,
id: T,
options: AvatarOptions,
) -> Result<String, AvatarRenderError> {
render_avatar_svg_with_identity_options(
spec,
AvatarIdentityOptions::new(namespace),
id,
options,
)
}
pub fn render_avatar_svg_with_identity_options<T: AsRef<[u8]>>(
spec: AvatarSpec,
identity_options: AvatarIdentityOptions<'_>,
id: T,
options: AvatarOptions,
) -> Result<String, AvatarRenderError> {
Ok(AvatarRenderPlan::new(spec, identity_options, id, options)?.render_svg())
}
/// Render an avatar with explicit visual layer style options as a compact SVG string.
pub fn render_avatar_svg_style_for_id<T: AsRef<[u8]>>(
spec: AvatarSpec,
id: T,
style: AvatarStyleOptions,
) -> Result<String, AvatarRenderError> {
render_avatar_svg_style_for_namespace(spec, AvatarNamespace::default(), id, style)
}
pub fn render_avatar_svg_style_for_namespace<T: AsRef<[u8]>>(
spec: AvatarSpec,
namespace: AvatarNamespace<'_>,
id: T,
style: AvatarStyleOptions,
) -> Result<String, AvatarRenderError> {
render_avatar_svg_style_with_identity_options(
spec,
AvatarIdentityOptions::new(namespace),
id,
style,
)
}
pub fn render_avatar_svg_style_with_identity_options<T: AsRef<[u8]>>(
spec: AvatarSpec,
identity_options: AvatarIdentityOptions<'_>,
id: T,
style: AvatarStyleOptions,
) -> Result<String, AvatarRenderError> {
Ok(AvatarRenderPlan::new_with_style(spec, identity_options, id, style)?.render_svg())
}
/// Render an avatar SVG with all public style choices derived from distinct
/// identity digest bytes.
pub fn render_avatar_svg_auto_for_id<T: AsRef<[u8]>>(
spec: AvatarSpec,
id: T,
) -> Result<String, AvatarRenderError> {
render_avatar_svg_auto_for_namespace(spec, AvatarNamespace::default(), id)
}
pub fn render_avatar_svg_auto_for_namespace<T: AsRef<[u8]>>(
spec: AvatarSpec,
namespace: AvatarNamespace<'_>,
id: T,
) -> Result<String, AvatarRenderError> {
render_avatar_svg_auto_with_identity_options(spec, AvatarIdentityOptions::new(namespace), id)
}
pub fn render_avatar_svg_auto_with_identity_options<T: AsRef<[u8]>>(
spec: AvatarSpec,
identity_options: AvatarIdentityOptions<'_>,
id: T,
) -> Result<String, AvatarRenderError> {
Ok(AvatarRenderPlan::new_auto(spec, identity_options, id)?.render_svg())
}
fn apply_style_layers(
image: &mut RgbaImage,
spec: AvatarSpec,
style: AvatarStyleOptions,
identity: &AvatarIdentity,
) {
if !style.has_extra_layers() {
return;
}
let accent = style_accent_color(style.color, identity);
if style.color != AvatarColor::Default {
draw_style_color_layer(image, spec, accent);
}
draw_expression_layer(image, spec, style.kind, style.expression, accent);
draw_accessory_layer(image, spec, style.kind, style.accessory, accent);
apply_shape_layer(image, spec, style.shape, accent);
}
#[derive(Clone, Copy, Debug)]
struct AvatarLayerAnchors {
left_eye: (f32, f32),
right_eye: (f32, f32),
mouth: (f32, f32),
top: f32,
neck: f32,
face_width: f32,
eye_radius: f32,
}
impl AvatarLayerAnchors {
fn point(self, spec: AvatarSpec, point: (f32, f32)) -> (i32, i32) {
(
(point.0 * spec.width as f32).round() as i32,
(point.1 * spec.height as f32).round() as i32,
)
}
fn y(self, spec: AvatarSpec, value: f32) -> i32 {
(value * spec.height as f32).round() as i32
}
fn span(self, spec: AvatarSpec, value: f32) -> i32 {
(value * spec.width.min(spec.height) as f32).round() as i32
}
}
fn avatar_layer_anchors(kind: AvatarKind) -> Option<AvatarLayerAnchors> {
let anchors = match kind {
AvatarKind::Cat => AvatarLayerAnchors {
left_eye: (0.40, 0.50),
right_eye: (0.60, 0.50),
mouth: (0.50, 0.62),
top: 0.28,
neck: 0.72,
face_width: 0.58,
eye_radius: 0.055,
},
AvatarKind::Dog => AvatarLayerAnchors {
left_eye: (0.40, 0.46),
right_eye: (0.60, 0.46),
mouth: (0.50, 0.61),
top: 0.24,
neck: 0.72,
face_width: 0.62,
eye_radius: 0.055,
},
AvatarKind::Robot => AvatarLayerAnchors {
left_eye: (0.38, 0.45),
right_eye: (0.62, 0.45),
mouth: (0.50, 0.60),
top: 0.25,
neck: 0.72,
face_width: 0.54,
eye_radius: 0.060,
},
AvatarKind::Fox => AvatarLayerAnchors {
left_eye: (0.40, 0.48),
right_eye: (0.60, 0.48),
mouth: (0.50, 0.62),
top: 0.25,
neck: 0.72,
face_width: 0.60,
eye_radius: 0.050,
},
AvatarKind::Alien => AvatarLayerAnchors {
left_eye: (0.39, 0.48),
right_eye: (0.61, 0.48),
mouth: (0.50, 0.63),
top: 0.25,
neck: 0.72,
face_width: 0.58,
eye_radius: 0.070,
},
AvatarKind::Monster => AvatarLayerAnchors {
left_eye: (0.45, 0.52),
right_eye: (0.55, 0.52),
mouth: (0.50, 0.66),
top: 0.27,
neck: 0.74,
face_width: 0.62,
eye_radius: 0.045,
},
AvatarKind::Ghost => AvatarLayerAnchors {
left_eye: (0.44, 0.50),
right_eye: (0.56, 0.50),
mouth: (0.50, 0.61),
top: 0.27,
neck: 0.72,
face_width: 0.54,
eye_radius: 0.045,
},
AvatarKind::Slime => AvatarLayerAnchors {
left_eye: (0.40, 0.48),
right_eye: (0.60, 0.48),
mouth: (0.50, 0.61),
top: 0.32,
neck: 0.73,
face_width: 0.58,
eye_radius: 0.055,
},
AvatarKind::Bird => AvatarLayerAnchors {
left_eye: (0.445, 0.51),
right_eye: (0.555, 0.51),
mouth: (0.50, 0.61),
top: 0.31,
neck: 0.70,
face_width: 0.50,
eye_radius: 0.035,
},
AvatarKind::Wizard => AvatarLayerAnchors {
left_eye: (0.455, 0.55),
right_eye: (0.545, 0.55),
mouth: (0.50, 0.67),
top: 0.36,
neck: 0.73,
face_width: 0.44,
eye_radius: 0.040,
},
AvatarKind::Skull => AvatarLayerAnchors {
left_eye: (0.40, 0.45),
right_eye: (0.60, 0.45),
mouth: (0.50, 0.63),
top: 0.24,
neck: 0.72,
face_width: 0.55,
eye_radius: 0.065,
},
AvatarKind::Frog => AvatarLayerAnchors {
left_eye: (0.37, 0.39),
right_eye: (0.63, 0.39),
mouth: (0.50, 0.62),
top: 0.25,
neck: 0.72,
face_width: 0.64,
eye_radius: 0.070,
},
AvatarKind::Panda => AvatarLayerAnchors {
left_eye: (0.41, 0.53),
right_eye: (0.59, 0.53),
mouth: (0.50, 0.62),
top: 0.28,
neck: 0.72,
face_width: 0.62,
eye_radius: 0.040,
},
AvatarKind::Octopus => AvatarLayerAnchors {
left_eye: (0.42, 0.52),
right_eye: (0.58, 0.52),
mouth: (0.50, 0.65),
top: 0.28,
neck: 0.70,
face_width: 0.58,
eye_radius: 0.045,
},
AvatarKind::Knight => AvatarLayerAnchors {
left_eye: (0.40, 0.45),
right_eye: (0.60, 0.45),
mouth: (0.50, 0.60),
top: 0.22,
neck: 0.72,
face_width: 0.58,
eye_radius: 0.050,
},
AvatarKind::Bear => AvatarLayerAnchors {
left_eye: (0.40, 0.48),
right_eye: (0.60, 0.48),
mouth: (0.50, 0.63),
top: 0.25,
neck: 0.73,
face_width: 0.62,
eye_radius: 0.055,
},
AvatarKind::Penguin => AvatarLayerAnchors {
left_eye: (0.40, 0.45),
right_eye: (0.60, 0.45),
mouth: (0.50, 0.57),
top: 0.20,
neck: 0.74,
face_width: 0.58,
eye_radius: 0.045,
},
AvatarKind::Dragon => AvatarLayerAnchors {
left_eye: (0.40, 0.48),
right_eye: (0.60, 0.48),
mouth: (0.50, 0.63),
top: 0.23,
neck: 0.73,
face_width: 0.62,
eye_radius: 0.052,
},
AvatarKind::Ninja => AvatarLayerAnchors {
left_eye: (0.40, 0.48),
right_eye: (0.60, 0.48),
mouth: (0.50, 0.64),
top: 0.24,
neck: 0.73,
face_width: 0.58,
eye_radius: 0.045,
},
AvatarKind::Astronaut => AvatarLayerAnchors {
left_eye: (0.40, 0.47),
right_eye: (0.60, 0.47),
mouth: (0.50, 0.62),
top: 0.19,
neck: 0.76,
face_width: 0.60,
eye_radius: 0.045,
},
AvatarKind::Paws
| AvatarKind::Planet
| AvatarKind::Rocket
| AvatarKind::Mushroom
| AvatarKind::Cactus
| AvatarKind::Cupcake
| AvatarKind::Pizza
| AvatarKind::Icecream
| AvatarKind::Diamond
| AvatarKind::CoffeeCup
| AvatarKind::Shield => return None,
};
Some(anchors)
}
fn glasses_y_offset(kind: AvatarKind) -> f32 {
match kind {
AvatarKind::Dog | AvatarKind::Robot | AvatarKind::Ghost => 0.025,
AvatarKind::Monster => 0.020,
AvatarKind::Wizard | AvatarKind::Knight => 0.035,
_ => 0.0,
}
}
fn hat_y_offset(kind: AvatarKind) -> f32 {
match kind {
AvatarKind::Cat | AvatarKind::Frog => -0.035,
_ => 0.0,
}
}
fn crown_y_offset(kind: AvatarKind) -> f32 {
match kind {
AvatarKind::Cat => -0.035,
AvatarKind::Alien | AvatarKind::Frog => -0.080,
_ => 0.0,
}
}
fn bowtie_y_offset(kind: AvatarKind) -> f32 {
match kind {
AvatarKind::Cat
| AvatarKind::Fox
| AvatarKind::Slime
| AvatarKind::Wizard
| AvatarKind::Octopus => 0.060,
_ => 0.0,
}
}
fn eyepatch_y_offset(kind: AvatarKind) -> f32 {
match kind {
AvatarKind::Knight => 0.030,
_ => 0.0,
}
}
fn horns_y_offset(kind: AvatarKind) -> f32 {
match kind {
AvatarKind::Dog | AvatarKind::Robot => 0.070,
_ => 0.0,
}
}
fn style_accent_color(color: AvatarColor, identity: &AvatarIdentity) -> Color {
match color {
AvatarColor::Default => hsl_to_color(180.0 + identity.unit_f32(25) * 160.0, 0.58, 0.48),
AvatarColor::NeonMint => Color::rgb(27, 235, 179),
AvatarColor::PastelPink => Color::rgb(246, 160, 196),
AvatarColor::Crimson => Color::rgb(190, 18, 60),
AvatarColor::Gold => Color::rgb(234, 179, 8),
AvatarColor::DeepSeaBlue => Color::rgb(14, 116, 144),
}
}
fn rgba_with_alpha(color: Color, alpha: u8) -> Rgba<u8> {
Rgba([color.0[0], color.0[1], color.0[2], alpha])
}
fn blend_pixel(image: &mut RgbaImage, x: i32, y: i32, source: Rgba<u8>) {
if !in_bounds(image, x, y) {
return;
}
let destination = *image.get_pixel(x as u32, y as u32);
let alpha = u32::from(source.0[3]);
let inverse = 255 - alpha;
let blended = Rgba([
((u32::from(source.0[0]) * alpha + u32::from(destination.0[0]) * inverse + 127) / 255)
as u8,
((u32::from(source.0[1]) * alpha + u32::from(destination.0[1]) * inverse + 127) / 255)
as u8,
((u32::from(source.0[2]) * alpha + u32::from(destination.0[2]) * inverse + 127) / 255)
as u8,
source.0[3].saturating_add(((u32::from(destination.0[3]) * inverse + 127) / 255) as u8),
]);
image.put_pixel(x as u32, y as u32, blended);
}
fn draw_blended_rect_mut(image: &mut RgbaImage, rect: Rect, color: Rgba<u8>) {
if image.width() == 0 || image.height() == 0 {
return;
}
let bounds = Rect::at(0, 0).of_size(image.width(), image.height());
if let Some(intersection) = bounds.intersect(rect) {
for dy in 0..intersection.height() {
for dx in 0..intersection.width() {
let x = intersection.left() + dx as i32;
let y = intersection.top() + dy as i32;
blend_pixel(image, x, y, color);
}
}
}
}
fn draw_style_color_layer(image: &mut RgbaImage, spec: AvatarSpec, accent: Color) {
let width = spec.width as i32;
let height = spec.height as i32;
let bar_height = ((height as f32 * 0.08) as u32).max(3);
draw_blended_rect_mut(
image,
Rect::at(0, height - bar_height as i32).of_size(spec.width, bar_height),
rgba_with_alpha(accent, 210),
);
let stripe = ((spec.width.min(spec.height) as f32 * 0.03) as u32).max(2);
draw_blended_rect_mut(
image,
Rect::at(0, 0).of_size(stripe, spec.height),
rgba_with_alpha(accent, 145),
);
draw_blended_rect_mut(
image,
Rect::at(width - stripe as i32, 0).of_size(stripe, spec.height),
rgba_with_alpha(accent, 145),
);
}
fn draw_accessory_layer(
image: &mut RgbaImage,
spec: AvatarSpec,
kind: AvatarKind,
accessory: AvatarAccessory,
accent: Color,
) {
if accessory == AvatarAccessory::None {
return;
}
let Some(anchors) = avatar_layer_anchors(kind) else {
return;
};
let min = spec.width.min(spec.height) as i32;
let (left_eye_x, left_eye_y) = anchors.point(spec, anchors.left_eye);
let (right_eye_x, right_eye_y) = anchors.point(spec, anchors.right_eye);
let (mouth_x, mouth_y) = anchors.point(spec, anchors.mouth);
let eye_y = (left_eye_y + right_eye_y) / 2;
let top_y = anchors.y(spec, anchors.top);
let neck_y = anchors.y(spec, anchors.neck);
let face_half = anchors.span(spec, anchors.face_width) / 2;
let eye_radius = anchors.span(spec, anchors.eye_radius).max(3);
let dark = Rgba([31, 41, 55, 255]);
let light = rgba_with_alpha(accent, 255);
match accessory {
AvatarAccessory::None => {}
AvatarAccessory::Glasses => {
let radius = (eye_radius * 2).max(5);
let glasses_offset = (glasses_y_offset(kind) * spec.height as f32).round() as i32;
let left_glasses_y = left_eye_y + glasses_offset;
let right_glasses_y = right_eye_y + glasses_offset;
let bridge_y = (left_glasses_y + right_glasses_y) / 2;
draw_hollow_circle_mut(image, (left_eye_x, left_glasses_y), radius, dark);
draw_hollow_circle_mut(image, (right_eye_x, right_glasses_y), radius, dark);
draw_line_segment_mut(
image,
((left_eye_x + radius) as f32, bridge_y as f32),
((right_eye_x - radius) as f32, bridge_y as f32),
dark,
);
}
AvatarAccessory::Hat => {
let brim_w = (face_half * 14 / 10).max(min * 20 / 100);
let brim_h = (min * 5 / 100).max(2);
let top_w = (brim_w * 7 / 10).max(min * 16 / 100);
let top_h = min * 18 / 100;
let y_offset = (hat_y_offset(kind) * spec.height as f32).round() as i32;
let y = (top_y - top_h / 2 + y_offset).max(0);
draw_filled_rect_mut(
image,
Rect::at(mouth_x - top_w / 2, y).of_size(top_w as u32, top_h as u32),
light,
);
draw_filled_rect_mut(
image,
Rect::at(mouth_x - brim_w / 2, y + top_h).of_size(brim_w as u32, brim_h as u32),
dark,
);
}
AvatarAccessory::Headphones => {
let side_r = (eye_radius * 2).max(5);
let left_x = mouth_x - face_half;
let right_x = mouth_x + face_half;
draw_top_hollow_ellipse_mut(
image,
(mouth_x, eye_y),
face_half,
(eye_y - top_y).max(min * 8 / 100),
dark,
);
draw_filled_circle_mut(image, (left_x, eye_y), side_r, light);
draw_filled_circle_mut(image, (right_x, eye_y), side_r, light);
draw_hollow_circle_mut(image, (left_x, eye_y), side_r, dark);
draw_hollow_circle_mut(image, (right_x, eye_y), side_r, dark);
}
AvatarAccessory::Crown => {
let y_offset = (crown_y_offset(kind) * spec.height as f32).round() as i32;
let base_y = (top_y + y_offset).max(0);
let points = [
Point::new(mouth_x - face_half * 7 / 10, base_y + min * 12 / 100),
Point::new(mouth_x - face_half * 45 / 100, base_y),
Point::new(mouth_x - face_half * 16 / 100, base_y + min * 10 / 100),
Point::new(mouth_x, (base_y - min * 4 / 100).max(0)),
Point::new(mouth_x + face_half * 16 / 100, base_y + min * 10 / 100),
Point::new(mouth_x + face_half * 45 / 100, base_y),
Point::new(mouth_x + face_half * 7 / 10, base_y + min * 12 / 100),
];
draw_polygon_mut(image, &points, light);
draw_line_segment_mut(
image,
(
(mouth_x - face_half * 7 / 10) as f32,
(base_y + min * 12 / 100) as f32,
),
(
(mouth_x + face_half * 7 / 10) as f32,
(base_y + min * 12 / 100) as f32,
),
dark,
);
}
AvatarAccessory::Bowtie => {
let y_offset = (bowtie_y_offset(kind) * spec.height as f32).round() as i32;
let y = neck_y + y_offset;
let size = min * 10 / 100;
let left = [
Point::new(mouth_x, y),
Point::new(mouth_x - size * 2, y - size),
Point::new(mouth_x - size * 2, y + size),
];
let right = [
Point::new(mouth_x, y),
Point::new(mouth_x + size * 2, y - size),
Point::new(mouth_x + size * 2, y + size),
];
draw_polygon_mut(image, &left, light);
draw_polygon_mut(image, &right, light);
draw_filled_circle_mut(image, (mouth_x, y), (size / 2).max(2), dark);
}
AvatarAccessory::Eyepatch => {
let patch_rx = (eye_radius * 2).max(5);
let patch_ry = (eye_radius * 3 / 2).max(4);
let y_offset = (eyepatch_y_offset(kind) * spec.height as f32).round() as i32;
let left_patch_y = left_eye_y + y_offset;
let strap_start_y = top_y + y_offset / 2;
let strap_end_y = mouth_y - eye_radius + y_offset;
draw_line_segment_mut(
image,
((mouth_x - face_half) as f32, strap_start_y as f32),
((mouth_x + face_half * 7 / 10) as f32, strap_end_y as f32),
dark,
);
draw_filled_ellipse_mut(image, (left_eye_x, left_patch_y), patch_rx, patch_ry, dark);
}
AvatarAccessory::Scarf => {
let scarf_h = (min * 8 / 100).max(4);
let y = neck_y;
draw_filled_rect_mut(
image,
Rect::at(mouth_x - face_half * 8 / 10, y)
.of_size((face_half * 16 / 10) as u32, scarf_h as u32),
light,
);
draw_filled_rect_mut(
image,
Rect::at(mouth_x + face_half / 5, y + scarf_h / 2)
.of_size((min * 9 / 100) as u32, (min * 20 / 100) as u32),
light,
);
}
AvatarAccessory::Halo => {
draw_hollow_ellipse_mut(
image,
(mouth_x, (top_y - min * 7 / 100).max(0)),
(face_half * 7 / 10).max(min * 10 / 100),
min * 7 / 100,
light,
);
}
AvatarAccessory::Horns => {
let y_offset = (horns_y_offset(kind) * spec.height as f32).round() as i32;
let y = top_y + min * 5 / 100 + y_offset;
let left = [
Point::new(mouth_x - face_half * 6 / 10, y + min * 7 / 100),
Point::new(mouth_x - face_half, (y - min * 12 / 100).max(0)),
Point::new(mouth_x - face_half * 3 / 10, y + min * 2 / 100),
];
let right = [
Point::new(mouth_x + face_half * 6 / 10, y + min * 7 / 100),
Point::new(mouth_x + face_half, (y - min * 12 / 100).max(0)),
Point::new(mouth_x + face_half * 3 / 10, y + min * 2 / 100),
];
draw_polygon_mut(image, &left, light);
draw_polygon_mut(image, &right, light);
}
}
}
fn draw_expression_layer(
image: &mut RgbaImage,
spec: AvatarSpec,
kind: AvatarKind,
expression: AvatarExpression,
accent: Color,
) {
if expression == AvatarExpression::Default {
return;
}
let Some(anchors) = avatar_layer_anchors(kind) else {
return;
};
let min = spec.width.min(spec.height) as i32;
let (left_eye_x, left_eye_y) = anchors.point(spec, anchors.left_eye);
let (right_eye_x, right_eye_y) = anchors.point(spec, anchors.right_eye);
let (mouth_x, mouth_y) = anchors.point(spec, anchors.mouth);
let eye_radius = anchors.span(spec, anchors.eye_radius).max(3);
let dark = Rgba([17, 24, 39, 255]);
let accent = rgba_with_alpha(accent, 255);
match expression {
AvatarExpression::Default => {}
AvatarExpression::Happy => {
draw_smile_curve(
image,
mouth_x,
mouth_y,
min * 14 / 100,
min * 8 / 100,
false,
dark,
);
}
AvatarExpression::Grumpy => {
draw_smile_curve(
image,
mouth_x,
mouth_y + min * 7 / 100,
min * 14 / 100,
min * 8 / 100,
true,
dark,
);
}
AvatarExpression::Surprised => {
draw_hollow_circle_mut(image, (mouth_x, mouth_y), (min * 7 / 100).max(3), dark);
}
AvatarExpression::Sleepy => {
draw_line_segment_mut(
image,
((left_eye_x - eye_radius) as f32, left_eye_y as f32),
((left_eye_x + eye_radius) as f32, left_eye_y as f32),
dark,
);
draw_line_segment_mut(
image,
((right_eye_x - eye_radius) as f32, right_eye_y as f32),
((right_eye_x + eye_radius) as f32, right_eye_y as f32),
dark,
);
}
AvatarExpression::Winking => {
draw_line_segment_mut(
image,
((left_eye_x - eye_radius) as f32, left_eye_y as f32),
((left_eye_x + eye_radius) as f32, left_eye_y as f32),
dark,
);
draw_filled_circle_mut(
image,
(right_eye_x, right_eye_y),
(eye_radius * 7 / 10).max(2),
dark,
);
}
AvatarExpression::Cool => {
let rect_h = (eye_radius * 2).max(4) as u32;
let left = left_eye_x.min(right_eye_x) - eye_radius * 2;
let width = (right_eye_x.max(left_eye_x) - left + eye_radius * 2).max(min * 24 / 100);
draw_filled_rect_mut(
image,
Rect::at(left, left_eye_y.min(right_eye_y) - eye_radius)
.of_size(width as u32, rect_h),
dark,
);
draw_blended_rect_mut(
image,
Rect::at(
left + eye_radius,
left_eye_y.min(right_eye_y) - eye_radius / 2,
)
.of_size((width / 3).max(2) as u32, (eye_radius / 2).max(2) as u32),
Rgba([255, 255, 255, 70]),
);
}
AvatarExpression::Crying => {
draw_smile_curve(
image,
mouth_x,
mouth_y + min * 7 / 100,
min * 12 / 100,
min * 7 / 100,
true,
dark,
);
draw_filled_ellipse_mut(
image,
(right_eye_x + eye_radius, right_eye_y + eye_radius * 2),
(eye_radius * 7 / 10).max(2),
(eye_radius * 3 / 2).max(4),
accent,
);
}
}
}
fn draw_smile_curve(
image: &mut RgbaImage,
cx: i32,
cy: i32,
width: i32,
height: i32,
inverted: bool,
color: Rgba<u8>,
) {
let mut previous = None;
for step in 0..=16 {
let t = step as f32 / 16.0;
let x = cx - width + (2.0 * width as f32 * t) as i32;
let curve = (1.0 - (2.0 * t - 1.0).powi(2)) * height as f32;
let y = if inverted {
cy - curve as i32
} else {
cy + curve as i32
};
if let Some((px, py)) = previous {
draw_antialiased_line_segment_mut(image, (px, py), (x, y), color, interpolate);
}
previous = Some((x, y));
}
}
fn draw_hollow_ellipse_mut(
image: &mut RgbaImage,
center: (i32, i32),
width_radius: i32,
height_radius: i32,
color: Rgba<u8>,
) {
let (cx, cy) = center;
let steps = (width_radius.max(height_radius) * 8).max(24);
let mut previous = None;
for step in 0..=steps {
let angle = step as f32 / steps as f32 * std::f32::consts::TAU;
let x = cx + (angle.cos() * width_radius as f32).round() as i32;
let y = cy + (angle.sin() * height_radius as f32).round() as i32;
if let Some((px, py)) = previous {
draw_antialiased_line_segment_mut(image, (px, py), (x, y), color, interpolate);
}
previous = Some((x, y));
}
}
fn draw_top_hollow_ellipse_mut(
image: &mut RgbaImage,
center: (i32, i32),
width_radius: i32,
height_radius: i32,
color: Rgba<u8>,
) {
let (cx, cy) = center;
let steps = (width_radius.max(height_radius) * 4).max(16);
let mut previous = None;
for step in 0..=steps {
let angle = std::f32::consts::PI + step as f32 / steps as f32 * std::f32::consts::PI;
let x = cx + (angle.cos() * width_radius as f32).round() as i32;
let y = cy + (angle.sin() * height_radius as f32).round() as i32;
if let Some((px, py)) = previous {
draw_antialiased_line_segment_mut(image, (px, py), (x, y), color, interpolate);
}
previous = Some((x, y));
}
}
fn apply_shape_layer(image: &mut RgbaImage, spec: AvatarSpec, shape: AvatarShape, accent: Color) {
if shape == AvatarShape::Square {
return;
}
for y in 0..spec.height {
for x in 0..spec.width {
if !point_inside_avatar_shape(x as i32, y as i32, spec, shape) {
image.put_pixel(x, y, Rgba([255, 255, 255, 0]));
}
}
}
let frame = rgba_with_alpha(accent, 255);
for y in 0..spec.height as i32 {
for x in 0..spec.width as i32 {
if point_inside_avatar_shape(x, y, spec, shape)
&& (!point_inside_avatar_shape(x - 1, y, spec, shape)
|| !point_inside_avatar_shape(x + 1, y, spec, shape)
|| !point_inside_avatar_shape(x, y - 1, spec, shape)
|| !point_inside_avatar_shape(x, y + 1, spec, shape))
{
image.put_pixel(x as u32, y as u32, frame);
}
}
}
}
fn point_inside_avatar_shape(x: i32, y: i32, spec: AvatarSpec, shape: AvatarShape) -> bool {
if x < 0 || y < 0 || x >= spec.width as i32 || y >= spec.height as i32 {
return false;
}
let w = i64::from(spec.width);
let h = i64::from(spec.height);
let x2 = i64::from(x) * 2 + 1;
let y2 = i64::from(y) * 2 + 1;
match shape {
AvatarShape::Square => true,
AvatarShape::Circle => {
let dx = x2 - w;
let dy = y2 - h;
dx * dx * h * h + dy * dy * w * w <= w * w * h * h
}
AvatarShape::Squircle => {
let r = (w.min(h) * 18 / 100).max(1);
let inner_left = r;
let inner_right = w - r;
let inner_top = r;
let inner_bottom = h - r;
if (x2 >= inner_left * 2 && x2 <= inner_right * 2)
|| (y2 >= inner_top * 2 && y2 <= inner_bottom * 2)
{
true
} else {
let cx = if x2 < inner_left * 2 {
inner_left
} else {
inner_right
};
let cy = if y2 < inner_top * 2 {
inner_top
} else {
inner_bottom
};
let dx = x2 - cx * 2;
let dy = y2 - cy * 2;
dx * dx + dy * dy <= (r * 2) * (r * 2)
}
}
AvatarShape::Hexagon => point_inside_percent_polygon(
x,
y,
spec,
&[(25, 0), (75, 0), (100, 50), (75, 100), (25, 100), (0, 50)],
),
AvatarShape::Octagon => point_inside_percent_polygon(
x,
y,
spec,
&[
(30, 0),
(70, 0),
(100, 30),
(100, 70),
(70, 100),
(30, 100),
(0, 70),
(0, 30),
],
),
}
}
fn point_inside_percent_polygon(x: i32, y: i32, spec: AvatarSpec, points: &[(i64, i64)]) -> bool {
let mut inside = false;
let width = i64::from(spec.width);
let height = i64::from(spec.height);
let px = (i64::from(x) * 2 + 1) * 50;
let py = (i64::from(y) * 2 + 1) * 50;
let Some(&last) = points.last() else {
return false;
};
let mut previous = last;
for ¤t in points {
let xi = current.0 * width;
let yi = current.1 * height;
let xj = previous.0 * width;
let yj = previous.1 * height;
let crosses = (yi > py) != (yj > py);
if crosses {
let lhs = (px - xi) * (yj - yi);
let rhs = (xj - xi) * (py - yi);
let is_left = if yj > yi { lhs < rhs } else { lhs > rhs };
if is_left {
inside = !inside;
}
}
previous = current;
}
inside
}
fn render_style_svg_layers(
spec: AvatarSpec,
style: AvatarStyleOptions,
identity: &AvatarIdentity,
) -> String {
if !style.has_extra_layers() {
return String::new();
}
let accent = style_accent_color(style.color, identity);
let mut svg = String::new();
if style.color != AvatarColor::Default {
svg.push_str(&render_color_svg_layer(spec, accent));
}
svg.push_str(&render_expression_svg_layer(
spec,
style.kind,
style.expression,
accent,
));
svg.push_str(&render_accessory_svg_layer(
spec,
style.kind,
style.accessory,
accent,
));
svg
}
fn render_color_svg_layer(spec: AvatarSpec, accent: Color) -> String {
let bar_height = spec.height as f32 * 0.08;
let stripe = spec.width.min(spec.height) as f32 * 0.03;
format!(
r#"<g data-layer="color"><rect x="0" y="{bar_y}" width="{w}" height="{bar_h}" fill="{fill}" opacity="0.82"/><rect x="0" y="0" width="{stripe}" height="{h}" fill="{fill}" opacity="0.56"/><rect x="{right}" y="0" width="{stripe}" height="{h}" fill="{fill}" opacity="0.56"/></g>"#,
bar_y = spec.height as f32 - bar_height,
w = spec.width,
h = spec.height,
bar_h = bar_height,
stripe = stripe,
right = spec.width as f32 - stripe,
fill = color_hex(accent),
)
}
fn render_accessory_svg_layer(
spec: AvatarSpec,
kind: AvatarKind,
accessory: AvatarAccessory,
accent: Color,
) -> String {
if accessory == AvatarAccessory::None {
return String::new();
}
let Some(anchors) = avatar_layer_anchors(kind) else {
return String::new();
};
let min = spec.width.min(spec.height) as f32;
let (left_eye_x, left_eye_y) = (
anchors.left_eye.0 * spec.width as f32,
anchors.left_eye.1 * spec.height as f32,
);
let (right_eye_x, right_eye_y) = (
anchors.right_eye.0 * spec.width as f32,
anchors.right_eye.1 * spec.height as f32,
);
let (mouth_x, mouth_y) = (
anchors.mouth.0 * spec.width as f32,
anchors.mouth.1 * spec.height as f32,
);
let eye_y = (left_eye_y + right_eye_y) / 2.0;
let top_y = anchors.top * spec.height as f32;
let neck_y = anchors.neck * spec.height as f32;
let face_half = anchors.face_width * min / 2.0;
let eye_radius = (anchors.eye_radius * min).max(3.0);
let glasses_offset = glasses_y_offset(kind) * spec.height as f32;
let fill = color_hex(accent);
let dark = "#1f2937";
let layer = accessory.as_str();
let body = match accessory {
AvatarAccessory::None => String::new(),
AvatarAccessory::Glasses => format!(
r#"<circle cx="{lx}" cy="{y}" r="{r}" fill="none" stroke="{dark}" stroke-width="3"/><circle cx="{rx}" cy="{y}" r="{r}" fill="none" stroke="{dark}" stroke-width="3"/><line x1="{l2}" y1="{y}" x2="{r2}" y2="{y}" stroke="{dark}" stroke-width="3"/>"#,
lx = left_eye_x,
rx = right_eye_x,
y = eye_y + glasses_offset,
r = eye_radius * 2.0,
l2 = left_eye_x + eye_radius * 2.0,
r2 = right_eye_x - eye_radius * 2.0,
),
AvatarAccessory::Hat => format!(
r#"<rect x="{x}" y="{y}" width="{tw}" height="{th}" fill="{fill}"/><rect x="{bx}" y="{by}" width="{bw}" height="{bh}" fill="{dark}"/>"#,
x = mouth_x - face_half * 0.35,
y = (top_y - min * 0.09 + hat_y_offset(kind) * spec.height as f32).max(0.0),
tw = face_half * 0.70,
th = min * 0.18,
bx = mouth_x - face_half * 0.70,
by = (top_y - min * 0.09 + hat_y_offset(kind) * spec.height as f32).max(0.0)
+ min * 0.18,
bw = face_half * 1.40,
bh = min * 0.05,
),
AvatarAccessory::Headphones => format!(
r#"<path d="M {l} {y} Q {cx} {top} {rr} {y}" fill="none" stroke="{dark}" stroke-width="4" stroke-linecap="round"/><circle cx="{l}" cy="{ear_y}" r="{er}" fill="{fill}" stroke="{dark}" stroke-width="2"/><circle cx="{rr}" cy="{ear_y}" r="{er}" fill="{fill}" stroke="{dark}" stroke-width="2"/>"#,
l = mouth_x - face_half,
rr = mouth_x + face_half,
cx = mouth_x,
y = eye_y,
top = top_y,
ear_y = eye_y,
er = eye_radius * 2.0,
),
AvatarAccessory::Crown => {
let crown_top_y = (top_y + crown_y_offset(kind) * spec.height as f32).max(0.0);
let p = format!(
"{},{} {},{} {},{} {},{} {},{} {},{} {},{}",
mouth_x - face_half * 0.70,
crown_top_y + min * 0.12,
mouth_x - face_half * 0.45,
crown_top_y,
mouth_x - face_half * 0.16,
crown_top_y + min * 0.10,
mouth_x,
(crown_top_y - min * 0.04).max(0.0),
mouth_x + face_half * 0.16,
crown_top_y + min * 0.10,
mouth_x + face_half * 0.45,
crown_top_y,
mouth_x + face_half * 0.70,
crown_top_y + min * 0.12
);
format!(
r#"<polygon points="{p}" fill="{fill}"/><line x1="{x1}" y1="{y}" x2="{x2}" y2="{y}" stroke="{dark}" stroke-width="3"/>"#,
x1 = mouth_x - face_half * 0.70,
x2 = mouth_x + face_half * 0.70,
y = crown_top_y + min * 0.12,
)
}
AvatarAccessory::Bowtie => {
let bowtie_y = neck_y + bowtie_y_offset(kind) * spec.height as f32;
let lp = format!(
"{},{} {},{} {},{}",
mouth_x,
bowtie_y,
mouth_x - min * 0.20,
bowtie_y - min * 0.10,
mouth_x - min * 0.20,
bowtie_y + min * 0.10
);
let rp = format!(
"{},{} {},{} {},{}",
mouth_x,
bowtie_y,
mouth_x + min * 0.20,
bowtie_y - min * 0.10,
mouth_x + min * 0.20,
bowtie_y + min * 0.10
);
format!(
r#"<polygon points="{lp}" fill="{fill}"/><polygon points="{rp}" fill="{fill}"/><circle cx="{cx}" cy="{y}" r="{r}" fill="{dark}"/>"#,
cx = mouth_x,
y = bowtie_y,
r = min * 0.05,
)
}
AvatarAccessory::Eyepatch => format!(
r#"<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="{dark}" stroke-width="3"/><ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="{dark}"/>"#,
x1 = mouth_x - face_half,
y1 = top_y + eyepatch_y_offset(kind) * spec.height as f32 * 0.50,
x2 = mouth_x + face_half * 0.70,
y2 = mouth_y - eye_radius + eyepatch_y_offset(kind) * spec.height as f32,
cx = left_eye_x,
cy = left_eye_y + eyepatch_y_offset(kind) * spec.height as f32,
rx = eye_radius * 2.0,
ry = eye_radius * 1.5,
),
AvatarAccessory::Scarf => format!(
r#"<rect x="{x}" y="{y}" width="{sw}" height="{sh}" fill="{fill}"/><rect x="{tx}" y="{ty}" width="{tw}" height="{th}" fill="{fill}"/>"#,
x = mouth_x - face_half * 0.80,
y = neck_y,
sw = face_half * 1.60,
sh = min * 0.08,
tx = mouth_x + face_half * 0.20,
ty = neck_y + min * 0.04,
tw = min * 0.09,
th = min * 0.20,
),
AvatarAccessory::Halo => format!(
r#"<ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="none" stroke="{fill}" stroke-width="3"/>"#,
cx = mouth_x,
cy = (top_y - min * 0.07).max(0.0),
rx = (face_half * 0.70).max(min * 0.10),
ry = min * 0.07,
),
AvatarAccessory::Horns => {
let horn_top_y = top_y + horns_y_offset(kind) * spec.height as f32;
let lp = format!(
"{},{} {},{} {},{}",
mouth_x - face_half * 0.60,
horn_top_y + min * 0.12,
mouth_x - face_half,
(horn_top_y - min * 0.12).max(0.0),
mouth_x - face_half * 0.30,
horn_top_y + min * 0.07
);
let rp = format!(
"{},{} {},{} {},{}",
mouth_x + face_half * 0.60,
horn_top_y + min * 0.12,
mouth_x + face_half,
(horn_top_y - min * 0.12).max(0.0),
mouth_x + face_half * 0.30,
horn_top_y + min * 0.07
);
format!(
r#"<polygon points="{lp}" fill="{fill}"/><polygon points="{rp}" fill="{fill}"/>"#
)
}
};
format!(r#"<g data-layer="accessory-{layer}">{body}</g>"#)
}
fn render_expression_svg_layer(
spec: AvatarSpec,
kind: AvatarKind,
expression: AvatarExpression,
accent: Color,
) -> String {
if expression == AvatarExpression::Default {
return String::new();
}
let Some(anchors) = avatar_layer_anchors(kind) else {
return String::new();
};
let min = spec.width.min(spec.height) as f32;
let left_eye_x = anchors.left_eye.0 * spec.width as f32;
let left_eye_y = anchors.left_eye.1 * spec.height as f32;
let right_eye_x = anchors.right_eye.0 * spec.width as f32;
let right_eye_y = anchors.right_eye.1 * spec.height as f32;
let mouth_x = anchors.mouth.0 * spec.width as f32;
let mouth_y = anchors.mouth.1 * spec.height as f32;
let eye_radius = (anchors.eye_radius * min).max(3.0);
let dark = "#111827";
let layer = expression.as_str();
let body = match expression {
AvatarExpression::Default => String::new(),
AvatarExpression::Happy => format!(
r#"<path d="M {x1} {y} Q {cx} {my} {x2} {y}" fill="none" stroke="{dark}" stroke-width="3" stroke-linecap="round"/>"#,
x1 = mouth_x - min * 0.14,
x2 = mouth_x + min * 0.14,
y = mouth_y,
cx = mouth_x,
my = mouth_y + min * 0.09,
),
AvatarExpression::Grumpy => format!(
r#"<path d="M {x1} {y} Q {cx} {my} {x2} {y}" fill="none" stroke="{dark}" stroke-width="3" stroke-linecap="round"/>"#,
x1 = mouth_x - min * 0.14,
x2 = mouth_x + min * 0.14,
y = mouth_y,
cx = mouth_x,
my = mouth_y - min * 0.09,
),
AvatarExpression::Surprised => format!(
r#"<circle cx="{cx}" cy="{y}" r="{r}" fill="none" stroke="{dark}" stroke-width="3"/>"#,
cx = mouth_x,
y = mouth_y,
r = min * 0.07,
),
AvatarExpression::Sleepy => format!(
r#"<line x1="{x1}" y1="{ey}" x2="{x2}" y2="{ey}" stroke="{dark}" stroke-width="3" stroke-linecap="round"/><line x1="{x3}" y1="{ey}" x2="{x4}" y2="{ey}" stroke="{dark}" stroke-width="3" stroke-linecap="round"/>"#,
x1 = left_eye_x - eye_radius,
x2 = left_eye_x + eye_radius,
x3 = right_eye_x - eye_radius,
x4 = right_eye_x + eye_radius,
ey = (left_eye_y + right_eye_y) / 2.0,
),
AvatarExpression::Winking => format!(
r#"<line x1="{x1}" y1="{ey}" x2="{x2}" y2="{ey}" stroke="{dark}" stroke-width="3" stroke-linecap="round"/><circle cx="{rx}" cy="{ey}" r="{r}" fill="{dark}"/>"#,
x1 = left_eye_x - eye_radius,
x2 = left_eye_x + eye_radius,
rx = right_eye_x,
ey = right_eye_y,
r = eye_radius * 0.70,
),
AvatarExpression::Cool => format!(
r#"<rect x="{x}" y="{ey}" width="{ww}" height="{hh}" fill="{dark}"/>"#,
x = left_eye_x.min(right_eye_x) - eye_radius * 2.0,
ey = left_eye_y.min(right_eye_y) - eye_radius,
ww = right_eye_x.max(left_eye_x) - left_eye_x.min(right_eye_x) + eye_radius * 4.0,
hh = eye_radius * 2.0,
),
AvatarExpression::Crying => format!(
r#"<path d="M {x1} {y} Q {cx} {my} {x2} {y}" fill="none" stroke="{dark}" stroke-width="3" stroke-linecap="round"/><ellipse cx="{tx}" cy="{ty}" rx="{rx}" ry="{ry}" fill="{fill}"/>"#,
x1 = mouth_x - min * 0.12,
x2 = mouth_x + min * 0.12,
y = mouth_y,
cx = mouth_x,
my = mouth_y - min * 0.08,
tx = right_eye_x + eye_radius,
ty = right_eye_y + eye_radius * 2.0,
rx = eye_radius * 0.70,
ry = eye_radius * 1.50,
fill = color_hex(accent),
),
};
format!(r#"<g data-layer="expression-{layer}">{body}</g>"#)
}
fn render_shape_svg_layer(spec: AvatarSpec, shape: AvatarShape, accent: Color) -> String {
if shape == AvatarShape::Square {
return String::new();
}
let w = spec.width as f32;
let h = spec.height as f32;
let fill = color_hex(accent);
let body = match shape {
AvatarShape::Square => String::new(),
AvatarShape::Circle => format!(
r#"<ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="none" stroke="{fill}" stroke-width="3"/>"#,
cx = w / 2.0,
cy = h / 2.0,
rx = w / 2.0 - 1.5,
ry = h / 2.0 - 1.5,
),
AvatarShape::Squircle => format!(
r#"<rect x="1.5" y="1.5" width="{rw}" height="{rh}" rx="{r}" ry="{r}" fill="none" stroke="{fill}" stroke-width="3"/>"#,
rw = w - 3.0,
rh = h - 3.0,
r = w.min(h) * 0.18,
),
AvatarShape::Hexagon => {
let points = format!(
"{},{} {},{} {},{} {},{} {},{} {},{}",
w * 0.25,
1.5,
w * 0.75,
1.5,
w - 1.5,
h * 0.5,
w * 0.75,
h - 1.5,
w * 0.25,
h - 1.5,
1.5,
h * 0.5
);
format!(r#"<polygon points="{points}" fill="none" stroke="{fill}" stroke-width="3"/>"#)
}
AvatarShape::Octagon => {
let points = format!(
"{},{} {},{} {},{} {},{} {},{} {},{} {},{} {},{}",
w * 0.30,
1.5,
w * 0.70,
1.5,
w - 1.5,
h * 0.30,
w - 1.5,
h * 0.70,
w * 0.70,
h - 1.5,
w * 0.30,
h - 1.5,
1.5,
h * 0.70,
1.5,
h * 0.30
);
format!(r#"<polygon points="{points}" fill="none" stroke="{fill}" stroke-width="3"/>"#)
}
};
format!(r#"<g data-layer="shape-{}">{body}</g>"#, shape.as_str())
}
fn render_shape_svg_clip(spec: AvatarSpec, shape: AvatarShape, content: &str) -> (String, String) {
if shape == AvatarShape::Square {
return (String::new(), content.to_owned());
}
let clip_id = "hashavatar-frame-clip";
let clip = render_shape_svg_clip_body(spec, shape);
(
format!(r#"<defs><clipPath id="{clip_id}">{clip}</clipPath></defs>"#),
format!(r#"<g clip-path="url(#{clip_id})">{content}</g>"#),
)
}
fn render_shape_svg_clip_body(spec: AvatarSpec, shape: AvatarShape) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
match shape {
AvatarShape::Square => format!(r#"<rect x="0" y="0" width="{w}" height="{h}"/>"#),
AvatarShape::Circle => format!(
r#"<ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}"/>"#,
cx = w / 2.0,
cy = h / 2.0,
rx = w / 2.0,
ry = h / 2.0,
),
AvatarShape::Squircle => format!(
r#"<rect x="0" y="0" width="{w}" height="{h}" rx="{r}" ry="{r}"/>"#,
r = w.min(h) * 0.18,
),
AvatarShape::Hexagon => {
let points = format!(
"{},{} {},{} {},{} {},{} {},{} {},{}",
w * 0.25,
0.0,
w * 0.75,
0.0,
w,
h * 0.5,
w * 0.75,
h,
w * 0.25,
h,
0.0,
h * 0.5
);
format!(r#"<polygon points="{points}"/>"#)
}
AvatarShape::Octagon => {
let points = format!(
"{},{} {},{} {},{} {},{} {},{} {},{} {},{} {},{}",
w * 0.30,
0.0,
w * 0.70,
0.0,
w,
h * 0.30,
w,
h * 0.70,
w * 0.70,
h,
w * 0.30,
h,
0.0,
h * 0.70,
0.0,
h * 0.30
);
format!(r#"<polygon points="{points}"/>"#)
}
}
}
/// Render a cat face avatar into an RGBA image.
pub fn render_cat_avatar(spec: AvatarSpec) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let seed = spec.seed.to_le_bytes();
let identity = AvatarIdentity::new_unchecked(AvatarIdentityOptions::default(), &seed);
Ok(render_cat_avatar_with_identity(
spec,
&identity,
AvatarBackground::Themed,
))
}
/// Render a cat face avatar from a stable identity digest.
pub fn render_cat_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
Ok(render_cat_avatar_with_identity(
spec,
identity,
AvatarBackground::Themed,
))
}
pub fn render_cat_avatar_for_identity_with_background(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
Ok(render_cat_avatar_with_identity(spec, identity, background))
}
fn seeded_renderer_rng(spec: AvatarSpec, identity: &AvatarIdentity) -> StdRng {
let mut rng_seed = identity.rng_seed();
for (index, byte) in spec.seed.to_le_bytes().iter().enumerate() {
rng_seed[index] ^= *byte;
}
let mut rng_seed_value = *rng_seed;
drop(rng_seed);
let rng = StdRng::from_seed(rng_seed_value);
rng_seed_value.zeroize();
rng
}
fn render_cat_avatar_with_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> RgbaImage {
let mut rng = seeded_renderer_rng(spec, identity);
let genome = CatGenome::from_identity(identity, &mut rng);
let palette = CatPalette::from_genome(&genome);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, palette.background).into(),
);
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = ((height as f32) * (0.53 + genome.head_drop * 0.08)) as i32;
let head_rx = ((width as f32) * (0.26 + genome.head_width * 0.07)) as i32;
let head_ry = ((height as f32) * (0.22 + genome.head_height * 0.08)) as i32;
let ear_height = ((height as f32) * (0.15 + genome.ear_height * 0.08)) as i32;
let ear_width = ((width as f32) * (0.12 + genome.ear_width * 0.08)) as i32;
draw_background_accent(
&mut image,
center_x,
center_y,
head_rx,
head_ry,
palette.accent,
genome.accent_band_height,
background,
);
draw_ear(
&mut image,
EarSpec::left(
center_x,
center_y,
head_rx,
head_ry,
ear_width,
ear_height,
genome.ear_tilt,
),
palette.head,
palette.ear_inner,
palette.outline,
);
draw_ear(
&mut image,
EarSpec::right(
center_x,
center_y,
head_rx,
head_ry,
ear_width,
ear_height,
genome.ear_tilt,
),
palette.head,
palette.ear_inner,
palette.outline,
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y),
head_rx,
head_ry,
palette.head.into(),
);
draw_hollow_circle_mut(
&mut image,
(center_x, center_y),
head_rx.min(head_ry),
palette.outline.into(),
);
let muzzle_center = (center_x, center_y + head_ry / 4);
draw_filled_ellipse_mut(
&mut image,
muzzle_center,
(head_rx as f32 * (0.40 + genome.muzzle_width * 0.18)) as i32,
(head_ry as f32 * (0.24 + genome.muzzle_height * 0.14)) as i32,
palette.muzzle.into(),
);
draw_eyes(
&mut image, center_x, center_y, head_rx, head_ry, palette, genome,
);
draw_nose_and_mouth(
&mut image, center_x, center_y, head_rx, head_ry, palette, genome,
);
draw_whiskers(
&mut image,
center_x,
center_y,
head_rx,
head_ry,
palette.outline,
genome,
);
draw_cat_markings(
&mut image,
center_x,
center_y,
head_rx,
head_ry,
palette.marking,
genome,
);
image
}
pub fn render_dog_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let mut image =
ImageBuffer::from_pixel(spec.width, spec.height, Color::rgb(255, 255, 255).into());
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.56) as i32;
let head_rx = (width as f32 * (0.24 + identity.unit_f32(0) * 0.08)) as i32;
let head_ry = (height as f32 * (0.22 + identity.unit_f32(1) * 0.09)) as i32;
let ear_drop = (head_ry as f32 * (0.65 + identity.unit_f32(2) * 0.35)) as i32;
let muzzle_rx = (head_rx as f32 * (0.30 + identity.unit_f32(3) * 0.16)) as i32;
let muzzle_ry = (head_ry as f32 * (0.22 + identity.unit_f32(4) * 0.10)) as i32;
let fur = hsl_to_color(
18.0 + identity.unit_f32(5) * 45.0,
0.40,
0.55 + identity.unit_f32(6) * 0.18,
);
let accent = hsl_to_color(190.0 + identity.unit_f32(7) * 70.0, 0.28, 0.88);
let ear = hsl_to_color(
22.0 + identity.unit_f32(8) * 30.0,
0.38,
0.38 + identity.unit_f32(9) * 0.12,
);
let muzzle = hsl_to_color(32.0 + identity.unit_f32(10) * 12.0, 0.18, 0.90);
let nose = Color::rgb(45, 36, 34);
let eye = Color::rgb(36, 26, 20);
let tongue = hsl_to_color(350.0 + identity.unit_f32(11) * 10.0, 0.70, 0.70);
let spot = hsl_to_color(
24.0 + identity.unit_f32(12) * 20.0,
0.36,
0.34 + identity.unit_f32(13) * 0.10,
);
let bg_fill = background_fill(background, accent);
image.pixels_mut().for_each(|pixel| *pixel = bg_fill.into());
draw_background_accent(
&mut image, center_x, center_y, head_rx, head_ry, accent, 0.45, background,
);
draw_filled_ellipse_mut(
&mut image,
(center_x - head_rx / 2, center_y - head_ry / 5),
head_rx / 3,
ear_drop,
ear.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x + head_rx / 2, center_y - head_ry / 5),
head_rx / 3,
ear_drop,
ear.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y),
head_rx,
head_ry,
fur.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y + head_ry / 3),
muzzle_rx,
muzzle_ry,
muzzle.into(),
);
if identity.byte(14).is_multiple_of(2) {
draw_filled_ellipse_mut(
&mut image,
(center_x - head_rx / 3, center_y - head_ry / 8),
head_rx / 4,
head_ry / 3,
Color::rgba(spot.0[0], spot.0[1], spot.0[2], 150).into(),
);
}
let eye_y = center_y - head_ry / 6;
let eye_offset = (head_rx as f32 * (0.28 + identity.unit_f32(15) * 0.12)) as i32;
for x in [center_x - eye_offset, center_x + eye_offset] {
draw_filled_circle_mut(
&mut image,
(x, eye_y),
(head_rx as f32 * 0.08) as i32,
Color::rgb(255, 255, 255).into(),
);
draw_filled_circle_mut(
&mut image,
(x, eye_y),
(head_rx as f32 * 0.04) as i32,
eye.into(),
);
}
let nose_y = center_y + head_ry / 5;
draw_filled_ellipse_mut(
&mut image,
(center_x, nose_y),
head_rx / 7,
head_ry / 10,
nose.into(),
);
draw_line_segment_mut(
&mut image,
(center_x as f32, nose_y as f32),
(center_x as f32, (nose_y + head_ry / 7) as f32),
nose.into(),
);
draw_smile_arc(
&mut image,
center_x - head_rx / 12,
nose_y + head_ry / 10,
head_rx / 7,
nose,
0.55,
);
draw_smile_arc(
&mut image,
center_x + head_rx / 12,
nose_y + head_ry / 10,
head_rx / 7,
nose,
0.55,
);
if !identity.byte(16).is_multiple_of(3) {
draw_filled_ellipse_mut(
&mut image,
(center_x, nose_y + head_ry / 4),
head_rx / 10,
head_ry / 7,
tongue.into(),
);
}
Ok(image)
}
pub fn render_robot_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let mut image =
ImageBuffer::from_pixel(spec.width, spec.height, Color::rgb(255, 255, 255).into());
let center_x = width / 2;
let center_y = (height as f32 * 0.56) as i32;
let bg = hsl_to_color(
210.0 + identity.unit_f32(0) * 70.0,
0.18 + identity.unit_f32(1) * 0.18,
0.92,
);
let accent = hsl_to_color(160.0 + identity.unit_f32(2) * 120.0, 0.48, 0.62);
let metal = hsl_to_color(200.0 + identity.unit_f32(3) * 28.0, 0.16, 0.74);
let trim = hsl_to_color(205.0 + identity.unit_f32(4) * 22.0, 0.18, 0.46);
let light = hsl_to_color(50.0 + identity.unit_f32(5) * 120.0, 0.84, 0.66);
let dark = Color::rgb(47, 60, 72);
let bg_fill = background_fill(background, bg);
image.pixels_mut().for_each(|pixel| *pixel = bg_fill.into());
let head_w = (width as f32 * (0.44 + identity.unit_f32(6) * 0.12)) as i32;
let head_h = (height as f32 * (0.34 + identity.unit_f32(7) * 0.10)) as i32;
let head_x = center_x - head_w / 2;
let head_y = center_y - head_h / 2;
draw_background_accent(
&mut image,
center_x,
center_y,
head_w / 2,
head_h / 2,
accent,
0.5,
background,
);
draw_filled_rect_mut(
&mut image,
Rect::at(head_x, head_y).of_size(head_w as u32, head_h as u32),
metal.into(),
);
draw_line_segment_mut(
&mut image,
(head_x as f32, head_y as f32),
((head_x + head_w) as f32, head_y as f32),
trim.into(),
);
draw_line_segment_mut(
&mut image,
(head_x as f32, (head_y + head_h) as f32),
((head_x + head_w) as f32, (head_y + head_h) as f32),
trim.into(),
);
draw_line_segment_mut(
&mut image,
(head_x as f32, head_y as f32),
(head_x as f32, (head_y + head_h) as f32),
trim.into(),
);
draw_line_segment_mut(
&mut image,
((head_x + head_w) as f32, head_y as f32),
((head_x + head_w) as f32, (head_y + head_h) as f32),
trim.into(),
);
let antenna_h = (height as f32 * 0.10) as i32;
draw_line_segment_mut(
&mut image,
(center_x as f32, (head_y - antenna_h / 2) as f32),
(center_x as f32, head_y as f32),
dark.into(),
);
draw_filled_circle_mut(
&mut image,
(center_x, head_y - antenna_h / 2),
(head_w as f32 * 0.05) as i32,
accent.into(),
);
let eye_y = center_y - head_h / 6;
let eye_offset = head_w / 4;
let eye_rx = (head_w as f32 * 0.12) as i32;
let eye_ry = (head_h as f32 * 0.10) as i32;
for x in [center_x - eye_offset, center_x + eye_offset] {
draw_filled_ellipse_mut(&mut image, (x, eye_y), eye_rx, eye_ry, light.into());
if identity.byte(8).is_multiple_of(2) {
draw_filled_circle_mut(
&mut image,
(x, eye_y),
(eye_rx as f32 * 0.35) as i32,
dark.into(),
);
}
}
let mouth_y = center_y + head_h / 5;
let mouth_w = (head_w as f32 * 0.42) as i32;
let mouth_h = (head_h as f32 * 0.12) as i32;
draw_filled_rect_mut(
&mut image,
Rect::at(center_x - mouth_w / 2, mouth_y - mouth_h / 2)
.of_size(mouth_w as u32, mouth_h as u32),
dark.into(),
);
let teeth = 4 + (identity.byte(9) % 4) as i32;
for idx in 1..teeth {
let x = center_x - mouth_w / 2 + idx * mouth_w / teeth;
draw_line_segment_mut(
&mut image,
(x as f32, (mouth_y - mouth_h / 2) as f32),
(x as f32, (mouth_y + mouth_h / 2) as f32),
metal.into(),
);
}
let bolt_y = center_y;
draw_filled_circle_mut(
&mut image,
(head_x + head_w / 8, bolt_y),
(head_w as f32 * 0.035) as i32,
trim.into(),
);
draw_filled_circle_mut(
&mut image,
(head_x + head_w - head_w / 8, bolt_y),
(head_w as f32 * 0.035) as i32,
trim.into(),
);
Ok(image)
}
pub fn render_fox_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.56) as i32;
let mut image =
ImageBuffer::from_pixel(spec.width, spec.height, Color::rgb(255, 255, 255).into());
let bg = hsl_to_color(22.0 + identity.unit_f32(0) * 26.0, 0.26, 0.92);
let orange = hsl_to_color(18.0 + identity.unit_f32(1) * 20.0, 0.76, 0.58);
let deep_orange = hsl_to_color(16.0 + identity.unit_f32(2) * 12.0, 0.72, 0.42);
let cream = hsl_to_color(40.0 + identity.unit_f32(3) * 10.0, 0.32, 0.93);
let eye = Color::rgb(34, 28, 24);
let nose = Color::rgb(55, 40, 34);
image
.pixels_mut()
.for_each(|pixel| *pixel = background_fill(background, bg).into());
let head_rx = (width as f32 * (0.25 + identity.unit_f32(4) * 0.08)) as i32;
let head_ry = (height as f32 * (0.22 + identity.unit_f32(5) * 0.08)) as i32;
let ear_h = (height as f32 * (0.16 + identity.unit_f32(6) * 0.09)) as i32;
let ear_w = (width as f32 * (0.12 + identity.unit_f32(7) * 0.05)) as i32;
draw_background_accent(
&mut image,
center_x,
center_y,
head_rx,
head_ry,
deep_orange,
0.35,
background,
);
draw_ear(
&mut image,
EarSpec::left(center_x, center_y, head_rx, head_ry, ear_w, ear_h, -0.2),
orange,
cream,
deep_orange,
);
draw_ear(
&mut image,
EarSpec::right(center_x, center_y, head_rx, head_ry, ear_w, ear_h, 0.2),
orange,
cream,
deep_orange,
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y),
head_rx,
head_ry,
orange.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y + head_ry / 4),
head_rx / 2,
head_ry / 3,
cream.into(),
);
draw_polygon_mut(
&mut image,
&[
Point::new(center_x - head_rx / 2, center_y - head_ry / 8),
Point::new(center_x, center_y + head_ry / 3),
Point::new(center_x - head_rx / 8, center_y + head_ry / 2),
],
cream.into(),
);
draw_polygon_mut(
&mut image,
&[
Point::new(center_x + head_rx / 2, center_y - head_ry / 8),
Point::new(center_x, center_y + head_ry / 3),
Point::new(center_x + head_rx / 8, center_y + head_ry / 2),
],
cream.into(),
);
let eye_y = center_y - head_ry / 7;
let eye_offset = head_rx / 3;
for x in [center_x - eye_offset, center_x + eye_offset] {
draw_filled_ellipse_mut(
&mut image,
(x, eye_y),
head_rx / 10,
head_ry / 8,
Color::rgb(255, 255, 255).into(),
);
draw_filled_ellipse_mut(
&mut image,
(x, eye_y),
head_rx / 18,
head_ry / 7,
eye.into(),
);
}
let nose_y = center_y + head_ry / 4;
draw_polygon_mut(
&mut image,
&[
Point::new(center_x - head_rx / 10, nose_y),
Point::new(center_x + head_rx / 10, nose_y),
Point::new(center_x, nose_y + head_ry / 10),
],
nose.into(),
);
draw_smile_arc(
&mut image,
center_x - head_rx / 12,
nose_y + head_ry / 10,
head_rx / 7,
nose,
0.45,
);
draw_smile_arc(
&mut image,
center_x + head_rx / 12,
nose_y + head_ry / 10,
head_rx / 7,
nose,
0.45,
);
Ok(image)
}
pub fn render_alien_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.56) as i32;
let mut image =
ImageBuffer::from_pixel(spec.width, spec.height, Color::rgb(255, 255, 255).into());
let skin = hsl_to_color(
90.0 + identity.unit_f32(0) * 80.0,
0.45 + identity.unit_f32(1) * 0.20,
0.68,
);
let shade = hsl_to_color(110.0 + identity.unit_f32(2) * 50.0, 0.38, 0.44);
let accent = hsl_to_color(280.0 + identity.unit_f32(3) * 40.0, 0.32, 0.92);
let eye = Color::rgb(28, 18, 38);
image
.pixels_mut()
.for_each(|pixel| *pixel = background_fill(background, accent).into());
let head_rx = (width as f32 * (0.20 + identity.unit_f32(4) * 0.08)) as i32;
let head_ry = (height as f32 * (0.28 + identity.unit_f32(5) * 0.10)) as i32;
draw_background_accent(
&mut image, center_x, center_y, head_rx, head_ry, shade, 0.28, background,
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y),
head_rx,
head_ry,
skin.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x - head_rx / 2, center_y - head_ry / 4),
head_rx / 5,
head_ry / 3,
eye.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x + head_rx / 2, center_y - head_ry / 4),
head_rx / 5,
head_ry / 3,
eye.into(),
);
draw_filled_circle_mut(
&mut image,
(center_x, center_y + head_ry / 8),
head_rx / 14,
shade.into(),
);
if identity.byte(6).is_multiple_of(2) {
draw_line_segment_mut(
&mut image,
(
(center_x - head_rx / 8) as f32,
(center_y + head_ry / 3) as f32,
),
(
(center_x + head_rx / 8) as f32,
(center_y + head_ry / 3) as f32,
),
shade.into(),
);
}
Ok(image)
}
pub fn render_monster_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.58) as i32;
let mut image =
ImageBuffer::from_pixel(spec.width, spec.height, Color::rgb(255, 255, 255).into());
let skin = hsl_to_color(
identity.unit_f32(0) * 360.0,
0.48 + identity.unit_f32(1) * 0.24,
0.46 + identity.unit_f32(2) * 0.20,
);
let shade = hsl_to_color(
identity.unit_f32(3) * 360.0,
0.38 + identity.unit_f32(4) * 0.18,
0.24 + identity.unit_f32(5) * 0.10,
);
let accent = hsl_to_color(
20.0 + identity.unit_f32(6) * 320.0,
0.34 + identity.unit_f32(7) * 0.26,
0.86,
);
let mouth = Color::rgb(48, 18, 24);
let eye_white = Color::rgb(252, 248, 236);
let pupil = Color::rgb(24, 20, 28);
image
.pixels_mut()
.for_each(|pixel| *pixel = background_fill(background, accent).into());
let head_rx = (width as f32 * (0.23 + identity.unit_f32(8) * 0.10)) as i32;
let head_ry = (height as f32 * (0.22 + identity.unit_f32(9) * 0.11)) as i32;
let horn_height = (height as f32 * (0.08 + identity.unit_f32(10) * 0.10)) as i32;
let horn_width = (width as f32 * (0.06 + identity.unit_f32(11) * 0.05)) as i32;
let eye_count = 1 + (identity.byte(12) % 3) as usize;
let mouth_style = identity.byte(13) % 3;
let body_style = identity.byte(14) % 3;
let spot_count = 3 + (identity.byte(15) % 5) as i32;
let tentacle_count = 2 + (identity.byte(16) % 4) as i32;
draw_background_accent(
&mut image,
center_x,
center_y,
head_rx,
head_ry,
shade,
0.30 + identity.unit_f32(17) * 0.25,
background,
);
match body_style {
0 => draw_filled_ellipse_mut(
&mut image,
(center_x, center_y),
head_rx,
head_ry,
skin.into(),
),
1 => {
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y + head_ry / 8),
head_rx,
head_ry - head_ry / 8,
skin.into(),
);
draw_polygon_mut(
&mut image,
&[
Point::new(center_x - head_rx, center_y),
Point::new(center_x, center_y - head_ry),
Point::new(center_x + head_rx, center_y),
Point::new(center_x + head_rx / 2, center_y + head_ry),
Point::new(center_x - head_rx / 2, center_y + head_ry),
],
skin.into(),
);
}
_ => {
draw_filled_rect_mut(
&mut image,
Rect::at(center_x - head_rx, center_y - head_ry)
.of_size((head_rx * 2) as u32, (head_ry * 2) as u32),
skin.into(),
);
draw_filled_circle_mut(
&mut image,
(center_x - head_rx, center_y - head_ry / 2),
head_ry / 2,
skin.into(),
);
draw_filled_circle_mut(
&mut image,
(center_x + head_rx, center_y - head_ry / 2),
head_ry / 2,
skin.into(),
);
draw_filled_circle_mut(
&mut image,
(center_x - head_rx, center_y + head_ry / 2),
head_ry / 2,
skin.into(),
);
draw_filled_circle_mut(
&mut image,
(center_x + head_rx, center_y + head_ry / 2),
head_ry / 2,
skin.into(),
);
}
}
if identity.byte(18).is_multiple_of(2) {
draw_polygon_mut(
&mut image,
&[
Point::new(center_x - head_rx / 2, center_y - head_ry),
Point::new(
center_x - head_rx / 3 - horn_width,
center_y - head_ry - horn_height,
),
Point::new(center_x - head_rx / 8, center_y - head_ry / 2),
],
shade.into(),
);
draw_polygon_mut(
&mut image,
&[
Point::new(center_x + head_rx / 2, center_y - head_ry),
Point::new(
center_x + head_rx / 3 + horn_width,
center_y - head_ry - horn_height,
),
Point::new(center_x + head_rx / 8, center_y - head_ry / 2),
],
shade.into(),
);
} else {
for spike in 0..3 {
let spike_x = center_x - head_rx / 2 + spike * head_rx / 2;
draw_polygon_mut(
&mut image,
&[
Point::new(spike_x - horn_width / 2, center_y - head_ry / 2),
Point::new(spike_x, center_y - head_ry - horn_height / 2),
Point::new(spike_x + horn_width / 2, center_y - head_ry / 2),
],
shade.into(),
);
}
}
for index in 0..spot_count {
let x = center_x - head_rx / 2 + (index * head_rx) / spot_count;
let y = center_y - head_ry / 3 + ((index * 37 + identity.byte(19) as i32) % head_ry.max(1));
let radius =
(head_rx as f32 * (0.05 + ((index + 1) as f32 / spot_count as f32) * 0.06)) as i32;
draw_filled_circle_mut(
&mut image,
(x, y),
radius.max(3),
Color::rgba(shade.0[0], shade.0[1], shade.0[2], 168).into(),
);
}
let eye_y = center_y - head_ry / 5;
let eye_rx = (head_rx as f32 * (0.10 + identity.unit_f32(20) * 0.08)) as i32;
let eye_ry = (head_ry as f32 * (0.10 + identity.unit_f32(21) * 0.10)) as i32;
let eye_spacing = if eye_count == 1 {
0
} else {
(head_rx as f32 * 0.46 / (eye_count - 1) as f32) as i32
};
let eye_start = center_x - eye_spacing * ((eye_count.saturating_sub(1)) as i32) / 2;
for index in 0..eye_count {
let x = eye_start + eye_spacing * index as i32;
draw_filled_ellipse_mut(&mut image, (x, eye_y), eye_rx, eye_ry, eye_white.into());
if identity.byte(22).is_multiple_of(2) {
draw_filled_ellipse_mut(
&mut image,
(x, eye_y),
(eye_rx / 3).max(2),
(eye_ry - 1).max(2),
pupil.into(),
);
} else {
draw_filled_circle_mut(&mut image, (x, eye_y), (eye_ry / 2).max(2), pupil.into());
}
draw_filled_circle_mut(
&mut image,
(x - eye_rx / 3, eye_y - eye_ry / 3),
(eye_rx / 5).max(1),
Color::rgba(255, 255, 255, 220).into(),
);
}
let mouth_y = center_y + head_ry / 3;
match mouth_style {
0 => {
draw_filled_ellipse_mut(
&mut image,
(center_x, mouth_y),
head_rx / 3,
head_ry / 8,
mouth.into(),
);
for fang_x in [center_x - head_rx / 8, center_x + head_rx / 8] {
draw_polygon_mut(
&mut image,
&[
Point::new(fang_x - head_rx / 24, mouth_y - 2),
Point::new(fang_x + head_rx / 24, mouth_y - 2),
Point::new(fang_x, mouth_y + head_ry / 5),
],
eye_white.into(),
);
}
}
1 => {
draw_smile_arc(
&mut image,
center_x - head_rx / 10,
mouth_y,
head_rx / 4,
mouth,
0.50,
);
draw_smile_arc(
&mut image,
center_x + head_rx / 10,
mouth_y,
head_rx / 4,
mouth,
0.50,
);
draw_line_segment_mut(
&mut image,
((center_x - head_rx / 4) as f32, mouth_y as f32),
((center_x + head_rx / 4) as f32, mouth_y as f32),
mouth.into(),
);
}
_ => {
draw_filled_rect_mut(
&mut image,
Rect::at(center_x - head_rx / 3, mouth_y - head_ry / 10)
.of_size((head_rx * 2 / 3) as u32, (head_ry / 5).max(1) as u32),
mouth.into(),
);
for tooth in 0..4 {
let tooth_x = center_x - head_rx / 4 + tooth * head_rx / 6;
draw_polygon_mut(
&mut image,
&[
Point::new(tooth_x - head_rx / 30, mouth_y - head_ry / 10),
Point::new(tooth_x + head_rx / 30, mouth_y - head_ry / 10),
Point::new(tooth_x, mouth_y + head_ry / 14),
],
eye_white.into(),
);
}
}
}
if identity.byte(23).is_multiple_of(2) {
for index in 0..tentacle_count {
let start_x = center_x - head_rx / 2 + (index * head_rx) / tentacle_count;
let start_y = center_y + head_ry - 4;
let end_x = start_x + ((index % 2) * 2 - 1) * head_rx / 6;
let end_y = start_y + head_ry / 2;
draw_antialiased_line_segment_mut(
&mut image,
(start_x, start_y),
(end_x, end_y),
shade.into(),
interpolate,
);
draw_filled_circle_mut(
&mut image,
(end_x, end_y),
(head_rx / 18).max(2),
shade.into(),
);
}
}
Ok(image)
}
#[derive(Clone, Copy)]
struct FaceLayout {
center_x: i32,
center_y: i32,
head_rx: i32,
head_ry: i32,
}
#[derive(Clone, Copy)]
enum CreatureEyeStyle {
Round,
Tall,
Hollow,
}
#[derive(Clone, Copy)]
#[allow(dead_code)]
enum CreatureMouthStyle {
Smile,
Fang,
Flat,
}
fn draw_creature_eyes(
image: &mut RgbaImage,
layout: FaceLayout,
count: usize,
style: CreatureEyeStyle,
eye_white: Color,
pupil: Color,
) {
let spacing = if count <= 1 {
0
} else {
(layout.head_rx as f32 * 0.48 / (count - 1) as f32) as i32
};
let start_x = layout.center_x - spacing * (count.saturating_sub(1) as i32) / 2;
let eye_y = layout.center_y - layout.head_ry / 5;
let eye_rx = (layout.head_rx as f32 * 0.12) as i32;
let eye_ry = (layout.head_ry as f32 * 0.12) as i32;
for index in 0..count {
let x = start_x + spacing * index as i32;
match style {
CreatureEyeStyle::Round => {
draw_filled_circle_mut(image, (x, eye_y), eye_rx.max(3), eye_white.into());
draw_filled_circle_mut(image, (x, eye_y), (eye_rx / 2).max(2), pupil.into());
}
CreatureEyeStyle::Tall => {
draw_filled_ellipse_mut(image, (x, eye_y), eye_rx, eye_ry + 4, eye_white.into());
draw_filled_ellipse_mut(
image,
(x, eye_y),
(eye_rx / 3).max(2),
(eye_ry + 2).max(2),
pupil.into(),
);
}
CreatureEyeStyle::Hollow => {
draw_filled_ellipse_mut(image, (x, eye_y), eye_rx + 3, eye_ry + 5, pupil.into());
draw_filled_ellipse_mut(
image,
(x, eye_y + 1),
(eye_rx / 2).max(2),
(eye_ry / 2).max(2),
Color::rgba(255, 255, 255, 20).into(),
);
}
}
}
}
fn draw_creature_mouth(
image: &mut RgbaImage,
layout: FaceLayout,
style: CreatureMouthStyle,
color: Color,
) {
let mouth_y = layout.center_y + layout.head_ry / 3;
match style {
CreatureMouthStyle::Smile => {
draw_smile_arc(
image,
layout.center_x - layout.head_rx / 10,
mouth_y,
layout.head_rx / 4,
color,
0.45,
);
draw_smile_arc(
image,
layout.center_x + layout.head_rx / 10,
mouth_y,
layout.head_rx / 4,
color,
0.45,
);
}
CreatureMouthStyle::Fang => {
draw_filled_ellipse_mut(
image,
(layout.center_x, mouth_y),
layout.head_rx / 3,
layout.head_ry / 8,
color.into(),
);
for tooth_x in [
layout.center_x - layout.head_rx / 7,
layout.center_x + layout.head_rx / 7,
] {
draw_polygon_mut(
image,
&[
Point::new(tooth_x - layout.head_rx / 26, mouth_y - 1),
Point::new(tooth_x + layout.head_rx / 26, mouth_y - 1),
Point::new(tooth_x, mouth_y + layout.head_ry / 5),
],
Color::rgb(248, 246, 238).into(),
);
}
}
CreatureMouthStyle::Flat => {
draw_filled_rect_mut(
image,
Rect::at(layout.center_x - layout.head_rx / 3, mouth_y - 3)
.of_size((layout.head_rx * 2 / 3) as u32, 6),
color.into(),
);
}
}
}
pub fn render_ghost_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let head_rx = (width as f32 * (0.19 + identity.unit_f32(3) * 0.08)) as i32;
let head_ry = (height as f32 * (0.21 + identity.unit_f32(4) * 0.08)) as i32;
let layout = FaceLayout {
center_x: width / 2,
center_y: (height as f32 * (0.52 + identity.unit_f32(5) * 0.06)) as i32,
head_rx,
head_ry,
};
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(
background,
hsl_to_color(
205.0 + identity.unit_f32(0) * 55.0,
0.16 + identity.unit_f32(6) * 0.08,
0.95,
),
)
.into(),
);
let body = hsl_to_color(
190.0 + identity.unit_f32(1) * 55.0,
0.10 + identity.unit_f32(7) * 0.10,
0.94 + identity.unit_f32(8) * 0.04,
);
let shade = hsl_to_color(
210.0 + identity.unit_f32(2) * 34.0,
0.16 + identity.unit_f32(9) * 0.12,
0.70 + identity.unit_f32(10) * 0.12,
);
draw_background_accent(
&mut image,
layout.center_x,
layout.center_y,
layout.head_rx,
layout.head_ry,
shade,
0.28,
background,
);
if background == AvatarBackground::Themed && identity.byte(11).is_multiple_of(2) {
draw_filled_ellipse_mut(
&mut image,
(
layout.center_x - layout.head_rx / 2,
layout.center_y + layout.head_ry / 3,
),
(layout.head_rx as f32 * 0.42) as i32,
(layout.head_ry as f32 * 0.18) as i32,
Color::rgba(shade.0[0], shade.0[1], shade.0[2], 80).into(),
);
}
draw_filled_ellipse_mut(
&mut image,
(layout.center_x, layout.center_y),
layout.head_rx,
layout.head_ry,
body.into(),
);
draw_filled_rect_mut(
&mut image,
Rect::at(layout.center_x - layout.head_rx, layout.center_y).of_size(
(layout.head_rx * 2) as u32,
(layout.head_ry + layout.head_ry / 2) as u32,
),
body.into(),
);
let scallops = 3 + (identity.byte(12) % 3) as i32;
for index in 0..scallops {
let denominator = (scallops - 1).max(1);
let x = layout.center_x - layout.head_rx + index * (layout.head_rx * 2 / denominator);
let radius = ((layout.head_rx as f32)
* (0.18 + identity.unit_f32(13 + index as usize) * 0.12)) as i32;
draw_filled_circle_mut(
&mut image,
(x, layout.center_y + layout.head_ry + layout.head_ry / 2),
radius.max(3),
body.into(),
);
}
if identity.byte(16).is_multiple_of(2) {
for side in [-1, 1] {
draw_filled_ellipse_mut(
&mut image,
(
layout.center_x + side * (layout.head_rx + layout.head_rx / 5),
layout.center_y + layout.head_ry / 4,
),
(layout.head_rx as f32 * (0.20 + identity.unit_f32(17) * 0.08)) as i32,
(layout.head_ry as f32 * 0.16) as i32,
Color::rgba(body.0[0], body.0[1], body.0[2], 210).into(),
);
}
}
draw_creature_eyes(
&mut image,
layout,
if identity.byte(18).is_multiple_of(5) {
3
} else {
2
},
if identity.byte(19).is_multiple_of(2) {
CreatureEyeStyle::Tall
} else {
CreatureEyeStyle::Hollow
},
Color::rgb(42, 48, 68),
Color::rgb(42, 48, 68),
);
let mouth_style = match identity.byte(20) % 3 {
0 => CreatureMouthStyle::Smile,
1 => CreatureMouthStyle::Fang,
_ => CreatureMouthStyle::Flat,
};
draw_creature_mouth(&mut image, layout, mouth_style, shade);
Ok(image)
}
pub fn render_slime_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let head_rx = (width as f32 * (0.20 + identity.unit_f32(6) * 0.10)) as i32;
let head_ry = (height as f32 * (0.16 + identity.unit_f32(7) * 0.08)) as i32;
let layout = FaceLayout {
center_x: width / 2,
center_y: (height as f32 * (0.56 + identity.unit_f32(8) * 0.08)) as i32,
head_rx,
head_ry,
};
let bg = hsl_to_color(110.0 + identity.unit_f32(3) * 80.0, 0.18, 0.93);
let slime = hsl_to_color(
70.0 + identity.unit_f32(4) * 130.0,
0.44 + identity.unit_f32(9) * 0.22,
0.46 + identity.unit_f32(10) * 0.18,
);
let dark = hsl_to_color(
95.0 + identity.unit_f32(5) * 80.0,
0.34 + identity.unit_f32(11) * 0.18,
0.25 + identity.unit_f32(12) * 0.14,
);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
draw_background_accent(
&mut image,
layout.center_x,
layout.center_y,
layout.head_rx,
layout.head_ry,
dark,
0.32,
background,
);
draw_filled_ellipse_mut(
&mut image,
(layout.center_x, layout.center_y),
layout.head_rx,
layout.head_ry,
slime.into(),
);
let drip_count = 2 + (identity.byte(13) % 4) as i32;
for index in 0..drip_count {
let spacing = (layout.head_rx * 2 / drip_count.max(1)).max(1);
let drip_w =
(layout.head_rx as f32 * (0.18 + identity.unit_f32(14 + index as usize) * 0.12)) as i32;
let drip_x = layout.center_x - layout.head_rx
+ index * spacing
+ (identity.byte(18 + index as usize) as i32 % spacing.max(1) / 3);
let drip_h =
(layout.head_ry as f32 * (0.35 + identity.unit_f32(22 + index as usize) * 0.55)) as i32;
draw_filled_rect_mut(
&mut image,
Rect::at(drip_x, layout.center_y).of_size(drip_w.max(2) as u32, drip_h.max(2) as u32),
slime.into(),
);
draw_filled_circle_mut(
&mut image,
(drip_x + drip_w / 2, layout.center_y + drip_h),
layout.head_rx / 7,
slime.into(),
);
}
let bubble_count = 3 + (identity.byte(27) % 5) as i32;
for bubble in 0..bubble_count {
let bx = layout.center_x - layout.head_rx
+ (identity.byte(28 + bubble as usize) as i32 % (layout.head_rx * 2).max(1));
let by = layout.center_y - layout.head_ry / 2
+ (identity.byte(35 + bubble as usize) as i32 % layout.head_ry.max(1));
draw_filled_circle_mut(
&mut image,
(bx, by),
((layout.head_rx as f32) * (0.05 + identity.unit_f32(42 + bubble as usize) * 0.07))
as i32,
Color::rgba(255, 255, 255, 90).into(),
);
}
draw_creature_eyes(
&mut image,
layout,
1 + (identity.byte(49) % 3) as usize,
if identity.byte(50).is_multiple_of(2) {
CreatureEyeStyle::Round
} else {
CreatureEyeStyle::Tall
},
Color::rgb(248, 255, 236),
Color::rgb(32, 48, 24),
);
let mouth_style = match identity.byte(51) % 3 {
0 => CreatureMouthStyle::Flat,
1 => CreatureMouthStyle::Smile,
_ => CreatureMouthStyle::Fang,
};
draw_creature_mouth(&mut image, layout, mouth_style, dark);
Ok(image)
}
pub fn render_bird_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let layout = FaceLayout {
center_x: width / 2,
center_y: (height as f32 * 0.56) as i32,
head_rx: (width as f32 * 0.22) as i32,
head_ry: (height as f32 * 0.22) as i32,
};
let bg = hsl_to_color(190.0 + identity.unit_f32(6) * 60.0, 0.18, 0.93);
let plumage = hsl_to_color(identity.unit_f32(7) * 360.0, 0.42, 0.62);
let wing = hsl_to_color(20.0 + identity.unit_f32(8) * 160.0, 0.32, 0.46);
let beak = hsl_to_color(32.0 + identity.unit_f32(9) * 26.0, 0.82, 0.58);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
draw_background_accent(
&mut image,
layout.center_x,
layout.center_y,
layout.head_rx,
layout.head_ry,
wing,
0.24,
background,
);
draw_filled_circle_mut(
&mut image,
(layout.center_x, layout.center_y),
layout.head_rx,
plumage.into(),
);
draw_filled_ellipse_mut(
&mut image,
(
layout.center_x - layout.head_rx / 2,
layout.center_y + layout.head_ry / 6,
),
layout.head_rx / 3,
layout.head_ry / 2,
wing.into(),
);
draw_filled_ellipse_mut(
&mut image,
(
layout.center_x + layout.head_rx / 2,
layout.center_y + layout.head_ry / 6,
),
layout.head_rx / 3,
layout.head_ry / 2,
wing.into(),
);
draw_polygon_mut(
&mut image,
&[
Point::new(layout.center_x, layout.center_y),
Point::new(
layout.center_x + layout.head_rx / 2,
layout.center_y + layout.head_ry / 6,
),
Point::new(layout.center_x, layout.center_y + layout.head_ry / 3),
],
beak.into(),
);
for feather in 0..3 {
let fx = layout.center_x - layout.head_rx / 5 + feather * layout.head_rx / 5;
draw_polygon_mut(
&mut image,
&[
Point::new(fx, layout.center_y - layout.head_ry),
Point::new(
fx + layout.head_rx / 10,
layout.center_y - layout.head_ry - layout.head_ry / 2,
),
Point::new(
fx + layout.head_rx / 5,
layout.center_y - layout.head_ry / 2,
),
],
wing.into(),
);
}
draw_creature_eyes(
&mut image,
layout,
2,
CreatureEyeStyle::Round,
Color::rgb(255, 255, 255),
Color::rgb(28, 24, 34),
);
Ok(image)
}
pub fn render_wizard_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let head_rx = (width as f32 * (0.16 + identity.unit_f32(15) * 0.06)) as i32;
let head_ry = (height as f32 * (0.16 + identity.unit_f32(16) * 0.06)) as i32;
let layout = FaceLayout {
center_x: width / 2,
center_y: (height as f32 * (0.57 + identity.unit_f32(17) * 0.07)) as i32,
head_rx,
head_ry,
};
let bg = hsl_to_color(
220.0 + identity.unit_f32(10) * 85.0,
0.20 + identity.unit_f32(18) * 0.12,
0.90 + identity.unit_f32(19) * 0.04,
);
let hat = hsl_to_color(
210.0 + identity.unit_f32(11) * 110.0,
0.34 + identity.unit_f32(20) * 0.22,
0.28 + identity.unit_f32(21) * 0.16,
);
let hat_band = hsl_to_color(
24.0 + identity.unit_f32(12) * 160.0,
0.62 + identity.unit_f32(22) * 0.24,
0.48 + identity.unit_f32(23) * 0.18,
);
let skin = hsl_to_color(
18.0 + identity.unit_f32(13) * 28.0,
0.22 + identity.unit_f32(24) * 0.20,
0.74 + identity.unit_f32(25) * 0.12,
);
let beard = hsl_to_color(
35.0 + identity.unit_f32(14) * 45.0,
0.06 + identity.unit_f32(26) * 0.12,
0.80 + identity.unit_f32(27) * 0.16,
);
let hat_width = (layout.head_rx as f32 * (1.0 + identity.unit_f32(28) * 0.55)) as i32;
let hat_height = (layout.head_ry as f32 * (1.7 + identity.unit_f32(29) * 0.75)) as i32;
let tip_shift = (layout.head_rx as f32 * (identity.unit_f32(30) - 0.5) * 0.8) as i32;
let brim_width = (layout.head_rx as f32 * (2.45 + identity.unit_f32(31) * 0.75)) as i32;
let brim_height = (layout.head_ry as f32 * (0.22 + identity.unit_f32(32) * 0.16)) as i32;
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
draw_background_accent(
&mut image,
layout.center_x,
layout.center_y,
layout.head_rx,
layout.head_ry,
hat_band,
0.20,
background,
);
draw_polygon_mut(
&mut image,
&[
Point::new(
layout.center_x - hat_width,
layout.center_y - layout.head_ry / 2,
),
Point::new(
layout.center_x + hat_width,
layout.center_y - layout.head_ry / 2,
),
Point::new(
layout.center_x + tip_shift,
layout.center_y - layout.head_ry / 2 - hat_height,
),
],
hat.into(),
);
draw_filled_rect_mut(
&mut image,
Rect::at(
layout.center_x - brim_width / 2,
layout.center_y - layout.head_ry / 2,
)
.of_size(brim_width.max(2) as u32, brim_height.max(2) as u32),
hat_band.into(),
);
let star_count = 1 + (identity.byte(33) % 4) as i32;
for star in 0..star_count {
let sx = layout.center_x - hat_width / 2
+ (identity.byte(34 + star as usize) as i32 % hat_width.max(1));
let sy = layout.center_y - layout.head_ry / 2 - hat_height / 2
+ (identity.byte(39 + star as usize) as i32 % (hat_height / 2).max(1));
draw_filled_circle_mut(
&mut image,
(sx, sy),
(layout.head_rx / 12).max(2),
Color::rgba(hat_band.0[0], hat_band.0[1], hat_band.0[2], 210).into(),
);
}
draw_filled_circle_mut(
&mut image,
(layout.center_x, layout.center_y),
layout.head_rx,
skin.into(),
);
draw_polygon_mut(
&mut image,
&[
Point::new(
layout.center_x
- layout.head_rx / 2
- (identity.byte(44) as i32 % layout.head_rx.max(1)) / 6,
layout.center_y + layout.head_ry / 3,
),
Point::new(
layout.center_x
+ layout.head_rx / 2
+ (identity.byte(45) as i32 % layout.head_rx.max(1)) / 6,
layout.center_y + layout.head_ry / 3,
),
Point::new(
layout.center_x + (identity.unit_f32(46) * layout.head_rx as f32 * 0.4) as i32
- layout.head_rx / 5,
layout.center_y
+ layout.head_ry
+ (layout.head_ry as f32 * (0.35 + identity.unit_f32(47) * 0.55)) as i32,
),
],
beard.into(),
);
draw_creature_eyes(
&mut image,
layout,
if identity.byte(48).is_multiple_of(7) {
1
} else {
2
},
if identity.byte(49).is_multiple_of(2) {
CreatureEyeStyle::Round
} else {
CreatureEyeStyle::Tall
},
Color::rgb(255, 255, 255),
Color::rgb(36, 30, 52),
);
draw_creature_mouth(
&mut image,
layout,
CreatureMouthStyle::Smile,
Color::rgb(86, 64, 58),
);
draw_filled_circle_mut(
&mut image,
(
layout.center_x + tip_shift + layout.head_rx / 2,
layout.center_y - layout.head_ry / 2 - hat_height,
),
(layout.head_rx / 6).max(3),
hat_band.into(),
);
Ok(image)
}
pub fn render_skull_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let head_rx = (width as f32 * (0.18 + identity.unit_f32(17) * 0.07)) as i32;
let head_ry = (height as f32 * (0.18 + identity.unit_f32(18) * 0.07)) as i32;
let layout = FaceLayout {
center_x: width / 2,
center_y: (height as f32 * (0.51 + identity.unit_f32(19) * 0.07)) as i32,
head_rx,
head_ry,
};
let bg = hsl_to_color(
195.0 + identity.unit_f32(15) * 55.0,
0.06 + identity.unit_f32(20) * 0.08,
0.92 + identity.unit_f32(21) * 0.04,
);
let bone = hsl_to_color(
28.0 + identity.unit_f32(16) * 34.0,
0.08 + identity.unit_f32(22) * 0.10,
0.82 + identity.unit_f32(23) * 0.12,
);
let crack = hsl_to_color(
20.0 + identity.unit_f32(24) * 40.0,
0.06,
0.22 + identity.unit_f32(25) * 0.12,
);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
draw_background_accent(
&mut image,
layout.center_x,
layout.center_y,
layout.head_rx,
layout.head_ry,
crack,
0.16,
background,
);
draw_filled_ellipse_mut(
&mut image,
(layout.center_x, layout.center_y),
layout.head_rx,
layout.head_ry,
bone.into(),
);
draw_filled_rect_mut(
&mut image,
Rect::at(
layout.center_x - layout.head_rx / 2,
layout.center_y + layout.head_ry / 2,
)
.of_size(
(layout.head_rx as f32 * (0.82 + identity.unit_f32(26) * 0.34)) as u32,
(layout.head_ry as f32 * (0.34 + identity.unit_f32(27) * 0.28)) as u32,
),
bone.into(),
);
draw_creature_eyes(
&mut image,
layout,
2,
if identity.byte(28).is_multiple_of(2) {
CreatureEyeStyle::Hollow
} else {
CreatureEyeStyle::Tall
},
Color::rgb(44, 42, 44),
Color::rgb(44, 42, 44),
);
let nose_half_width = (layout.head_rx as f32 * (0.08 + identity.unit_f32(29) * 0.08)) as i32;
let nose_height = (layout.head_ry as f32 * (0.12 + identity.unit_f32(30) * 0.12)) as i32;
draw_polygon_mut(
&mut image,
&[
Point::new(layout.center_x, layout.center_y),
Point::new(
layout.center_x - nose_half_width,
layout.center_y + nose_height,
),
Point::new(
layout.center_x + nose_half_width,
layout.center_y + nose_height,
),
],
crack.into(),
);
draw_creature_mouth(
&mut image,
layout,
if identity.byte(31).is_multiple_of(2) {
CreatureMouthStyle::Flat
} else {
CreatureMouthStyle::Smile
},
crack,
);
let tooth_count = 3 + (identity.byte(32) % 4) as i32;
for tooth in 0..tooth_count {
let x = layout.center_x - layout.head_rx / 3
+ tooth * (layout.head_rx * 2 / tooth_count.max(1));
draw_line_segment_mut(
&mut image,
(x as f32, (layout.center_y + layout.head_ry / 2) as f32),
(x as f32, (layout.center_y + layout.head_ry) as f32),
crack.into(),
);
}
let crack_count = 1 + (identity.byte(33) % 3) as i32;
for line in 0..crack_count {
let start_x = layout.center_x - layout.head_rx / 4
+ (identity.byte(34 + line as usize) as i32 % (layout.head_rx / 2).max(1));
let start_y = layout.center_y - layout.head_ry / 2
+ (identity.byte(38 + line as usize) as i32 % (layout.head_ry / 2).max(1));
let end_x = start_x
+ ((identity.unit_f32(42 + line as usize) - 0.5) * layout.head_rx as f32 * 0.45) as i32;
let end_y = start_y
+ (layout.head_ry as f32 * (0.18 + identity.unit_f32(46 + line as usize) * 0.32))
as i32;
draw_line_segment_mut(
&mut image,
(start_x as f32, start_y as f32),
(end_x as f32, end_y as f32),
crack.into(),
);
}
draw_line_segment_mut(
&mut image,
(
(layout.center_x + layout.head_rx / 4) as f32,
(layout.center_y - layout.head_ry / 2) as f32,
),
(
(layout.center_x + layout.head_rx / 8) as f32,
(layout.center_y - layout.head_ry / 8) as f32,
),
crack.into(),
);
Ok(image)
}
/// Render a ringed planet avatar from a stable identity.
pub fn render_planet_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * (0.53 + identity.unit_f32(8) * 0.06)) as i32;
let bg = hsl_to_color(215.0 + identity.unit_f32(0) * 90.0, 0.24, 0.91);
let planet = hsl_to_color(identity.unit_f32(1) * 360.0, 0.46, 0.58);
let shade = hsl_to_color(identity.unit_f32(2) * 360.0, 0.38, 0.42);
let ring = hsl_to_color(32.0 + identity.unit_f32(3) * 120.0, 0.44, 0.72);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
if background == AvatarBackground::Themed {
let star_count = 3 + (identity.byte(4) % 4) as i32;
for star in 0..star_count {
let x = width / 8 + (identity.byte(5 + star as usize) as i32 % (width * 3 / 4).max(1));
let y =
height / 8 + (identity.byte(10 + star as usize) as i32 % (height * 3 / 4).max(1));
draw_filled_circle_mut(
&mut image,
(x, y),
(width as f32 * (0.010 + identity.unit_f32(15 + star as usize) * 0.010)) as i32,
Color::rgba(255, 255, 255, 170).into(),
);
}
} else {
draw_decorative_background(&mut image, background, ring);
}
let radius = (width.min(height) as f32 * (0.18 + identity.unit_f32(20) * 0.08)) as i32;
let ring_rx = (radius as f32 * (1.55 + identity.unit_f32(21) * 0.28)) as i32;
let ring_ry = (radius as f32 * (0.38 + identity.unit_f32(22) * 0.12)) as i32;
let bg_fill = background_fill(background, bg);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y),
ring_rx,
ring_ry,
ring.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y),
(ring_rx as f32 * 0.84) as i32,
(ring_ry as f32 * 0.58) as i32,
bg_fill.into(),
);
draw_filled_circle_mut(&mut image, (center_x, center_y), radius, planet.into());
draw_filled_ellipse_mut(
&mut image,
(center_x - radius / 4, center_y - radius / 5),
radius / 2,
radius / 5,
Color::rgba(shade.0[0], shade.0[1], shade.0[2], 120).into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x + radius / 4, center_y + radius / 5),
radius / 2,
radius / 6,
Color::rgba(255, 255, 255, 80).into(),
);
draw_filled_rect_mut(
&mut image,
Rect::at(center_x - ring_rx, center_y - ring_ry / 5)
.of_size((ring_rx * 2) as u32, (ring_ry / 3).max(1) as u32),
Color::rgba(ring.0[0], ring.0[1], ring.0[2], 190).into(),
);
Ok(image)
}
/// Render a rocket avatar from a stable identity.
pub fn render_rocket_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.52) as i32;
let bg = hsl_to_color(205.0 + identity.unit_f32(0) * 70.0, 0.22, 0.92);
let hull = hsl_to_color(200.0 + identity.unit_f32(1) * 50.0, 0.12, 0.88);
let trim = hsl_to_color(identity.unit_f32(2) * 360.0, 0.58, 0.54);
let window = hsl_to_color(185.0 + identity.unit_f32(3) * 70.0, 0.54, 0.72);
let flame = hsl_to_color(20.0 + identity.unit_f32(4) * 30.0, 0.86, 0.58);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
draw_background_accent(
&mut image,
center_x,
center_y,
width / 5,
height / 5,
trim,
0.18,
background,
);
let body_w = (width as f32 * (0.18 + identity.unit_f32(5) * 0.05)) as i32;
let body_h = (height as f32 * (0.42 + identity.unit_f32(6) * 0.08)) as i32;
let top_y = center_y - body_h / 2;
let bottom_y = center_y + body_h / 2;
draw_polygon_mut(
&mut image,
&[
Point::new(center_x - body_w / 2, top_y + body_w / 2),
Point::new(center_x + body_w / 2, top_y + body_w / 2),
Point::new(center_x, top_y - body_w / 2),
],
trim.into(),
);
draw_filled_rect_mut(
&mut image,
Rect::at(center_x - body_w / 2, top_y + body_w / 2)
.of_size(body_w as u32, (body_h - body_w / 2).max(1) as u32),
hull.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x, top_y + body_w / 2),
body_w / 2,
body_w / 5,
hull.into(),
);
draw_polygon_mut(
&mut image,
&[
Point::new(center_x - body_w / 2, bottom_y - body_w / 2),
Point::new(center_x - body_w, bottom_y + body_w / 3),
Point::new(center_x - body_w / 2, bottom_y),
],
trim.into(),
);
draw_polygon_mut(
&mut image,
&[
Point::new(center_x + body_w / 2, bottom_y - body_w / 2),
Point::new(center_x + body_w, bottom_y + body_w / 3),
Point::new(center_x + body_w / 2, bottom_y),
],
trim.into(),
);
let window_count = 1 + (identity.byte(7) % 2) as i32;
for window_index in 0..window_count {
let y = top_y + body_h / 3 + window_index * body_w;
draw_filled_circle_mut(
&mut image,
(center_x, y),
(body_w as f32 * 0.22) as i32,
trim.into(),
);
draw_filled_circle_mut(
&mut image,
(center_x, y),
(body_w as f32 * 0.15) as i32,
window.into(),
);
}
draw_polygon_mut(
&mut image,
&[
Point::new(center_x - body_w / 4, bottom_y),
Point::new(center_x + body_w / 4, bottom_y),
Point::new(
center_x,
bottom_y + (height as f32 * (0.10 + identity.unit_f32(8) * 0.08)) as i32,
),
],
flame.into(),
);
Ok(image)
}
/// Render a mushroom avatar from a stable identity.
pub fn render_mushroom_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.56) as i32;
let bg = hsl_to_color(18.0 + identity.unit_f32(0) * 35.0, 0.20, 0.93);
let cap = hsl_to_color(350.0 + identity.unit_f32(1) * 45.0, 0.58, 0.52);
let stem = hsl_to_color(35.0 + identity.unit_f32(2) * 20.0, 0.24, 0.86);
let gill = hsl_to_color(26.0 + identity.unit_f32(3) * 20.0, 0.20, 0.70);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
let cap_rx = (width as f32 * (0.24 + identity.unit_f32(4) * 0.08)) as i32;
let cap_ry = (height as f32 * (0.14 + identity.unit_f32(5) * 0.06)) as i32;
let stem_rx = (width as f32 * (0.09 + identity.unit_f32(6) * 0.04)) as i32;
let stem_ry = (height as f32 * (0.18 + identity.unit_f32(7) * 0.05)) as i32;
draw_background_accent(
&mut image, center_x, center_y, cap_rx, cap_ry, gill, 0.24, background,
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y + stem_ry / 3),
stem_rx,
stem_ry,
stem.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y - cap_ry / 2),
cap_rx,
cap_ry,
cap.into(),
);
draw_filled_rect_mut(
&mut image,
Rect::at(center_x - cap_rx, center_y - cap_ry / 2)
.of_size((cap_rx * 2) as u32, cap_ry.max(1) as u32),
cap.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y + cap_ry / 3),
cap_rx,
cap_ry / 3,
gill.into(),
);
let spot_count = 3 + (identity.byte(8) % 4) as i32;
for spot in 0..spot_count {
let sx = center_x - cap_rx / 2 + (identity.byte(9 + spot as usize) as i32 % cap_rx.max(1));
let sy = center_y - cap_ry + (identity.byte(14 + spot as usize) as i32 % cap_ry.max(1));
draw_filled_circle_mut(
&mut image,
(sx, sy),
(cap_rx as f32 * (0.06 + identity.unit_f32(19 + spot as usize) * 0.04)) as i32,
Color::rgba(255, 246, 230, 230).into(),
);
}
Ok(image)
}
/// Render a cactus avatar from a stable identity.
pub fn render_cactus_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.58) as i32;
let bg = hsl_to_color(80.0 + identity.unit_f32(0) * 55.0, 0.20, 0.92);
let cactus = hsl_to_color(105.0 + identity.unit_f32(1) * 60.0, 0.42, 0.42);
let shadow = hsl_to_color(105.0 + identity.unit_f32(2) * 60.0, 0.38, 0.30);
let flower = hsl_to_color(320.0 + identity.unit_f32(3) * 55.0, 0.58, 0.64);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
let body_w = (width as f32 * (0.13 + identity.unit_f32(4) * 0.04)) as i32;
let body_h = (height as f32 * (0.36 + identity.unit_f32(5) * 0.10)) as i32;
let top_y = center_y - body_h / 2;
draw_background_accent(
&mut image,
center_x,
center_y,
body_w * 2,
body_h / 2,
shadow,
0.20,
background,
);
draw_filled_rect_mut(
&mut image,
Rect::at(center_x - body_w / 2, top_y).of_size(body_w as u32, body_h as u32),
cactus.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x, top_y),
body_w / 2,
body_w / 2,
cactus.into(),
);
for side in [-1, 1] {
if side == 1 || identity.byte(6).is_multiple_of(2) {
let arm_y =
center_y - body_h / 5 + side * (identity.byte(7) as i32 % (body_h / 6).max(1));
let arm_len = (width as f32
* (0.11 + identity.unit_f32(8 + side.unsigned_abs() as usize) * 0.05))
as i32;
let arm_x = if side < 0 {
center_x - body_w / 3 - arm_len
} else {
center_x + body_w / 3
};
let cap_x = if side < 0 {
center_x - body_w / 3 - arm_len
} else {
center_x + body_w / 3 + arm_len
};
draw_filled_rect_mut(
&mut image,
Rect::at(arm_x, arm_y - body_w / 4)
.of_size(arm_len.max(2) as u32, (body_w / 2).max(2) as u32),
cactus.into(),
);
draw_filled_ellipse_mut(
&mut image,
(cap_x, arm_y),
body_w / 4,
body_w / 4,
cactus.into(),
);
}
}
for needle in 0..5 {
let y = top_y + body_h / 5 + needle * body_h / 7;
draw_line_segment_mut(
&mut image,
((center_x - body_w / 8) as f32, y as f32),
((center_x - body_w / 4) as f32, (y - body_w / 8) as f32),
Color::rgba(242, 255, 224, 180).into(),
);
draw_line_segment_mut(
&mut image,
((center_x + body_w / 8) as f32, (y + body_w / 12) as f32),
((center_x + body_w / 4) as f32, y as f32),
Color::rgba(242, 255, 224, 180).into(),
);
}
if !identity.byte(12).is_multiple_of(3) {
draw_filled_circle_mut(
&mut image,
(center_x, top_y - body_w / 2),
body_w / 4,
flower.into(),
);
}
Ok(image)
}
/// Render a frog avatar from a stable identity.
pub fn render_frog_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * (0.57 + identity.unit_f32(6) * 0.04)) as i32;
let bg = hsl_to_color(95.0 + identity.unit_f32(0) * 65.0, 0.23, 0.92);
let green = hsl_to_color(92.0 + identity.unit_f32(1) * 72.0, 0.46, 0.54);
let dark = hsl_to_color(98.0 + identity.unit_f32(2) * 60.0, 0.40, 0.28);
let cheek = hsl_to_color(335.0 + identity.unit_f32(3) * 24.0, 0.42, 0.76);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
let head_rx = (width as f32 * (0.24 + identity.unit_f32(4) * 0.06)) as i32;
let head_ry = (height as f32 * (0.18 + identity.unit_f32(5) * 0.05)) as i32;
draw_background_accent(
&mut image, center_x, center_y, head_rx, head_ry, dark, 0.20, background,
);
let eye_offset = (head_rx as f32 * 0.50) as i32;
let eye_r = (head_rx as f32 * (0.18 + identity.unit_f32(7) * 0.04)) as i32;
for side in [-1, 1] {
draw_filled_circle_mut(
&mut image,
(center_x + side * eye_offset, center_y - head_ry),
eye_r,
green.into(),
);
}
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y),
head_rx,
head_ry,
green.into(),
);
for side in [-1, 1] {
let ex = center_x + side * eye_offset;
let ey = center_y - head_ry;
draw_filled_circle_mut(
&mut image,
(ex, ey),
(eye_r as f32 * 0.64) as i32,
Color::rgb(255, 255, 245).into(),
);
draw_filled_circle_mut(
&mut image,
(ex, ey),
(eye_r as f32 * 0.30) as i32,
dark.into(),
);
draw_filled_circle_mut(
&mut image,
(center_x + side * head_rx / 2, center_y + head_ry / 4),
head_rx / 9,
Color::rgba(cheek.0[0], cheek.0[1], cheek.0[2], 150).into(),
);
}
draw_smile_arc(
&mut image,
center_x - head_rx / 9,
center_y + head_ry / 4,
head_rx / 4,
dark,
0.50,
);
draw_smile_arc(
&mut image,
center_x + head_rx / 9,
center_y + head_ry / 4,
head_rx / 4,
dark,
0.50,
);
if identity.byte(8).is_multiple_of(2) {
draw_line_segment_mut(
&mut image,
(
(center_x - head_rx / 10) as f32,
(center_y + head_ry / 6) as f32,
),
(
(center_x - head_rx / 10) as f32,
(center_y + head_ry / 4) as f32,
),
dark.into(),
);
draw_line_segment_mut(
&mut image,
(
(center_x + head_rx / 10) as f32,
(center_y + head_ry / 6) as f32,
),
(
(center_x + head_rx / 10) as f32,
(center_y + head_ry / 4) as f32,
),
dark.into(),
);
}
Ok(image)
}
/// Render a panda avatar from a stable identity.
pub fn render_panda_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * (0.56 + identity.unit_f32(5) * 0.04)) as i32;
let bg = hsl_to_color(200.0 + identity.unit_f32(0) * 45.0, 0.08, 0.94);
let white = hsl_to_color(36.0 + identity.unit_f32(1) * 18.0, 0.10, 0.92);
let black = hsl_to_color(210.0 + identity.unit_f32(2) * 28.0, 0.10, 0.18);
let blush = hsl_to_color(345.0 + identity.unit_f32(3) * 25.0, 0.32, 0.78);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
let head_rx = (width as f32 * (0.24 + identity.unit_f32(6) * 0.05)) as i32;
let head_ry = (height as f32 * (0.22 + identity.unit_f32(7) * 0.04)) as i32;
let ear_r = (head_rx as f32 * (0.28 + identity.unit_f32(8) * 0.08)) as i32;
draw_background_accent(
&mut image, center_x, center_y, head_rx, head_ry, black, 0.16, background,
);
for side in [-1, 1] {
draw_filled_circle_mut(
&mut image,
(
center_x + side * head_rx * 3 / 4,
center_y - head_ry * 3 / 4,
),
ear_r,
black.into(),
);
}
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y),
head_rx,
head_ry,
white.into(),
);
for side in [-1, 1] {
let patch_x = center_x + side * head_rx / 3;
let patch_y = center_y - head_ry / 8;
draw_filled_ellipse_mut(
&mut image,
(patch_x, patch_y),
(head_rx as f32 * (0.20 + identity.unit_f32(9) * 0.05)) as i32,
(head_ry as f32 * (0.26 + identity.unit_f32(10) * 0.05)) as i32,
black.into(),
);
draw_filled_circle_mut(
&mut image,
(patch_x, patch_y),
(head_rx as f32 * 0.055) as i32,
Color::rgb(248, 248, 244).into(),
);
if identity.byte(11).is_multiple_of(2) {
draw_filled_circle_mut(
&mut image,
(center_x + side * head_rx / 2, center_y + head_ry / 4),
head_rx / 10,
Color::rgba(blush.0[0], blush.0[1], blush.0[2], 120).into(),
);
}
}
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y + head_ry / 5),
head_rx / 9,
head_ry / 12,
black.into(),
);
draw_smile_arc(
&mut image,
center_x - head_rx / 12,
center_y + head_ry / 4,
head_rx / 6,
black,
0.45,
);
draw_smile_arc(
&mut image,
center_x + head_rx / 12,
center_y + head_ry / 4,
head_rx / 6,
black,
0.45,
);
Ok(image)
}
/// Render a cupcake avatar from a stable identity.
pub fn render_cupcake_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.58) as i32;
let bg = hsl_to_color(320.0 + identity.unit_f32(0) * 45.0, 0.22, 0.94);
let wrapper = hsl_to_color(28.0 + identity.unit_f32(1) * 35.0, 0.46, 0.62);
let frosting = hsl_to_color(identity.unit_f32(2) * 360.0, 0.38, 0.78);
let shadow = hsl_to_color(330.0 + identity.unit_f32(3) * 45.0, 0.28, 0.58);
let cherry = hsl_to_color(345.0 + identity.unit_f32(4) * 22.0, 0.66, 0.50);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
let cup_w = (width as f32 * (0.26 + identity.unit_f32(5) * 0.07)) as i32;
let cup_h = (height as f32 * (0.22 + identity.unit_f32(6) * 0.05)) as i32;
let frosting_rx = (cup_w as f32 * (0.58 + identity.unit_f32(7) * 0.10)) as i32;
let frosting_ry = (height as f32 * (0.13 + identity.unit_f32(8) * 0.04)) as i32;
draw_background_accent(
&mut image,
center_x,
center_y,
cup_w / 2,
cup_h,
shadow,
0.24,
background,
);
draw_polygon_mut(
&mut image,
&[
Point::new(center_x - cup_w / 2, center_y),
Point::new(center_x + cup_w / 2, center_y),
Point::new(center_x + cup_w / 3, center_y + cup_h),
Point::new(center_x - cup_w / 3, center_y + cup_h),
],
wrapper.into(),
);
for stripe in [-2, 0, 2] {
let x = center_x + stripe * cup_w / 10;
draw_line_segment_mut(
&mut image,
(x as f32, center_y as f32),
((x - stripe * cup_w / 40) as f32, (center_y + cup_h) as f32),
Color::rgba(255, 244, 214, 115).into(),
);
}
let base_y = center_y - frosting_ry / 2;
for layer in 0..3 {
let y = base_y - layer * frosting_ry / 2;
let rx = (frosting_rx as f32 * (1.0 - layer as f32 * 0.22)) as i32;
let ry = (frosting_ry as f32 * (0.82 - layer as f32 * 0.10)) as i32;
draw_filled_ellipse_mut(&mut image, (center_x, y), rx, ry, frosting.into());
}
let sprinkle_count = 3 + (identity.byte(9) % 5) as i32;
for sprinkle in 0..sprinkle_count {
let sx = center_x - frosting_rx / 2
+ (identity.byte(10 + sprinkle as usize) as i32 % frosting_rx.max(1));
let sy = base_y - frosting_ry
+ (identity.byte(16 + sprinkle as usize) as i32 % (frosting_ry * 2).max(1));
let color = hsl_to_color(
identity.unit_f32(23 + sprinkle as usize) * 360.0,
0.62,
0.55,
);
draw_filled_rect_mut(
&mut image,
Rect::at(sx, sy).of_size((width / 40).max(2) as u32, (height / 80).max(2) as u32),
color.into(),
);
}
if !identity.byte(30).is_multiple_of(3) {
draw_filled_circle_mut(
&mut image,
(center_x, base_y - frosting_ry),
(width as f32 * 0.035) as i32,
cherry.into(),
);
}
Ok(image)
}
/// Render a pizza-slice avatar from a stable identity.
pub fn render_pizza_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.53) as i32;
let bg = hsl_to_color(36.0 + identity.unit_f32(0) * 30.0, 0.24, 0.93);
let crust = hsl_to_color(30.0 + identity.unit_f32(1) * 28.0, 0.54, 0.58);
let cheese = hsl_to_color(45.0 + identity.unit_f32(2) * 16.0, 0.74, 0.70);
let sauce = hsl_to_color(8.0 + identity.unit_f32(3) * 16.0, 0.62, 0.48);
let topping = hsl_to_color(350.0 + identity.unit_f32(4) * 22.0, 0.54, 0.46);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
let half_w = (width as f32 * (0.22 + identity.unit_f32(5) * 0.06)) as i32;
let slice_h = (height as f32 * (0.44 + identity.unit_f32(6) * 0.07)) as i32;
let top_y = center_y - slice_h / 2;
let tip_y = center_y + slice_h / 2;
draw_background_accent(
&mut image,
center_x,
center_y,
half_w,
slice_h / 2,
sauce,
0.16,
background,
);
draw_polygon_mut(
&mut image,
&[
Point::new(center_x - half_w, top_y),
Point::new(center_x + half_w, top_y),
Point::new(center_x, tip_y),
],
cheese.into(),
);
draw_line_segment_mut(
&mut image,
((center_x - half_w) as f32, top_y as f32),
((center_x + half_w) as f32, top_y as f32),
crust.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x, top_y),
half_w,
(height as f32 * 0.035) as i32,
crust.into(),
);
draw_polygon_mut(
&mut image,
&[
Point::new(center_x - half_w + half_w / 6, top_y + slice_h / 5),
Point::new(center_x + half_w - half_w / 6, top_y + slice_h / 5),
Point::new(center_x, tip_y - slice_h / 10),
],
Color::rgba(sauce.0[0], sauce.0[1], sauce.0[2], 95).into(),
);
let topping_count = 3 + (identity.byte(7) % 4) as i32;
for item in 0..topping_count {
let y = top_y + slice_h / 5 + item * slice_h / (topping_count + 2);
let span = half_w - (y - top_y) * half_w / slice_h;
let x = center_x - span / 2 + (identity.byte(8 + item as usize) as i32 % span.max(1));
draw_filled_circle_mut(
&mut image,
(x, y),
(width as f32 * (0.025 + identity.unit_f32(14 + item as usize) * 0.012)) as i32,
topping.into(),
);
}
Ok(image)
}
/// Render an ice cream cone avatar from a stable identity.
pub fn render_icecream_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.55) as i32;
let bg = hsl_to_color(190.0 + identity.unit_f32(0) * 95.0, 0.18, 0.94);
let scoop = hsl_to_color(identity.unit_f32(1) * 360.0, 0.42, 0.76);
let cone = hsl_to_color(32.0 + identity.unit_f32(2) * 22.0, 0.50, 0.64);
let waffle = hsl_to_color(28.0 + identity.unit_f32(3) * 22.0, 0.42, 0.45);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
let scoop_r = (width as f32 * (0.18 + identity.unit_f32(4) * 0.06)) as i32;
let cone_w = (width as f32 * (0.24 + identity.unit_f32(5) * 0.05)) as i32;
let cone_h = (height as f32 * (0.32 + identity.unit_f32(6) * 0.06)) as i32;
let scoop_y = center_y - scoop_r / 2;
let cone_top_y = scoop_y + scoop_r / 2;
draw_background_accent(
&mut image,
center_x,
center_y,
scoop_r,
cone_h / 2,
waffle,
0.18,
background,
);
draw_polygon_mut(
&mut image,
&[
Point::new(center_x - cone_w / 2, cone_top_y),
Point::new(center_x + cone_w / 2, cone_top_y),
Point::new(center_x, cone_top_y + cone_h),
],
cone.into(),
);
for line in [-1, 1] {
draw_line_segment_mut(
&mut image,
(
(center_x + line * cone_w / 3) as f32,
(cone_top_y + cone_h / 8) as f32,
),
(center_x as f32, (cone_top_y + cone_h * 3 / 4) as f32),
waffle.into(),
);
}
draw_filled_circle_mut(&mut image, (center_x, scoop_y), scoop_r, scoop.into());
if identity.byte(7).is_multiple_of(2) {
draw_filled_circle_mut(
&mut image,
(center_x - scoop_r / 2, scoop_y + scoop_r / 3),
scoop_r / 5,
scoop.into(),
);
draw_filled_circle_mut(
&mut image,
(center_x + scoop_r / 3, scoop_y + scoop_r / 2),
scoop_r / 6,
scoop.into(),
);
}
let chip_count = 2 + (identity.byte(8) % 4) as i32;
for chip in 0..chip_count {
let x = center_x - scoop_r / 2 + (identity.byte(9 + chip as usize) as i32 % scoop_r.max(1));
let y = scoop_y - scoop_r / 3 + (identity.byte(14 + chip as usize) as i32 % scoop_r.max(1));
draw_filled_circle_mut(
&mut image,
(x, y),
(width as f32 * 0.010).max(2.0) as i32,
waffle.into(),
);
}
Ok(image)
}
/// Render an octopus avatar from a stable identity.
pub fn render_octopus_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * (0.54 + identity.unit_f32(5) * 0.05)) as i32;
let bg = hsl_to_color(185.0 + identity.unit_f32(0) * 70.0, 0.22, 0.92);
let body = hsl_to_color(identity.unit_f32(1) * 360.0, 0.42, 0.58);
let shade = hsl_to_color(identity.unit_f32(2) * 360.0, 0.34, 0.38);
let eye = Color::rgb(28, 26, 38);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
let head_rx = (width as f32 * (0.21 + identity.unit_f32(3) * 0.06)) as i32;
let head_ry = (height as f32 * (0.20 + identity.unit_f32(4) * 0.06)) as i32;
draw_background_accent(
&mut image, center_x, center_y, head_rx, head_ry, shade, 0.22, background,
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y),
head_rx,
head_ry,
body.into(),
);
let tentacles = 4 + (identity.byte(6) % 3) as i32;
for index in 0..tentacles {
let denominator = (tentacles - 1).max(1);
let x = center_x - head_rx + index * (head_rx * 2 / denominator);
let length =
(head_ry as f32 * (0.42 + identity.unit_f32(7 + index as usize) * 0.35)) as i32;
draw_filled_rect_mut(
&mut image,
Rect::at(x - head_rx / 12, center_y + head_ry / 2)
.of_size((head_rx / 6).max(2) as u32, length.max(2) as u32),
body.into(),
);
draw_filled_circle_mut(
&mut image,
(x, center_y + head_ry / 2 + length),
(head_rx / 10).max(3),
body.into(),
);
}
for side in [-1, 1] {
let ex = center_x + side * head_rx / 3;
let ey = center_y - head_ry / 6;
draw_filled_circle_mut(
&mut image,
(ex, ey),
head_rx / 9,
Color::rgb(255, 255, 248).into(),
);
draw_filled_circle_mut(&mut image, (ex, ey), head_rx / 20, eye.into());
}
let mouth = match identity.byte(14) % 3 {
0 => CreatureMouthStyle::Smile,
1 => CreatureMouthStyle::Flat,
_ => CreatureMouthStyle::Fang,
};
draw_creature_mouth(
&mut image,
FaceLayout {
center_x,
center_y,
head_rx,
head_ry,
},
mouth,
shade,
);
Ok(image)
}
/// Render a knight helmet avatar from a stable identity.
pub fn render_knight_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.55) as i32;
let bg = hsl_to_color(215.0 + identity.unit_f32(0) * 30.0, 0.12, 0.92);
let steel = hsl_to_color(205.0 + identity.unit_f32(1) * 45.0, 0.12, 0.66);
let dark = hsl_to_color(215.0 + identity.unit_f32(2) * 45.0, 0.14, 0.22);
let plume = hsl_to_color(identity.unit_f32(3) * 360.0, 0.58, 0.54);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
let helm_rx = (width as f32 * (0.20 + identity.unit_f32(4) * 0.05)) as i32;
let helm_ry = (height as f32 * (0.24 + identity.unit_f32(5) * 0.05)) as i32;
draw_background_accent(
&mut image, center_x, center_y, helm_rx, helm_ry, plume, 0.18, background,
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y - helm_ry / 5),
helm_rx,
helm_ry,
steel.into(),
);
draw_filled_rect_mut(
&mut image,
Rect::at(center_x - helm_rx, center_y - helm_ry / 5)
.of_size((helm_rx * 2) as u32, (helm_ry * 6 / 5) as u32),
steel.into(),
);
draw_filled_rect_mut(
&mut image,
Rect::at(center_x - helm_rx * 3 / 4, center_y - helm_ry / 5)
.of_size((helm_rx * 3 / 2) as u32, (helm_ry / 5).max(2) as u32),
dark.into(),
);
let slit_count = 2 + (identity.byte(6) % 3) as i32;
for slit in 0..slit_count {
let x = center_x - helm_rx / 2 + slit * helm_rx / slit_count.max(1);
draw_filled_rect_mut(
&mut image,
Rect::at(x, center_y - helm_ry / 5)
.of_size((helm_rx / 10).max(2) as u32, (helm_ry / 5).max(2) as u32),
Color::rgba(255, 255, 255, 90).into(),
);
}
draw_line_segment_mut(
&mut image,
(center_x as f32, (center_y - helm_ry) as f32),
(center_x as f32, (center_y + helm_ry) as f32),
Color::rgba(255, 255, 255, 130).into(),
);
if !identity.byte(7).is_multiple_of(3) {
draw_polygon_mut(
&mut image,
&[
Point::new(center_x, center_y - helm_ry),
Point::new(center_x - helm_rx / 5, center_y - helm_ry - helm_ry / 2),
Point::new(center_x + helm_rx / 4, center_y - helm_ry - helm_ry / 3),
],
plume.into(),
);
}
Ok(image)
}
pub fn render_bear_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.56) as i32;
let head_rx = (width as f32 * (0.27 + identity.unit_f32(4) * 0.05)) as i32;
let head_ry = (height as f32 * (0.24 + identity.unit_f32(5) * 0.05)) as i32;
let fur = hsl_to_color(24.0 + identity.unit_f32(1) * 24.0, 0.38, 0.48);
let muzzle = hsl_to_color(32.0 + identity.unit_f32(2) * 12.0, 0.22, 0.84);
let inner = hsl_to_color(18.0 + identity.unit_f32(3) * 18.0, 0.34, 0.72);
let bg = hsl_to_color(34.0 + identity.unit_f32(6) * 24.0, 0.18, 0.93);
let dark = Color::rgb(45, 34, 28);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
draw_background_accent(
&mut image, center_x, center_y, head_rx, head_ry, inner, 0.42, background,
);
let ear_r = (head_rx as f32 * 0.28) as i32;
for x in [center_x - head_rx * 3 / 4, center_x + head_rx * 3 / 4] {
draw_filled_circle_mut(&mut image, (x, center_y - head_ry), ear_r, fur.into());
draw_filled_circle_mut(&mut image, (x, center_y - head_ry), ear_r / 2, inner.into());
}
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y),
head_rx,
head_ry,
fur.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y + head_ry / 4),
head_rx * 2 / 5,
head_ry / 3,
muzzle.into(),
);
let eye_y = center_y - head_ry / 5;
for x in [center_x - head_rx / 3, center_x + head_rx / 3] {
draw_filled_circle_mut(&mut image, (x, eye_y), (head_rx / 10).max(3), dark.into());
}
let nose_y = center_y + head_ry / 6;
draw_filled_ellipse_mut(
&mut image,
(center_x, nose_y),
head_rx / 8,
head_ry / 10,
dark.into(),
);
draw_smile_arc(
&mut image,
center_x,
nose_y + head_ry / 12,
head_rx / 5,
dark,
0.35,
);
Ok(image)
}
pub fn render_penguin_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.56) as i32;
let body_rx = (width as f32 * 0.25) as i32;
let body_ry = (height as f32 * 0.34) as i32;
let black = hsl_to_color(210.0 + identity.unit_f32(1) * 30.0, 0.22, 0.18);
let white = hsl_to_color(205.0 + identity.unit_f32(2) * 25.0, 0.16, 0.94);
let orange = hsl_to_color(32.0 + identity.unit_f32(3) * 18.0, 0.72, 0.58);
let bg = hsl_to_color(190.0 + identity.unit_f32(4) * 35.0, 0.22, 0.93);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
draw_background_accent(
&mut image, center_x, center_y, body_rx, body_ry, orange, 0.36, background,
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y),
body_rx,
body_ry,
black.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y + body_ry / 6),
body_rx * 3 / 5,
body_ry * 2 / 3,
white.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x - body_rx, center_y + body_ry / 10),
body_rx / 4,
body_ry / 2,
black.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x + body_rx, center_y + body_ry / 10),
body_rx / 4,
body_ry / 2,
black.into(),
);
let eye_y = center_y - body_ry / 3;
for x in [center_x - body_rx / 3, center_x + body_rx / 3] {
draw_filled_circle_mut(
&mut image,
(x, eye_y),
(body_rx / 10).max(3),
Color::rgb(10, 15, 20).into(),
);
}
draw_polygon_mut(
&mut image,
&[
Point::new(center_x - body_rx / 7, center_y - body_ry / 6),
Point::new(center_x + body_rx / 7, center_y - body_ry / 6),
Point::new(center_x, center_y),
],
orange.into(),
);
for x in [center_x - body_rx / 3, center_x + body_rx / 3] {
draw_filled_ellipse_mut(
&mut image,
(x, center_y + body_ry),
body_rx / 4,
body_ry / 10,
orange.into(),
);
}
Ok(image)
}
pub fn render_dragon_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.57) as i32;
let head_rx = (width as f32 * 0.27) as i32;
let head_ry = (height as f32 * 0.23) as i32;
let scale = hsl_to_color(105.0 + identity.unit_f32(1) * 70.0, 0.46, 0.46);
let belly = hsl_to_color(70.0 + identity.unit_f32(2) * 35.0, 0.42, 0.72);
let horn = hsl_to_color(40.0 + identity.unit_f32(3) * 20.0, 0.34, 0.84);
let flame = hsl_to_color(14.0 + identity.unit_f32(4) * 25.0, 0.78, 0.56);
let bg = hsl_to_color(120.0 + identity.unit_f32(5) * 45.0, 0.18, 0.92);
let dark = Color::rgb(24, 48, 34);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
draw_background_accent(
&mut image, center_x, center_y, head_rx, head_ry, flame, 0.40, background,
);
for side in [-1, 1] {
let horn_points = [
Point::new(center_x + side * head_rx / 2, center_y - head_ry),
Point::new(center_x + side * head_rx / 4, center_y - head_ry * 8 / 5),
Point::new(center_x + side * head_rx / 8, center_y - head_ry),
];
draw_polygon_mut(&mut image, &horn_points, horn.into());
}
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y),
head_rx,
head_ry,
scale.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y + head_ry / 4),
head_rx / 2,
head_ry / 3,
belly.into(),
);
for x in [center_x - head_rx / 3, center_x + head_rx / 3] {
draw_filled_circle_mut(
&mut image,
(x, center_y - head_ry / 5),
(head_rx / 10).max(3),
Color::rgb(255, 255, 255).into(),
);
draw_filled_circle_mut(
&mut image,
(x, center_y - head_ry / 5),
(head_rx / 20).max(2),
dark.into(),
);
}
for x in [center_x - head_rx / 7, center_x + head_rx / 7] {
draw_filled_circle_mut(
&mut image,
(x, center_y + head_ry / 3),
(head_rx / 24).max(2),
dark.into(),
);
}
for offset in [-1, 0, 1] {
let x = center_x + offset * head_rx / 5;
let spike_y = center_y - head_ry;
let spike = [
Point::new(x - head_rx / 14, spike_y),
Point::new(x, spike_y - head_ry / 4),
Point::new(x + head_rx / 14, spike_y),
];
draw_polygon_mut(&mut image, &spike, flame.into());
}
Ok(image)
}
pub fn render_ninja_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.56) as i32;
let head_r = (width.min(height) as f32 * 0.28) as i32;
let cloth = hsl_to_color(220.0 + identity.unit_f32(1) * 50.0, 0.18, 0.14);
let skin = hsl_to_color(28.0 + identity.unit_f32(2) * 18.0, 0.42, 0.72);
let band = hsl_to_color(identity.unit_f32(3) * 360.0, 0.56, 0.50);
let bg = hsl_to_color(225.0 + identity.unit_f32(4) * 30.0, 0.13, 0.92);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
draw_background_accent(
&mut image, center_x, center_y, head_r, head_r, band, 0.38, background,
);
draw_filled_circle_mut(&mut image, (center_x, center_y), head_r, cloth.into());
draw_filled_rect_mut(
&mut image,
Rect::at(center_x - head_r * 3 / 5, center_y - head_r / 4)
.of_size((head_r * 6 / 5) as u32, (head_r / 2) as u32),
skin.into(),
);
draw_filled_rect_mut(
&mut image,
Rect::at(center_x - head_r, center_y - head_r * 2 / 3)
.of_size((head_r * 2) as u32, (head_r / 6).max(2) as u32),
band.into(),
);
for x in [center_x - head_r / 3, center_x + head_r / 3] {
draw_filled_ellipse_mut(
&mut image,
(x, center_y - head_r / 12),
head_r / 9,
head_r / 14,
Color::rgb(20, 24, 32).into(),
);
}
let tie = [
Point::new(center_x + head_r * 4 / 5, center_y - head_r * 2 / 3),
Point::new(center_x + head_r * 7 / 5, center_y - head_r),
Point::new(center_x + head_r, center_y - head_r / 3),
];
draw_polygon_mut(&mut image, &tie, band.into());
Ok(image)
}
pub fn render_astronaut_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.54) as i32;
let helmet_r = (width.min(height) as f32 * 0.28) as i32;
let suit = hsl_to_color(205.0 + identity.unit_f32(1) * 35.0, 0.16, 0.90);
let visor = hsl_to_color(195.0 + identity.unit_f32(2) * 55.0, 0.52, 0.56);
let trim = hsl_to_color(identity.unit_f32(3) * 360.0, 0.45, 0.55);
let bg = hsl_to_color(220.0 + identity.unit_f32(4) * 60.0, 0.18, 0.91);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
draw_background_accent(
&mut image, center_x, center_y, helmet_r, helmet_r, trim, 0.40, background,
);
draw_filled_rect_mut(
&mut image,
Rect::at(center_x - helmet_r / 2, center_y + helmet_r / 2)
.of_size(helmet_r as u32, (helmet_r * 3 / 5) as u32),
suit.into(),
);
draw_filled_circle_mut(&mut image, (center_x, center_y), helmet_r, suit.into());
draw_hollow_circle_mut(
&mut image,
(center_x, center_y),
helmet_r,
Color::rgb(96, 110, 128).into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y),
helmet_r * 2 / 3,
helmet_r / 2,
visor.into(),
);
draw_blended_rect_mut(
&mut image,
Rect::at(center_x - helmet_r / 3, center_y - helmet_r / 4)
.of_size((helmet_r / 2).max(2) as u32, (helmet_r / 8).max(2) as u32),
Rgba([255, 255, 255, 90]),
);
draw_filled_circle_mut(
&mut image,
(center_x + helmet_r / 2, center_y + helmet_r * 2 / 3),
(helmet_r / 10).max(3),
trim.into(),
);
Ok(image)
}
pub fn render_diamond_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.56) as i32;
let rx = (width as f32 * 0.25) as i32;
let ry = (height as f32 * 0.30) as i32;
let gem = hsl_to_color(180.0 + identity.unit_f32(1) * 95.0, 0.55, 0.60);
let highlight = hsl_to_color(190.0 + identity.unit_f32(2) * 70.0, 0.40, 0.82);
let shade = hsl_to_color(200.0 + identity.unit_f32(3) * 70.0, 0.42, 0.42);
let bg = hsl_to_color(200.0 + identity.unit_f32(4) * 50.0, 0.18, 0.94);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
draw_background_accent(
&mut image, center_x, center_y, rx, ry, highlight, 0.32, background,
);
let outer = [
Point::new(center_x - rx, center_y - ry / 3),
Point::new(center_x - rx / 2, center_y - ry),
Point::new(center_x + rx / 2, center_y - ry),
Point::new(center_x + rx, center_y - ry / 3),
Point::new(center_x, center_y + ry),
];
draw_polygon_mut(&mut image, &outer, gem.into());
draw_polygon_mut(
&mut image,
&[
outer[0],
outer[1],
Point::new(center_x, center_y + ry),
Point::new(center_x - rx / 5, center_y - ry / 3),
],
highlight.into(),
);
draw_polygon_mut(
&mut image,
&[
outer[2],
outer[3],
Point::new(center_x, center_y + ry),
Point::new(center_x + rx / 5, center_y - ry / 3),
],
shade.into(),
);
for x in [center_x - rx / 2, center_x, center_x + rx / 2] {
draw_line_segment_mut(
&mut image,
(x as f32, (center_y - ry) as f32),
(center_x as f32, (center_y + ry) as f32),
Color::rgba(255, 255, 255, 110).into(),
);
}
Ok(image)
}
pub fn render_coffee_cup_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.58) as i32;
let cup_w = (width as f32 * 0.38) as i32;
let cup_h = (height as f32 * 0.32) as i32;
let cup = hsl_to_color(20.0 + identity.unit_f32(1) * 35.0, 0.42, 0.60);
let coffee = hsl_to_color(24.0 + identity.unit_f32(2) * 18.0, 0.42, 0.26);
let cream = hsl_to_color(38.0 + identity.unit_f32(3) * 18.0, 0.26, 0.88);
let bg = hsl_to_color(34.0 + identity.unit_f32(4) * 20.0, 0.18, 0.93);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
draw_background_accent(
&mut image,
center_x,
center_y,
cup_w / 2,
cup_h / 2,
cream,
0.30,
background,
);
draw_filled_rect_mut(
&mut image,
Rect::at(center_x - cup_w / 2, center_y - cup_h / 2).of_size(cup_w as u32, cup_h as u32),
cup.into(),
);
draw_filled_ellipse_mut(
&mut image,
(center_x, center_y - cup_h / 2),
cup_w / 2,
cup_h / 7,
coffee.into(),
);
draw_hollow_ellipse_mut(
&mut image,
(center_x + cup_w / 2, center_y - cup_h / 10),
cup_w / 4,
cup_h / 4,
cup.into(),
);
draw_filled_rect_mut(
&mut image,
Rect::at(center_x - cup_w * 3 / 5, center_y + cup_h / 2)
.of_size((cup_w * 6 / 5) as u32, (cup_h / 8).max(2) as u32),
Color::rgba(80, 55, 42, 180).into(),
);
for offset in [-1, 0, 1] {
let x = center_x + offset * cup_w / 5;
draw_line_segment_mut(
&mut image,
(x as f32, (center_y - cup_h) as f32),
((x + cup_w / 10) as f32, (center_y - cup_h * 4 / 3) as f32),
Color::rgba(120, 98, 82, 120).into(),
);
}
Ok(image)
}
pub fn render_shield_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let center_x = width / 2;
let center_y = (height as f32 * 0.55) as i32;
let rx = (width as f32 * 0.25) as i32;
let ry = (height as f32 * 0.32) as i32;
let metal = hsl_to_color(210.0 + identity.unit_f32(1) * 45.0, 0.28, 0.58);
let accent = hsl_to_color(identity.unit_f32(2) * 360.0, 0.50, 0.50);
let light = hsl_to_color(210.0 + identity.unit_f32(3) * 35.0, 0.18, 0.82);
let bg = hsl_to_color(215.0 + identity.unit_f32(4) * 35.0, 0.16, 0.92);
let mut image = ImageBuffer::from_pixel(
spec.width,
spec.height,
background_fill(background, bg).into(),
);
draw_background_accent(
&mut image, center_x, center_y, rx, ry, accent, 0.36, background,
);
let shield = [
Point::new(center_x - rx, center_y - ry),
Point::new(center_x + rx, center_y - ry),
Point::new(center_x + rx * 4 / 5, center_y + ry / 4),
Point::new(center_x, center_y + ry),
Point::new(center_x - rx * 4 / 5, center_y + ry / 4),
];
draw_polygon_mut(&mut image, &shield, metal.into());
draw_polygon_mut(
&mut image,
&[
Point::new(center_x - rx, center_y - ry),
Point::new(center_x, center_y - ry),
Point::new(center_x, center_y + ry),
Point::new(center_x - rx * 4 / 5, center_y + ry / 4),
],
light.into(),
);
draw_filled_rect_mut(
&mut image,
Rect::at(center_x - rx / 8, center_y - ry * 3 / 4)
.of_size((rx / 4).max(2) as u32, (ry * 6 / 5) as u32),
accent.into(),
);
draw_filled_rect_mut(
&mut image,
Rect::at(center_x - rx * 2 / 3, center_y - ry / 5)
.of_size((rx * 4 / 3) as u32, (ry / 4).max(2) as u32),
accent.into(),
);
Ok(image)
}
pub fn render_paws_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> Result<RgbaImage, AvatarSpecError> {
spec.validate()?;
let width = spec.width as i32;
let height = spec.height as i32;
let mut image =
ImageBuffer::from_pixel(spec.width, spec.height, Color::rgb(255, 255, 255).into());
let bg = hsl_to_color(24.0 + identity.unit_f32(0) * 36.0, 0.20, 0.94);
let fur = hsl_to_color(
identity.unit_f32(1) * 360.0,
0.32 + identity.unit_f32(2) * 0.18,
0.60,
);
let pad = hsl_to_color(
330.0 + identity.unit_f32(3) * 20.0,
0.36 + identity.unit_f32(4) * 0.18,
0.72,
);
let accent = hsl_to_color(18.0 + identity.unit_f32(5) * 24.0, 0.34, 0.82);
image
.pixels_mut()
.for_each(|pixel| *pixel = background_fill(background, bg).into());
if background == AvatarBackground::Themed {
for stripe in 0..4 {
let y = (height / 8) + stripe * (height / 5);
draw_filled_rect_mut(
&mut image,
Rect::at(0, y).of_size(spec.width, (height / 18).max(1) as u32),
Color::rgba(accent.0[0], accent.0[1], accent.0[2], 70).into(),
);
}
} else {
draw_decorative_background(&mut image, background, accent);
}
let primary_x = width / 2;
let primary_y = height / 2 + height / 12;
let palm_rx = (width as f32 * (0.14 + identity.unit_f32(6) * 0.04)) as i32;
let palm_ry = (height as f32 * (0.16 + identity.unit_f32(7) * 0.04)) as i32;
draw_paw_print(
&mut image,
primary_x,
primary_y,
palm_rx,
palm_ry,
fur,
pad,
identity.byte(8),
);
if identity.byte(9).is_multiple_of(2) {
draw_paw_print(
&mut image,
width / 3,
height / 3,
(palm_rx as f32 * 0.82) as i32,
(palm_ry as f32 * 0.82) as i32,
hsl_to_color(identity.unit_f32(10) * 360.0, 0.28, 0.66),
pad,
identity.byte(11),
);
}
if !identity.byte(12).is_multiple_of(3) {
draw_paw_print(
&mut image,
width * 2 / 3,
height / 3 + height / 8,
(palm_rx as f32 * 0.70) as i32,
(palm_ry as f32 * 0.70) as i32,
fur,
hsl_to_color(340.0 + identity.unit_f32(13) * 12.0, 0.30, 0.80),
identity.byte(14),
);
}
Ok(image)
}
#[allow(clippy::too_many_arguments)]
fn draw_paw_print(
image: &mut RgbaImage,
center_x: i32,
center_y: i32,
palm_rx: i32,
palm_ry: i32,
fur: Color,
pad: Color,
shape_seed: u8,
) {
let toe_offset_y = palm_ry;
let toe_spacing = (palm_rx as f32 * (0.48 + (shape_seed as f32 / 255.0) * 0.12)) as i32;
let toe_rx = (palm_rx as f32 * (0.26 + (shape_seed as f32 / 255.0) * 0.04)) as i32;
let toe_ry = (palm_ry as f32 * (0.24 + ((shape_seed >> 2) as f32 / 255.0) * 0.06)) as i32;
draw_filled_ellipse_mut(image, (center_x, center_y), palm_rx, palm_ry, fur.into());
draw_filled_ellipse_mut(
image,
(center_x, center_y + palm_ry / 8),
(palm_rx as f32 * 0.72) as i32,
(palm_ry as f32 * 0.68) as i32,
pad.into(),
);
for (index, offset) in [-3, -1, 1, 3].into_iter().enumerate() {
let x = center_x + offset * toe_spacing / 4;
let y = center_y - toe_offset_y + if index % 2 == 0 { 0 } else { toe_ry / 3 };
draw_filled_ellipse_mut(image, (x, y), toe_rx, toe_ry, fur.into());
draw_filled_ellipse_mut(
image,
(x, y + toe_ry / 5),
(toe_rx as f32 * 0.68) as i32,
(toe_ry as f32 * 0.68) as i32,
pad.into(),
);
}
}
#[derive(Clone, Copy, Debug)]
struct CatPalette {
background: Color,
accent: Color,
head: Color,
ear_inner: Color,
muzzle: Color,
eye: Color,
pupil: Color,
nose: Color,
outline: Color,
marking: Color,
}
impl CatPalette {
fn from_genome(genome: &CatGenome) -> Self {
let hue = genome.base_hue;
let head = hsl_to_color(
hue,
0.42 + genome.head_saturation * 0.25,
0.55 + genome.head_lightness * 0.16,
);
let background = hsl_to_color(
(hue + 180.0 + genome.background_shift * 40.0) % 360.0,
0.25 + genome.background_sat * 0.20,
0.90,
);
let accent = hsl_to_color(
(hue + 18.0 + genome.accent_shift * 60.0) % 360.0,
0.34 + genome.accent_sat * 0.20,
0.80,
);
Self {
background,
accent,
head,
ear_inner: hsl_to_color(
hue - 6.0,
0.50 + genome.ear_inner_sat * 0.20,
0.72 + genome.ear_inner_light * 0.12,
),
muzzle: hsl_to_color(
hue + 8.0,
0.18 + genome.muzzle_sat * 0.16,
0.84 + genome.muzzle_light * 0.10,
),
eye: hsl_to_color(
genome.eye_hue,
0.65 + genome.eye_sat * 0.20,
0.50 + genome.eye_light * 0.12,
),
pupil: Color::rgb(28, 24, 18),
nose: hsl_to_color(
344.0 + genome.nose_hue * 18.0,
0.58 + genome.nose_sat * 0.18,
0.66 + genome.nose_light * 0.10,
),
outline: Color::rgb(64, 45, 32),
marking: hsl_to_color(
hue + genome.marking_hue_shift * 24.0,
0.25 + genome.marking_sat * 0.20,
0.42 + genome.marking_light * 0.16,
),
}
}
}
#[derive(Clone, Copy, Debug)]
struct CatGenome {
base_hue: f32,
eye_hue: f32,
head_saturation: f32,
head_lightness: f32,
background_shift: f32,
background_sat: f32,
accent_shift: f32,
accent_sat: f32,
ear_inner_sat: f32,
ear_inner_light: f32,
muzzle_sat: f32,
muzzle_light: f32,
eye_sat: f32,
eye_light: f32,
nose_hue: f32,
nose_sat: f32,
nose_light: f32,
marking_hue_shift: f32,
marking_sat: f32,
marking_light: f32,
head_width: f32,
head_height: f32,
head_drop: f32,
ear_width: f32,
ear_height: f32,
ear_tilt: f32,
muzzle_width: f32,
muzzle_height: f32,
eye_spacing: f32,
eye_width: f32,
eye_height: f32,
pupil_width: f32,
whisker_len: f32,
whisker_tilt: f32,
smile_width: f32,
smile_depth: f32,
accent_band_height: f32,
forehead_mark: f32,
cheek_spots: f32,
stripe_count: u8,
}
impl CatGenome {
fn from_identity(identity: &AvatarIdentity, rng: &mut StdRng) -> Self {
let mut noise =
|idx: usize| (identity.unit_f32(idx) + rng.random_range(0.0..0.03)).min(1.0);
Self {
base_hue: 12.0 + identity.unit_f32(0) * 300.0,
eye_hue: 45.0 + identity.unit_f32(1) * 120.0,
head_saturation: noise(2),
head_lightness: noise(3),
background_shift: noise(4),
background_sat: noise(5),
accent_shift: noise(6),
accent_sat: noise(7),
ear_inner_sat: noise(8),
ear_inner_light: noise(9),
muzzle_sat: noise(10),
muzzle_light: noise(11),
eye_sat: noise(12),
eye_light: noise(13),
nose_hue: noise(14),
nose_sat: noise(15),
nose_light: noise(16),
marking_hue_shift: identity.unit_f32(17) * 2.0 - 1.0,
marking_sat: noise(18),
marking_light: noise(19),
head_width: noise(20),
head_height: noise(21),
head_drop: noise(22),
ear_width: noise(23),
ear_height: noise(24),
ear_tilt: identity.unit_f32(25) * 2.0 - 1.0,
muzzle_width: noise(26),
muzzle_height: noise(27),
eye_spacing: noise(28),
eye_width: noise(29),
eye_height: noise(30),
pupil_width: noise(31),
whisker_len: noise(32),
whisker_tilt: identity.unit_f32(33) * 2.0 - 1.0,
smile_width: noise(34),
smile_depth: noise(35),
accent_band_height: noise(36),
forehead_mark: noise(37),
cheek_spots: noise(38),
stripe_count: 2 + (identity.byte(39) % 4),
}
}
}
#[derive(Clone, Copy, Debug)]
struct EarSpec {
outer: [Point<i32>; 3],
inner: [Point<i32>; 3],
}
impl EarSpec {
fn left(
center_x: i32,
center_y: i32,
head_rx: i32,
head_ry: i32,
ear_width: i32,
ear_height: i32,
ear_tilt: f32,
) -> Self {
let base_x = center_x - head_rx / 2;
let base_y = center_y - head_ry + 12;
let tip_shift = (ear_width as f32 * 0.35 * ear_tilt) as i32;
Self {
outer: [
Point::new(base_x - ear_width / 2, base_y + ear_height / 2),
Point::new(base_x + ear_width / 3 + tip_shift, base_y - ear_height),
Point::new(base_x + ear_width, base_y + ear_height / 3),
],
inner: [
Point::new(base_x - ear_width / 6, base_y + ear_height / 4),
Point::new(
base_x + ear_width / 4 + tip_shift / 2,
base_y - (ear_height as f32 * 0.55) as i32,
),
Point::new(
base_x + (ear_width as f32 * 0.6) as i32,
base_y + ear_height / 8,
),
],
}
}
fn right(
center_x: i32,
center_y: i32,
head_rx: i32,
head_ry: i32,
ear_width: i32,
ear_height: i32,
ear_tilt: f32,
) -> Self {
let base_x = center_x + head_rx / 2;
let base_y = center_y - head_ry + 12;
let tip_shift = (ear_width as f32 * 0.35 * ear_tilt) as i32;
Self {
outer: [
Point::new(base_x - ear_width, base_y + ear_height / 3),
Point::new(base_x - ear_width / 3 - tip_shift, base_y - ear_height),
Point::new(base_x + ear_width / 2, base_y + ear_height / 2),
],
inner: [
Point::new(
base_x - (ear_width as f32 * 0.6) as i32,
base_y + ear_height / 8,
),
Point::new(
base_x - ear_width / 4 - tip_shift / 2,
base_y - (ear_height as f32 * 0.55) as i32,
),
Point::new(base_x + ear_width / 6, base_y + ear_height / 4),
],
}
}
}
#[allow(clippy::too_many_arguments)]
fn draw_background_accent(
image: &mut RgbaImage,
center_x: i32,
center_y: i32,
head_rx: i32,
head_ry: i32,
accent: Color,
accent_band_height: f32,
background: AvatarBackground,
) {
if background != AvatarBackground::Themed {
draw_decorative_background(image, background, accent);
return;
}
let width = image.width() as i32;
let stripe_top = center_y - head_ry - 18;
let stripe_height = ((head_ry as f32) * (0.25 + accent_band_height * 0.45)) as i32;
draw_filled_rect_mut(
image,
Rect::at(0, stripe_top.max(0)).of_size(width as u32, stripe_height.max(1) as u32),
accent.into(),
);
draw_filled_circle_mut(
image,
(center_x + head_rx / 2, center_y - head_ry / 2),
head_ry / 3,
Color::rgba(accent.0[0], accent.0[1], accent.0[2], 180).into(),
);
}
fn draw_ear(
image: &mut RgbaImage,
spec: EarSpec,
outer_color: Color,
inner_color: Color,
outline: Color,
) {
draw_polygon_mut(image, &spec.outer, outer_color.into());
draw_polygon_mut(image, &spec.inner, inner_color.into());
for edge in spec.outer.windows(2) {
draw_antialiased_line_segment_mut(
image,
(edge[0].x, edge[0].y),
(edge[1].x, edge[1].y),
outline.into(),
interpolate,
);
}
draw_antialiased_line_segment_mut(
image,
(spec.outer[2].x, spec.outer[2].y),
(spec.outer[0].x, spec.outer[0].y),
outline.into(),
interpolate,
);
}
fn draw_eyes(
image: &mut RgbaImage,
center_x: i32,
center_y: i32,
head_rx: i32,
head_ry: i32,
palette: CatPalette,
genome: CatGenome,
) {
let eye_offset_x = (head_rx as f32 * (0.31 + genome.eye_spacing * 0.18)) as i32;
let eye_y = center_y - head_ry / 6;
let eye_rx = (head_rx as f32 * (0.12 + genome.eye_width * 0.10)) as i32;
let eye_ry = (head_ry as f32 * (0.11 + genome.eye_height * 0.10)) as i32;
let pupil_ry = (eye_ry as f32 * 0.90) as i32;
let pupil_rx = ((eye_rx as f32) * (0.12 + genome.pupil_width * 0.18)) as i32;
for eye_x in [center_x - eye_offset_x, center_x + eye_offset_x] {
draw_filled_ellipse_mut(image, (eye_x, eye_y), eye_rx, eye_ry, palette.eye.into());
draw_filled_ellipse_mut(
image,
(eye_x, eye_y),
pupil_rx,
pupil_ry,
palette.pupil.into(),
);
draw_filled_circle_mut(
image,
(eye_x - eye_rx / 3, eye_y - eye_ry / 3),
(eye_rx as f32 * 0.15) as i32,
Color::rgba(255, 255, 255, 220).into(),
);
}
}
fn draw_nose_and_mouth(
image: &mut RgbaImage,
center_x: i32,
center_y: i32,
head_rx: i32,
head_ry: i32,
palette: CatPalette,
genome: CatGenome,
) {
let nose_y = center_y + head_ry / 7;
let nose_half_width = (head_rx as f32 * (0.08 + genome.muzzle_width * 0.05)) as i32;
let nose_height = (head_ry as f32 * (0.08 + genome.muzzle_height * 0.05)) as i32;
let nose = [
Point::new(center_x - nose_half_width, nose_y),
Point::new(center_x + nose_half_width, nose_y),
Point::new(center_x, nose_y + nose_height),
];
draw_polygon_mut(image, &nose, palette.nose.into());
let mouth_top = nose_y + nose_height;
draw_line_segment_mut(
image,
(center_x as f32, mouth_top as f32),
(center_x as f32, (mouth_top + head_ry / 8) as f32),
palette.outline.into(),
);
let smile_radius = (head_rx as f32 * (0.08 + genome.smile_width * 0.10)) as i32;
draw_smile_arc(
image,
center_x - smile_radius,
mouth_top + smile_radius / 2,
smile_radius,
palette.outline,
genome.smile_depth,
);
draw_smile_arc(
image,
center_x + smile_radius,
mouth_top + smile_radius / 2,
smile_radius,
palette.outline,
genome.smile_depth,
);
}
fn draw_smile_arc(
image: &mut RgbaImage,
center_x: i32,
center_y: i32,
radius: i32,
color: Color,
smile_depth: f32,
) {
for step in 20..=160 {
let theta = (step as f32).to_radians();
let x = center_x as f32 + theta.cos() * radius as f32 * 0.55;
let y = center_y as f32 + theta.sin() * radius as f32 * (0.24 + smile_depth * 0.28);
draw_filled_circle_mut(image, (x.round() as i32, y.round() as i32), 1, color.into());
}
}
fn draw_whiskers(
image: &mut RgbaImage,
center_x: i32,
center_y: i32,
head_rx: i32,
head_ry: i32,
color: Color,
genome: CatGenome,
) {
let muzzle_y = center_y + head_ry / 5;
let left_start = center_x - head_rx / 6;
let right_start = center_x + head_rx / 6;
let whisker_len = (head_rx as f32 * (0.58 + genome.whisker_len * 0.42)) as i32;
let whisker_slope = (genome.whisker_tilt * 12.0) as i32;
for offset in [-12, 0, 12] {
draw_antialiased_line_segment_mut(
image,
(left_start, muzzle_y + offset),
(
left_start - whisker_len,
muzzle_y + offset - 8 + whisker_slope,
),
color.into(),
interpolate,
);
draw_antialiased_line_segment_mut(
image,
(right_start, muzzle_y + offset),
(
right_start + whisker_len,
muzzle_y + offset - 8 - whisker_slope,
),
color.into(),
interpolate,
);
}
}
fn draw_cat_markings(
image: &mut RgbaImage,
center_x: i32,
center_y: i32,
head_rx: i32,
head_ry: i32,
color: Color,
genome: CatGenome,
) {
let stripe_count = genome.stripe_count as i32;
let forehead_y = center_y - head_ry / 2;
let stripe_spacing = (head_rx / 5).max(6);
let stripe_length = ((head_ry as f32) * (0.14 + genome.forehead_mark * 0.12)) as i32;
for stripe in 0..stripe_count {
let offset = stripe - stripe_count / 2;
let x = center_x + offset * stripe_spacing / 2;
draw_line_segment_mut(
image,
(x as f32, forehead_y as f32),
((x + offset * 2) as f32, (forehead_y + stripe_length) as f32),
color.into(),
);
}
if genome.cheek_spots > 0.35 {
let cheek_y = center_y + head_ry / 5;
let cheek_x = (head_rx as f32 * 0.55) as i32;
let cheek_radius = ((head_rx as f32) * (0.05 + genome.cheek_spots * 0.04)) as i32;
draw_filled_circle_mut(
image,
(center_x - cheek_x, cheek_y),
cheek_radius,
Color::rgba(color.0[0], color.0[1], color.0[2], 120).into(),
);
draw_filled_circle_mut(
image,
(center_x + cheek_x, cheek_y),
cheek_radius,
Color::rgba(color.0[0], color.0[1], color.0[2], 120).into(),
);
}
}
fn hsl_to_color(hue: f32, saturation: f32, lightness: f32) -> Color {
let rgb_u8: Srgb<u8> = Srgb::from_color(Hsl::new(hue, saturation, lightness)).into_format();
Color::rgb(rgb_u8.red, rgb_u8.green, rgb_u8.blue)
}
fn background_fill(background: AvatarBackground, themed: Color) -> Color {
match background {
AvatarBackground::Themed => themed,
AvatarBackground::White => Color::rgb(255, 255, 255),
AvatarBackground::Black => Color::rgb(0, 0, 0),
AvatarBackground::Dark => Color::rgb(17, 24, 39),
AvatarBackground::Light => Color::rgb(248, 250, 247),
AvatarBackground::Transparent => Color::rgba(255, 255, 255, 0),
AvatarBackground::PolkaDot
| AvatarBackground::Striped
| AvatarBackground::Checkerboard
| AvatarBackground::Grid => Color::rgb(248, 250, 247),
AvatarBackground::Sunrise => Color::rgb(255, 244, 214),
AvatarBackground::Ocean => Color::rgb(221, 246, 252),
AvatarBackground::Starry => Color::rgb(17, 24, 39),
}
}
fn draw_decorative_background(image: &mut RgbaImage, background: AvatarBackground, accent: Color) {
match background {
AvatarBackground::PolkaDot => draw_polka_dot_background(image, accent),
AvatarBackground::Striped => draw_striped_background(image, accent),
AvatarBackground::Checkerboard => draw_checkerboard_background(image),
AvatarBackground::Grid => draw_grid_background(image),
AvatarBackground::Sunrise => draw_vertical_gradient_background(
image,
Color::rgb(255, 247, 212),
Color::rgb(255, 184, 107),
),
AvatarBackground::Ocean => draw_vertical_gradient_background(
image,
Color::rgb(220, 248, 252),
Color::rgb(75, 145, 190),
),
AvatarBackground::Starry => draw_starry_background(image),
AvatarBackground::Themed
| AvatarBackground::White
| AvatarBackground::Black
| AvatarBackground::Dark
| AvatarBackground::Light
| AvatarBackground::Transparent => {}
}
}
fn draw_polka_dot_background(image: &mut RgbaImage, accent: Color) {
let base = Color::rgb(248, 250, 247);
let dot = rgba_over(base, Color::rgba(accent.0[0], accent.0[1], accent.0[2], 62));
fill_image(image, base);
let min_side = image.width().min(image.height()).max(1);
let step = (min_side / 8).clamp(8, 44) as i32;
let radius = (step / 5).max(1);
for y in (step / 2..image.height() as i32).step_by(step as usize) {
for x in (step / 2..image.width() as i32).step_by(step as usize) {
draw_filled_circle_mut(image, (x, y), radius, dot.into());
}
}
}
fn draw_striped_background(image: &mut RgbaImage, accent: Color) {
let base = Color::rgb(248, 250, 247);
let stripe = rgba_over(base, Color::rgba(accent.0[0], accent.0[1], accent.0[2], 42));
let min_side = image.width().min(image.height()).max(1);
let width = (min_side / 10).clamp(6, 36);
for y in 0..image.height() {
for x in 0..image.width() {
let band = ((x + y) / width).is_multiple_of(2);
image.put_pixel(x, y, if band { stripe.into() } else { base.into() });
}
}
}
fn draw_checkerboard_background(image: &mut RgbaImage) {
let light = Color::rgb(248, 250, 247);
let dark = Color::rgb(232, 236, 231);
let min_side = image.width().min(image.height()).max(1);
let tile = (min_side / 8).clamp(8, 48);
for y in 0..image.height() {
for x in 0..image.width() {
let even = ((x / tile) + (y / tile)).is_multiple_of(2);
image.put_pixel(x, y, if even { light.into() } else { dark.into() });
}
}
}
fn draw_grid_background(image: &mut RgbaImage) {
let base = Color::rgb(248, 250, 247);
let line = Color::rgb(221, 226, 221);
let min_side = image.width().min(image.height()).max(1);
let step = (min_side / 8).clamp(8, 48);
for y in 0..image.height() {
for x in 0..image.width() {
let grid_line = x.is_multiple_of(step) || y.is_multiple_of(step);
image.put_pixel(x, y, if grid_line { line.into() } else { base.into() });
}
}
}
fn draw_vertical_gradient_background(image: &mut RgbaImage, top: Color, bottom: Color) {
let max_y = image.height().saturating_sub(1).max(1);
for y in 0..image.height() {
let color = lerp_color_u32(top, bottom, y, max_y);
for x in 0..image.width() {
image.put_pixel(x, y, color.into());
}
}
}
fn draw_starry_background(image: &mut RgbaImage) {
let base = Color::rgb(17, 24, 39);
fill_image(image, base);
let min_side = image.width().min(image.height()).max(1);
let star_count = (min_side / 7).clamp(10, 180);
let mut state = 0x9e37_79b9_u32
^ image.width().wrapping_mul(0x85eb_ca6b)
^ image.height().wrapping_mul(0xc2b2_ae35);
for index in 0..star_count {
state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
let x = state % image.width().max(1);
state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
let y = state % image.height().max(1);
let radius = if index.is_multiple_of(7) { 2 } else { 1 };
draw_filled_circle_mut(
image,
(x as i32, y as i32),
radius,
Rgba([255, 255, 255, 170]),
);
}
}
fn fill_image(image: &mut RgbaImage, color: Color) {
for pixel in image.pixels_mut() {
*pixel = color.into();
}
}
fn rgba_over(bottom: Color, top: Color) -> Color {
let alpha = u32::from(top.0[3]);
let inverse = 255 - alpha;
Color::rgb(
((u32::from(top.0[0]) * alpha + u32::from(bottom.0[0]) * inverse + 127) / 255) as u8,
((u32::from(top.0[1]) * alpha + u32::from(bottom.0[1]) * inverse + 127) / 255) as u8,
((u32::from(top.0[2]) * alpha + u32::from(bottom.0[2]) * inverse + 127) / 255) as u8,
)
}
fn lerp_color_u32(start: Color, end: Color, position: u32, max_position: u32) -> Color {
let max = max_position.max(1);
Color::rgb(
lerp_channel_u32(start.0[0], end.0[0], position, max),
lerp_channel_u32(start.0[1], end.0[1], position, max),
lerp_channel_u32(start.0[2], end.0[2], position, max),
)
}
fn lerp_channel_u32(start: u8, end: u8, position: u32, max_position: u32) -> u8 {
let start = u32::from(start);
let end = u32::from(end);
((start * (max_position - position) + end * position + max_position / 2) / max_position) as u8
}
fn color_hex(color: Color) -> String {
format!("#{:02x}{:02x}{:02x}", color.0[0], color.0[1], color.0[2])
}
fn render_cat_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.56;
let rx = w * (0.26 + identity.unit_f32(20) * 0.07);
let ry = h * (0.22 + identity.unit_f32(21) * 0.08);
let head = hsl_to_color(20.0 + identity.unit_f32(0) * 40.0, 0.48, 0.64);
let muzzle = hsl_to_color(28.0 + identity.unit_f32(1) * 18.0, 0.18, 0.90);
let eye = hsl_to_color(90.0 + identity.unit_f32(2) * 40.0, 0.7, 0.55);
let outline = Color::rgb(64, 45, 32);
let left_ear = format!(
"{},{} {},{} {},{}",
cx - rx * 0.8,
cy - ry * 0.4,
cx - rx * 0.4,
cy - ry * 1.3,
cx - rx * 0.1,
cy - ry * 0.1
);
let right_ear = format!(
"{},{} {},{} {},{}",
cx + rx * 0.8,
cy - ry * 0.4,
cx + rx * 0.4,
cy - ry * 1.3,
cx + rx * 0.1,
cy - ry * 0.1
);
let nose = format!(
"{},{} {},{} {},{}",
cx - rx * 0.06,
cy + ry * 0.1,
cx + rx * 0.06,
cy + ry * 0.1,
cx,
cy + ry * 0.2
);
format!(
r##"<polygon points="{left_ear}" fill="{head}"/><polygon points="{right_ear}" fill="{head}"/><ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="{head}"/><ellipse cx="{cx}" cy="{muzzle_y}" rx="{muzzle_rx}" ry="{muzzle_ry}" fill="{muzzle}"/><ellipse cx="{left_eye_x}" cy="{eye_y}" rx="{eye_rx}" ry="{eye_ry}" fill="{eye}"/><ellipse cx="{right_eye_x}" cy="{eye_y}" rx="{eye_rx}" ry="{eye_ry}" fill="{eye}"/><polygon points="{nose}" fill="#d6818d"/><path d="M {left_mx} {mouth_y} q {curve_x} {curve_y} {curve_end} 0 M {right_mx} {mouth_y} q {curve_x} {curve_y} {curve_end} 0" stroke="{outline}" stroke-width="3" fill="none" stroke-linecap="round"/>"##,
left_ear = left_ear,
right_ear = right_ear,
head = color_hex(head),
cx = cx,
cy = cy,
rx = rx,
ry = ry,
muzzle_y = cy + ry * 0.28,
muzzle_rx = rx * 0.45,
muzzle_ry = ry * 0.28,
muzzle = color_hex(muzzle),
left_eye_x = cx - rx * 0.34,
right_eye_x = cx + rx * 0.34,
eye_y = cy - ry * 0.1,
eye_rx = rx * 0.13,
eye_ry = ry * 0.16,
eye = color_hex(eye),
nose = nose,
left_mx = cx - rx * 0.08,
right_mx = cx + rx * 0.08,
mouth_y = cy + ry * 0.22,
curve_x = rx * 0.1,
curve_y = ry * 0.12,
curve_end = rx * 0.16,
outline = color_hex(outline),
)
}
fn render_dog_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.56;
let fur = hsl_to_color(18.0 + identity.unit_f32(5) * 45.0, 0.42, 0.60);
let ear = hsl_to_color(18.0 + identity.unit_f32(6) * 30.0, 0.44, 0.40);
let muzzle = hsl_to_color(34.0, 0.18, 0.92);
format!(
r##"<ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="{}"/><ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="{}"/><ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="{}"/><ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="{}"/><circle cx="{}" cy="{}" r="{}" fill="#fff"/><circle cx="{}" cy="{}" r="{}" fill="#241a14"/><circle cx="{}" cy="{}" r="{}" fill="#fff"/><circle cx="{}" cy="{}" r="{}" fill="#241a14"/><ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="#2d2422"/><path d="M {} {} q {} {} {} 0 M {} {} q {} {} {} 0" stroke="#2d2422" stroke-width="3" fill="none" stroke-linecap="round"/>"##,
cx - w * 0.14,
cy - h * 0.03,
w * 0.09,
h * 0.18,
color_hex(ear),
cx + w * 0.14,
cy - h * 0.03,
w * 0.09,
h * 0.18,
color_hex(ear),
cx,
cy,
w * 0.26,
h * 0.24,
color_hex(fur),
cx,
cy + h * 0.08,
w * 0.12,
h * 0.07,
color_hex(muzzle),
cx - w * 0.08,
cy - h * 0.05,
w * 0.03,
cx - w * 0.08,
cy - h * 0.05,
w * 0.015,
cx + w * 0.08,
cy - h * 0.05,
w * 0.03,
cx + w * 0.08,
cy - h * 0.05,
w * 0.015,
cx,
cy + h * 0.06,
w * 0.035,
h * 0.026,
cx - w * 0.03,
cy + h * 0.09,
w * 0.05,
h * 0.05,
w * 0.10,
cx + w * 0.03,
cy + h * 0.09,
w * 0.05,
h * 0.05,
w * 0.10,
)
}
fn render_robot_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.56;
let metal = hsl_to_color(205.0 + identity.unit_f32(3) * 25.0, 0.16, 0.74);
let trim = hsl_to_color(205.0 + identity.unit_f32(4) * 22.0, 0.18, 0.46);
let light = hsl_to_color(60.0 + identity.unit_f32(5) * 110.0, 0.78, 0.66);
let head_w = w * 0.48;
let head_h = h * 0.38;
let x = cx - head_w / 2.0;
let y = cy - head_h / 2.0;
format!(
r##"<line x1="{cx}" y1="{a1}" x2="{cx}" y2="{a2}" stroke="{trim}" stroke-width="4"/><circle cx="{cx}" cy="{a1}" r="{ar}" fill="{light}"/><rect x="{x}" y="{y}" width="{head_w}" height="{head_h}" rx="14" fill="{metal}" stroke="{trim}" stroke-width="4"/><ellipse cx="{ex1}" cy="{ey}" rx="{erx}" ry="{ery}" fill="{light}"/><ellipse cx="{ex2}" cy="{ey}" rx="{erx}" ry="{ery}" fill="{light}"/><rect x="{mx}" y="{my}" width="{mw}" height="{mh}" rx="6" fill="#2f3c48"/><circle cx="{bx1}" cy="{cy}" r="{br}" fill="{trim}"/><circle cx="{bx2}" cy="{cy}" r="{br}" fill="{trim}"/>"##,
cx = cx,
a1 = y - h * 0.10,
a2 = y,
ar = w * 0.02,
x = x,
y = y,
head_w = head_w,
head_h = head_h,
metal = color_hex(metal),
trim = color_hex(trim),
light = color_hex(light),
ex1 = cx - head_w * 0.24,
ex2 = cx + head_w * 0.24,
ey = cy - head_h * 0.14,
erx = w * 0.055,
ery = h * 0.04,
mx = cx - head_w * 0.18,
my = cy + head_h * 0.12,
mw = head_w * 0.36,
mh = head_h * 0.10,
bx1 = x + head_w * 0.1,
bx2 = x + head_w * 0.9,
br = w * 0.02,
)
}
fn render_fox_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.56;
let orange = hsl_to_color(18.0 + identity.unit_f32(1) * 20.0, 0.76, 0.58);
let cream = hsl_to_color(40.0, 0.30, 0.94);
format!(
r##"<polygon points="{},{}, {},{}, {},{}" fill="{}"/><polygon points="{},{}, {},{}, {},{}" fill="{}"/><ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="{}"/><polygon points="{},{}, {},{}, {},{}" fill="{}"/><polygon points="{},{}, {},{}, {},{}" fill="{}"/><ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="#fff"/><ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="#fff"/><ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="#221c18"/><ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="#221c18"/>"##,
cx - w * 0.18,
cy - h * 0.12,
cx - w * 0.10,
cy - h * 0.28,
cx - w * 0.02,
cy - h * 0.05,
color_hex(orange),
cx + w * 0.18,
cy - h * 0.12,
cx + w * 0.10,
cy - h * 0.28,
cx + w * 0.02,
cy - h * 0.05,
color_hex(orange),
cx,
cy,
w * 0.24,
h * 0.22,
color_hex(orange),
cx - w * 0.18,
cy - h * 0.03,
cx,
cy + h * 0.10,
cx - w * 0.06,
cy + h * 0.18,
color_hex(cream),
cx + w * 0.18,
cy - h * 0.03,
cx,
cy + h * 0.10,
cx + w * 0.06,
cy + h * 0.18,
color_hex(cream),
cx - w * 0.08,
cy - h * 0.04,
w * 0.03,
h * 0.03,
cx + w * 0.08,
cy - h * 0.04,
w * 0.03,
h * 0.03,
cx - w * 0.08,
cy - h * 0.04,
w * 0.013,
h * 0.022,
cx + w * 0.08,
cy - h * 0.04,
w * 0.013,
h * 0.022,
)
}
fn render_alien_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.56;
let skin = hsl_to_color(90.0 + identity.unit_f32(0) * 80.0, 0.48, 0.70);
format!(
r##"<ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="{}"/><ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="#261832"/><ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="#261832"/><circle cx="{}" cy="{}" r="{}" fill="#5e8c58"/>"##,
cx,
cy,
w * 0.18,
h * 0.28,
color_hex(skin),
cx - w * 0.08,
cy - h * 0.07,
w * 0.04,
h * 0.09,
cx + w * 0.08,
cy - h * 0.07,
w * 0.04,
h * 0.09,
cx,
cy + h * 0.03,
w * 0.012,
)
}
fn render_monster_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.58;
let skin = hsl_to_color(
identity.unit_f32(0) * 360.0,
0.52 + identity.unit_f32(1) * 0.20,
0.50 + identity.unit_f32(2) * 0.16,
);
let shade = hsl_to_color(
identity.unit_f32(3) * 360.0,
0.40 + identity.unit_f32(4) * 0.16,
0.26 + identity.unit_f32(5) * 0.08,
);
let eyes = 1 + (identity.byte(12) % 3) as usize;
let eye_spacing = if eyes == 1 {
0.0
} else {
w * 0.22 / (eyes - 1) as f32
};
let eye_start = cx - eye_spacing * (eyes.saturating_sub(1) as f32) / 2.0;
let mut eye_markup = String::new();
for index in 0..eyes {
let ex = eye_start + eye_spacing * index as f32;
eye_markup.push_str(&format!(
r##"<ellipse cx="{ex}" cy="{ey}" rx="{erx}" ry="{ery}" fill="#fcf8ec"/><ellipse cx="{ex}" cy="{ey}" rx="{prx}" ry="{pry}" fill="#18141c"/>"##,
ey = cy - h * 0.08,
erx = w * 0.038,
ery = h * 0.042,
prx = w * 0.012,
pry = h * 0.030,
));
}
let horns = if identity.byte(18).is_multiple_of(2) {
format!(
r#"<polygon points="{},{}, {},{}, {},{}" fill="{}"/><polygon points="{},{}, {},{}, {},{}" fill="{}"/>"#,
cx - w * 0.18,
cy - h * 0.18,
cx - w * 0.22,
cy - h * 0.34,
cx - w * 0.08,
cy - h * 0.14,
color_hex(shade),
cx + w * 0.18,
cy - h * 0.18,
cx + w * 0.22,
cy - h * 0.34,
cx + w * 0.08,
cy - h * 0.14,
color_hex(shade),
)
} else {
format!(
r#"<polygon points="{},{}, {},{}, {},{}" fill="{}"/><polygon points="{},{}, {},{}, {},{}" fill="{}"/><polygon points="{},{}, {},{}, {},{}" fill="{}"/>"#,
cx - w * 0.12,
cy - h * 0.14,
cx - w * 0.08,
cy - h * 0.30,
cx - w * 0.02,
cy - h * 0.14,
color_hex(shade),
cx,
cy - h * 0.15,
cx,
cy - h * 0.32,
cx + w * 0.05,
cy - h * 0.15,
color_hex(shade),
cx + w * 0.12,
cy - h * 0.14,
cx + w * 0.08,
cy - h * 0.30,
cx + w * 0.02,
cy - h * 0.14,
color_hex(shade),
)
};
format!(
r##"<ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="{skin}"/>{horns}<circle cx="{sx1}" cy="{sy1}" r="{sr}" fill="{shade}" fill-opacity="0.55"/><circle cx="{sx2}" cy="{sy2}" r="{sr2}" fill="{shade}" fill-opacity="0.55"/>{eye_markup}<rect x="{mx}" y="{my}" width="{mw}" height="{mh}" rx="{mr}" fill="#301218"/><polygon points="{tx1},{ty1}, {tx2},{ty1}, {txm1},{ty2}" fill="#fcf8ec"/><polygon points="{tx3},{ty1}, {tx4},{ty1}, {txm2},{ty2}" fill="#fcf8ec"/>"##,
cx = cx,
cy = cy,
rx = w * 0.24,
ry = h * 0.23,
skin = color_hex(skin),
horns = horns,
shade = color_hex(shade),
sx1 = cx - w * 0.12,
sy1 = cy - h * 0.02,
sr = w * 0.034,
sx2 = cx + w * 0.14,
sy2 = cy + h * 0.07,
sr2 = w * 0.026,
eye_markup = eye_markup,
mx = cx - w * 0.14,
my = cy + h * 0.08,
mw = w * 0.28,
mh = h * 0.09,
mr = w * 0.02,
tx1 = cx - w * 0.10,
tx2 = cx - w * 0.06,
txm1 = cx - w * 0.08,
tx3 = cx + w * 0.06,
tx4 = cx + w * 0.10,
txm2 = cx + w * 0.08,
ty1 = cy + h * 0.08,
ty2 = cy + h * 0.16,
)
}
fn render_ghost_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * (0.53 + identity.unit_f32(5) * 0.06);
let rx = w * (0.19 + identity.unit_f32(3) * 0.08);
let ry = h * (0.21 + identity.unit_f32(4) * 0.08);
let body = hsl_to_color(
190.0 + identity.unit_f32(1) * 55.0,
0.10 + identity.unit_f32(7) * 0.10,
0.94 + identity.unit_f32(8) * 0.04,
);
let eye_style = if identity.byte(19).is_multiple_of(2) {
(w * 0.026, h * 0.054)
} else {
(w * 0.038, h * 0.038)
};
let mouth = if identity.byte(20) % 3 == 1 {
format!(
r##"<ellipse cx="{cx}" cy="{my}" rx="{mrx}" ry="{mry}" fill="#8da0b2"/>"##,
my = cy + h * 0.08,
mrx = w * 0.035,
mry = h * 0.045,
)
} else {
format!(
r##"<path d="M {mx1} {my} q {cq} {cyq} {ce} 0 M {mx2} {my} q {cq} {cyq} {ce} 0" stroke="#8da0b2" stroke-width="3" fill="none" stroke-linecap="round"/>"##,
mx1 = cx - w * 0.03,
mx2 = cx + w * 0.03,
my = cy + h * 0.08,
cq = w * 0.04,
cyq = if identity.byte(20) % 3 == 2 {
0.0
} else {
h * 0.05
},
ce = w * 0.06,
)
};
format!(
r##"<ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="{body}"/><rect x="{x}" y="{cy}" width="{rw}" height="{rh}" fill="{body}"/><circle cx="{c1}" cy="{scy}" r="{sr1}" fill="{body}"/><circle cx="{c2}" cy="{scy}" r="{sr2}" fill="{body}"/><circle cx="{c3}" cy="{scy}" r="{sr3}" fill="{body}"/><ellipse cx="{lx}" cy="{ey}" rx="{erx}" ry="{ery}" fill="#30384a"/><ellipse cx="{rx2}" cy="{ey}" rx="{erx}" ry="{ery}" fill="#30384a"/>{mouth}"##,
cx = cx,
cy = cy,
rx = rx,
ry = ry,
body = color_hex(body),
x = cx - rx,
rw = rx * 2.0,
rh = ry * (0.82 + identity.unit_f32(12) * 0.28),
c1 = cx - rx * 0.70,
c2 = cx,
c3 = cx + rx * 0.70,
scy = cy + ry * 1.36,
sr1 = rx * (0.18 + identity.unit_f32(13) * 0.12),
sr2 = rx * (0.18 + identity.unit_f32(14) * 0.12),
sr3 = rx * (0.18 + identity.unit_f32(15) * 0.12),
lx = cx - rx * 0.36,
rx2 = cx + rx * 0.36,
ey = cy - ry * 0.25,
erx = eye_style.0,
ery = eye_style.1,
mouth = mouth,
)
}
fn render_slime_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * (0.56 + identity.unit_f32(8) * 0.08);
let rx = w * (0.20 + identity.unit_f32(6) * 0.10);
let ry = h * (0.16 + identity.unit_f32(7) * 0.08);
let slime = hsl_to_color(
70.0 + identity.unit_f32(4) * 130.0,
0.44 + identity.unit_f32(9) * 0.22,
0.46 + identity.unit_f32(10) * 0.18,
);
let eye_count = 1 + (identity.byte(49) % 3) as usize;
let eye_markup = match eye_count {
1 => format!(
r##"<circle cx="{cx}" cy="{ey}" r="{er}" fill="#f8ffec"/><circle cx="{cx}" cy="{ey}" r="{pr}" fill="#203018"/>"##,
ey = cy - ry * 0.20,
er = rx * 0.14,
pr = rx * 0.055,
),
2 => format!(
r##"<circle cx="{lx}" cy="{ey}" r="{er}" fill="#f8ffec"/><circle cx="{rx2}" cy="{ey}" r="{er}" fill="#f8ffec"/><circle cx="{lx}" cy="{ey}" r="{pr}" fill="#203018"/><circle cx="{rx2}" cy="{ey}" r="{pr}" fill="#203018"/>"##,
lx = cx - rx * 0.34,
rx2 = cx + rx * 0.34,
ey = cy - ry * 0.22,
er = rx * 0.12,
pr = rx * 0.050,
),
_ => format!(
r##"<circle cx="{lx}" cy="{ey}" r="{er}" fill="#f8ffec"/><circle cx="{cx}" cy="{ey2}" r="{er2}" fill="#f8ffec"/><circle cx="{rx2}" cy="{ey}" r="{er}" fill="#f8ffec"/><circle cx="{lx}" cy="{ey}" r="{pr}" fill="#203018"/><circle cx="{cx}" cy="{ey2}" r="{pr}" fill="#203018"/><circle cx="{rx2}" cy="{ey}" r="{pr}" fill="#203018"/>"##,
lx = cx - rx * 0.34,
rx2 = cx + rx * 0.34,
ey = cy - ry * 0.26,
ey2 = cy - ry * 0.14,
er = rx * 0.11,
er2 = rx * 0.095,
pr = rx * 0.045,
),
};
format!(
r##"<ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="{slime}"/><rect x="{dx1}" y="{cy}" width="{dw1}" height="{dh1}" fill="{slime}"/><rect x="{dx2}" y="{cy}" width="{dw2}" height="{dh2}" fill="{slime}"/><rect x="{dx3}" y="{cy}" width="{dw3}" height="{dh3}" fill="{slime}"/>{eye_markup}<rect x="{mx}" y="{my}" width="{mw}" height="{mh}" rx="{mr}" fill="#305228"/>"##,
cx = cx,
cy = cy,
rx = rx,
ry = ry,
slime = color_hex(slime),
dx1 = cx - rx * 0.66,
dx2 = cx - rx * 0.14,
dx3 = cx + rx * 0.34,
dw1 = rx * (0.24 + identity.unit_f32(14) * 0.12),
dw2 = rx * (0.20 + identity.unit_f32(15) * 0.14),
dw3 = rx * (0.22 + identity.unit_f32(16) * 0.14),
dh1 = ry * (0.62 + identity.unit_f32(22) * 0.60),
dh2 = ry * (0.42 + identity.unit_f32(23) * 0.55),
dh3 = ry * (0.54 + identity.unit_f32(24) * 0.62),
eye_markup = eye_markup,
mx = cx - rx * 0.42,
my = cy + ry * 0.40,
mw = rx * 0.84,
mh = h * 0.02,
mr = w * 0.01,
)
}
fn render_bird_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.56;
let plumage = hsl_to_color(identity.unit_f32(7) * 360.0, 0.42, 0.62);
let wing = hsl_to_color(20.0 + identity.unit_f32(8) * 160.0, 0.32, 0.46);
let beak = hsl_to_color(32.0 + identity.unit_f32(9) * 26.0, 0.82, 0.58);
format!(
r##"<circle cx="{cx}" cy="{cy}" r="{r}" fill="{plumage}"/><ellipse cx="{lx}" cy="{wy}" rx="{wrx}" ry="{wry}" fill="{wing}"/><ellipse cx="{rx2}" cy="{wy}" rx="{wrx}" ry="{wry}" fill="{wing}"/><polygon points="{cx},{cy} {bx},{by} {cx},{by2}" fill="{beak}"/><circle cx="{elx}" cy="{ey}" r="{er}" fill="#fff"/><circle cx="{erx}" cy="{ey}" r="{er}" fill="#fff"/><circle cx="{elx}" cy="{ey}" r="{pr}" fill="#181822"/><circle cx="{erx}" cy="{ey}" r="{pr}" fill="#181822"/>"##,
cx = cx,
cy = cy,
r = w * 0.22,
plumage = color_hex(plumage),
lx = cx - w * 0.12,
rx2 = cx + w * 0.12,
wy = cy + h * 0.04,
wrx = w * 0.08,
wry = h * 0.12,
wing = color_hex(wing),
bx = cx + w * 0.12,
by = cy + h * 0.04,
by2 = cy + h * 0.10,
beak = color_hex(beak),
elx = cx - w * 0.07,
erx = cx + w * 0.07,
ey = cy - h * 0.05,
er = w * 0.028,
pr = w * 0.012,
)
}
fn render_wizard_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * (0.57 + identity.unit_f32(17) * 0.07);
let r = w * (0.16 + identity.unit_f32(15) * 0.06);
let hat = hsl_to_color(
210.0 + identity.unit_f32(11) * 110.0,
0.34 + identity.unit_f32(20) * 0.22,
0.28 + identity.unit_f32(21) * 0.16,
);
let band = hsl_to_color(
24.0 + identity.unit_f32(12) * 160.0,
0.62 + identity.unit_f32(22) * 0.24,
0.48 + identity.unit_f32(23) * 0.18,
);
let skin = hsl_to_color(
18.0 + identity.unit_f32(13) * 28.0,
0.22 + identity.unit_f32(24) * 0.20,
0.74 + identity.unit_f32(25) * 0.12,
);
let beard = hsl_to_color(
35.0 + identity.unit_f32(14) * 45.0,
0.06 + identity.unit_f32(26) * 0.12,
0.80 + identity.unit_f32(27) * 0.16,
);
let hat_width = r * (1.0 + identity.unit_f32(28) * 0.55);
let hat_height = h * (0.28 + identity.unit_f32(29) * 0.12);
let tip_shift = (identity.unit_f32(30) - 0.5) * r * 0.9;
let stars = format!(
r##"<circle cx="{s1x}" cy="{s1y}" r="{sr}" fill="{band}"/><circle cx="{s2x}" cy="{s2y}" r="{sr2}" fill="{band}"/>"##,
s1x = cx - hat_width * 0.35,
s1y = cy - h * 0.20,
s2x = cx + tip_shift * 0.5 + hat_width * 0.22,
s2y = cy - h * 0.28,
sr = w * (0.010 + identity.unit_f32(34) * 0.012),
sr2 = w * (0.008 + identity.unit_f32(35) * 0.010),
band = color_hex(band),
);
format!(
r##"<polygon points="{x1},{y1} {x2},{y1} {tx},{y2}" fill="{hat}"/><rect x="{bx}" y="{by}" width="{bw}" height="{bh}" fill="{band}"/>{stars}<circle cx="{cx}" cy="{cy}" r="{r}" fill="{skin}"/><polygon points="{b1},{b2} {b3},{b2} {bt},{b4}" fill="{beard}"/><circle cx="{elx}" cy="{ey}" r="{er}" fill="#fff"/><circle cx="{erx}" cy="{ey}" r="{er}" fill="#fff"/><circle cx="{elx}" cy="{ey}" r="{pr}" fill="#241e34"/><circle cx="{erx}" cy="{ey}" r="{pr}" fill="#241e34"/><circle cx="{sx}" cy="{sy}" r="{sr}" fill="{band}"/>"##,
cx = cx,
cy = cy,
x1 = cx - hat_width,
x2 = cx + hat_width,
tx = cx + tip_shift,
y1 = cy - h * 0.08,
y2 = cy - h * 0.08 - hat_height,
hat = color_hex(hat),
bx = cx - w * (0.24 + identity.unit_f32(31) * 0.08),
by = cy - h * 0.08,
bw = w * (0.48 + identity.unit_f32(31) * 0.16),
bh = h * (0.030 + identity.unit_f32(32) * 0.025),
band = color_hex(band),
stars = stars,
r = r,
skin = color_hex(skin),
b1 = cx - r * (0.52 + identity.unit_f32(44) * 0.20),
b2 = cy + h * 0.06,
b3 = cx + r * (0.52 + identity.unit_f32(45) * 0.20),
bt = cx + (identity.unit_f32(46) - 0.5) * r * 0.45,
b4 = cy + h * (0.22 + identity.unit_f32(47) * 0.10),
beard = color_hex(beard),
elx = cx - r * 0.36,
erx = cx + r * 0.36,
ey = cy - h * 0.03,
er = r * 0.13,
pr = r * 0.055,
sx = cx + tip_shift + r * 0.50,
sy = cy - h * 0.08 - hat_height,
sr = r * 0.10,
)
}
fn render_skull_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * (0.51 + identity.unit_f32(19) * 0.07);
let rx = w * (0.18 + identity.unit_f32(17) * 0.07);
let ry = h * (0.18 + identity.unit_f32(18) * 0.07);
let bone = hsl_to_color(
28.0 + identity.unit_f32(16) * 34.0,
0.08 + identity.unit_f32(22) * 0.10,
0.82 + identity.unit_f32(23) * 0.12,
);
let crack = color_hex(hsl_to_color(
20.0 + identity.unit_f32(24) * 40.0,
0.06,
0.22 + identity.unit_f32(25) * 0.12,
));
let teeth = 3 + (identity.byte(32) % 4) as usize;
let mut tooth_markup = String::new();
for tooth in 0..teeth {
let x = cx - rx * 0.34 + tooth as f32 * (rx * 0.68 / teeth.max(1) as f32);
tooth_markup.push_str(&format!(
r##"<line x1="{x}" y1="{ty1}" x2="{x}" y2="{ty2}" stroke="{crack}" stroke-width="3"/>"##,
ty1 = cy + ry * 0.52,
ty2 = cy + ry * (0.86 + identity.unit_f32(46 + tooth) * 0.25),
));
}
format!(
r##"<ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="{bone}"/><rect x="{jx}" y="{jy}" width="{jw}" height="{jh}" fill="{bone}"/><ellipse cx="{elx}" cy="{ey}" rx="{erx}" ry="{ery}" fill="{crack}"/><ellipse cx="{erx2}" cy="{ey}" rx="{erx}" ry="{ery}" fill="{crack}"/><polygon points="{cx},{ny} {nx1},{ny2} {nx2},{ny2}" fill="{crack}"/><rect x="{mx}" y="{my}" width="{mw}" height="{mh}" fill="{crack}"/>{tooth_markup}<line x1="{cx1}" y1="{cy1}" x2="{cx2}" y2="{cy2}" stroke="{crack}" stroke-width="3"/>"##,
cx = cx,
cy = cy,
rx = rx,
ry = ry,
bone = color_hex(bone),
crack = crack,
jx = cx - rx * 0.45,
jy = cy + ry * 0.50,
jw = rx * (0.82 + identity.unit_f32(26) * 0.34),
jh = ry * (0.34 + identity.unit_f32(27) * 0.28),
elx = cx - rx * 0.34,
erx2 = cx + rx * 0.34,
ey = cy - ry * 0.20,
erx = rx * (0.18 + identity.unit_f32(29) * 0.08),
ery = ry * (0.25 + identity.unit_f32(30) * 0.12),
ny = cy,
nx1 = cx - rx * 0.10,
nx2 = cx + rx * 0.10,
ny2 = cy + ry * (0.32 + identity.unit_f32(30) * 0.16),
mx = cx - rx * 0.48,
my = cy + ry * 0.50,
mw = rx * 0.96,
mh = h * 0.02,
tooth_markup = tooth_markup,
cx1 = cx - rx * 0.15,
cy1 = cy - ry * 0.45,
cx2 = cx + (identity.unit_f32(42) - 0.5) * rx * 0.40,
cy2 = cy + ry * 0.10,
)
}
fn render_planet_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * (0.53 + identity.unit_f32(8) * 0.06);
let r = w.min(h) * (0.18 + identity.unit_f32(20) * 0.08);
let planet = hsl_to_color(identity.unit_f32(1) * 360.0, 0.46, 0.58);
let shade = hsl_to_color(identity.unit_f32(2) * 360.0, 0.38, 0.42);
let ring = hsl_to_color(32.0 + identity.unit_f32(3) * 120.0, 0.44, 0.72);
let ring_rx = r * (1.55 + identity.unit_f32(21) * 0.28);
let ring_ry = r * (0.38 + identity.unit_f32(22) * 0.12);
format!(
r##"<ellipse cx="{cx}" cy="{cy}" rx="{rrx}" ry="{rry}" fill="{ring}"/><circle cx="{cx}" cy="{cy}" r="{r}" fill="{planet}"/><ellipse cx="{sx}" cy="{sy}" rx="{srx}" ry="{sry}" fill="{shade}" opacity="0.45"/><ellipse cx="{hx}" cy="{hy}" rx="{hrx}" ry="{hry}" fill="#ffffff" opacity="0.32"/><rect x="{rx}" y="{ry}" width="{rw}" height="{rh}" rx="{cr}" fill="{ring}" opacity="0.78"/>"##,
cx = cx,
cy = cy,
r = r,
rrx = ring_rx,
rry = ring_ry,
ring = color_hex(ring),
planet = color_hex(planet),
sx = cx - r * 0.25,
sy = cy - r * 0.20,
srx = r * 0.50,
sry = r * 0.20,
shade = color_hex(shade),
hx = cx + r * 0.25,
hy = cy + r * 0.20,
hrx = r * 0.50,
hry = r * 0.16,
rx = cx - ring_rx,
ry = cy - ring_ry * 0.16,
rw = ring_rx * 2.0,
rh = ring_ry * 0.32,
cr = ring_ry * 0.16,
)
}
fn render_rocket_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.52;
let body_w = w * (0.18 + identity.unit_f32(5) * 0.05);
let body_h = h * (0.42 + identity.unit_f32(6) * 0.08);
let top_y = cy - body_h / 2.0;
let bottom_y = cy + body_h / 2.0;
let hull = hsl_to_color(200.0 + identity.unit_f32(1) * 50.0, 0.12, 0.88);
let trim = hsl_to_color(identity.unit_f32(2) * 360.0, 0.58, 0.54);
let window = hsl_to_color(185.0 + identity.unit_f32(3) * 70.0, 0.54, 0.72);
let flame = hsl_to_color(20.0 + identity.unit_f32(4) * 30.0, 0.86, 0.58);
let mut windows = String::new();
for index in 0..(1 + (identity.byte(7) % 2) as usize) {
let wy = top_y + body_h / 3.0 + index as f32 * body_w;
windows.push_str(&format!(
r##"<circle cx="{cx}" cy="{wy}" r="{wr}" fill="{trim}"/><circle cx="{cx}" cy="{wy}" r="{ir}" fill="{window}"/>"##,
wr = body_w * 0.22,
ir = body_w * 0.15,
trim = color_hex(trim),
window = color_hex(window),
));
}
format!(
r##"<polygon points="{nx1},{ny1} {nx2},{ny1} {cx},{ny2}" fill="{trim}"/><rect x="{bx}" y="{by}" width="{bw}" height="{bh}" fill="{hull}"/><ellipse cx="{cx}" cy="{by}" rx="{erx}" ry="{ery}" fill="{hull}"/><polygon points="{lf1},{lfy1} {lf2},{lfy2} {lf3},{lfy3}" fill="{trim}"/><polygon points="{rf1},{lfy1} {rf2},{lfy2} {rf3},{lfy3}" fill="{trim}"/>{windows}<polygon points="{fx1},{fy1} {fx2},{fy1} {cx},{fy2}" fill="{flame}"/>"##,
cx = cx,
nx1 = cx - body_w / 2.0,
nx2 = cx + body_w / 2.0,
ny1 = top_y + body_w / 2.0,
ny2 = top_y - body_w / 2.0,
trim = color_hex(trim),
bx = cx - body_w / 2.0,
by = top_y + body_w / 2.0,
bw = body_w,
bh = body_h - body_w / 2.0,
hull = color_hex(hull),
erx = body_w / 2.0,
ery = body_w / 5.0,
lf1 = cx - body_w / 2.0,
lf2 = cx - body_w,
lf3 = cx - body_w / 2.0,
rf1 = cx + body_w / 2.0,
rf2 = cx + body_w,
rf3 = cx + body_w / 2.0,
lfy1 = bottom_y - body_w / 2.0,
lfy2 = bottom_y + body_w / 3.0,
lfy3 = bottom_y,
windows = windows,
fx1 = cx - body_w / 4.0,
fx2 = cx + body_w / 4.0,
fy1 = bottom_y,
fy2 = bottom_y + h * (0.10 + identity.unit_f32(8) * 0.08),
flame = color_hex(flame),
)
}
fn render_mushroom_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.56;
let cap_rx = w * (0.24 + identity.unit_f32(4) * 0.08);
let cap_ry = h * (0.14 + identity.unit_f32(5) * 0.06);
let stem_rx = w * (0.09 + identity.unit_f32(6) * 0.04);
let stem_ry = h * (0.18 + identity.unit_f32(7) * 0.05);
let cap = hsl_to_color(350.0 + identity.unit_f32(1) * 45.0, 0.58, 0.52);
let stem = hsl_to_color(35.0 + identity.unit_f32(2) * 20.0, 0.24, 0.86);
let gill = hsl_to_color(26.0 + identity.unit_f32(3) * 20.0, 0.20, 0.70);
let spots = format!(
r##"<circle cx="{s1x}" cy="{s1y}" r="{sr1}" fill="#fff6e6" opacity="0.92"/><circle cx="{s2x}" cy="{s2y}" r="{sr2}" fill="#fff6e6" opacity="0.92"/><circle cx="{s3x}" cy="{s3y}" r="{sr3}" fill="#fff6e6" opacity="0.92"/>"##,
s1x = cx - cap_rx * 0.36,
s2x = cx + cap_rx * 0.08,
s3x = cx + cap_rx * 0.40,
s1y = cy - cap_ry * 0.88,
s2y = cy - cap_ry * 0.58,
s3y = cy - cap_ry * 0.76,
sr1 = cap_rx * (0.06 + identity.unit_f32(19) * 0.04),
sr2 = cap_rx * (0.07 + identity.unit_f32(20) * 0.04),
sr3 = cap_rx * (0.05 + identity.unit_f32(21) * 0.04),
);
format!(
r##"<ellipse cx="{cx}" cy="{scy}" rx="{srx}" ry="{sry}" fill="{stem}"/><ellipse cx="{cx}" cy="{ccy}" rx="{crx}" ry="{cry}" fill="{cap}"/><rect x="{rx}" y="{ry}" width="{rw}" height="{rh}" fill="{cap}"/><ellipse cx="{cx}" cy="{gcy}" rx="{grx}" ry="{gry}" fill="{gill}"/>{spots}"##,
cx = cx,
scy = cy + stem_ry / 3.0,
srx = stem_rx,
sry = stem_ry,
stem = color_hex(stem),
ccy = cy - cap_ry / 2.0,
crx = cap_rx,
cry = cap_ry,
cap = color_hex(cap),
rx = cx - cap_rx,
ry = cy - cap_ry / 2.0,
rw = cap_rx * 2.0,
rh = cap_ry,
gcy = cy + cap_ry / 3.0,
grx = cap_rx,
gry = cap_ry / 3.0,
gill = color_hex(gill),
spots = spots,
)
}
fn render_cactus_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.58;
let body_w = w * (0.13 + identity.unit_f32(4) * 0.04);
let body_h = h * (0.36 + identity.unit_f32(5) * 0.10);
let top_y = cy - body_h / 2.0;
let cactus = hsl_to_color(105.0 + identity.unit_f32(1) * 60.0, 0.42, 0.42);
let flower = hsl_to_color(320.0 + identity.unit_f32(3) * 55.0, 0.58, 0.64);
format!(
r##"<rect x="{bx}" y="{by}" width="{bw}" height="{bh}" rx="{br}" fill="{cactus}"/><circle cx="{cx}" cy="{by}" r="{br}" fill="{cactus}"/><rect x="{lax}" y="{lay}" width="{al}" height="{ah}" rx="{ar}" fill="{cactus}"/><circle cx="{lax}" cy="{lcy}" r="{ar}" fill="{cactus}"/><rect x="{rax}" y="{ray}" width="{al}" height="{ah}" rx="{ar}" fill="{cactus}"/><circle cx="{rcx}" cy="{rcy}" r="{ar}" fill="{cactus}"/><line x1="{n1x}" y1="{n1y}" x2="{n2x}" y2="{n2y}" stroke="#f2ffe0" stroke-width="2" opacity="0.7"/><line x1="{n3x}" y1="{n3y}" x2="{n4x}" y2="{n4y}" stroke="#f2ffe0" stroke-width="2" opacity="0.7"/><circle cx="{cx}" cy="{fy}" r="{fr}" fill="{flower}"/>"##,
bx = cx - body_w / 2.0,
by = top_y,
bw = body_w,
bh = body_h,
br = body_w / 2.0,
cactus = color_hex(cactus),
lax = cx - body_w * 1.45,
lay = cy - body_h * 0.13,
lcy = cy - body_h * 0.13 + body_w * 0.25,
rax = cx + body_w * 0.35,
ray = cy - body_h * 0.23,
rcx = cx + body_w * 1.45,
rcy = cy - body_h * 0.23 + body_w * 0.25,
al = body_w * 1.10,
ah = body_w * 0.50,
ar = body_w * 0.25,
n1x = cx - body_w * 0.12,
n1y = top_y + body_h * 0.34,
n2x = cx - body_w * 0.34,
n2y = top_y + body_h * 0.29,
n3x = cx + body_w * 0.10,
n3y = top_y + body_h * 0.58,
n4x = cx + body_w * 0.33,
n4y = top_y + body_h * 0.54,
fy = top_y - body_w * 0.42,
fr = body_w * (0.16 + identity.unit_f32(12) * 0.08),
flower = color_hex(flower),
)
}
fn render_frog_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * (0.57 + identity.unit_f32(6) * 0.04);
let rx = w * (0.24 + identity.unit_f32(4) * 0.06);
let ry = h * (0.18 + identity.unit_f32(5) * 0.05);
let green = hsl_to_color(92.0 + identity.unit_f32(1) * 72.0, 0.46, 0.54);
let dark = hsl_to_color(98.0 + identity.unit_f32(2) * 60.0, 0.40, 0.28);
let cheek = hsl_to_color(335.0 + identity.unit_f32(3) * 24.0, 0.42, 0.76);
let er = rx * (0.18 + identity.unit_f32(7) * 0.04);
format!(
r##"<circle cx="{elx}" cy="{ey}" r="{er}" fill="{green}"/><circle cx="{erx}" cy="{ey}" r="{er}" fill="{green}"/><ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="{green}"/><circle cx="{elx}" cy="{ey}" r="{ew}" fill="#fffff5"/><circle cx="{erx}" cy="{ey}" r="{ew}" fill="#fffff5"/><circle cx="{elx}" cy="{ey}" r="{pr}" fill="{dark}"/><circle cx="{erx}" cy="{ey}" r="{pr}" fill="{dark}"/><circle cx="{clx}" cy="{ccy}" r="{cr}" fill="{cheek}" opacity="0.6"/><circle cx="{crx}" cy="{ccy}" r="{cr}" fill="{cheek}" opacity="0.6"/><path d="M {mx1} {my} q {q1} {qd} {q2} 0 M {mx2} {my} q {q1} {qd} {q2} 0" stroke="{dark}" stroke-width="3" fill="none" stroke-linecap="round"/>"##,
cx = cx,
cy = cy,
rx = rx,
ry = ry,
green = color_hex(green),
elx = cx - rx * 0.50,
erx = cx + rx * 0.50,
ey = cy - ry,
er = er,
ew = er * 0.64,
pr = er * 0.30,
dark = color_hex(dark),
clx = cx - rx * 0.50,
crx = cx + rx * 0.50,
ccy = cy + ry * 0.25,
cr = rx * 0.09,
cheek = color_hex(cheek),
mx1 = cx - rx * 0.20,
mx2 = cx + rx * 0.02,
my = cy + ry * 0.22,
q1 = rx * 0.20,
qd = ry * 0.35,
q2 = rx * 0.36,
)
}
fn render_panda_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * (0.56 + identity.unit_f32(5) * 0.04);
let rx = w * (0.24 + identity.unit_f32(6) * 0.05);
let ry = h * (0.22 + identity.unit_f32(7) * 0.04);
let white = hsl_to_color(36.0 + identity.unit_f32(1) * 18.0, 0.10, 0.92);
let black = hsl_to_color(210.0 + identity.unit_f32(2) * 28.0, 0.10, 0.18);
let er = rx * (0.28 + identity.unit_f32(8) * 0.08);
format!(
r##"<circle cx="{lelx}" cy="{eary}" r="{er}" fill="{black}"/><circle cx="{rerx}" cy="{eary}" r="{er}" fill="{black}"/><ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="{white}"/><ellipse cx="{plx}" cy="{py}" rx="{prx}" ry="{pry}" fill="{black}"/><ellipse cx="{prx2}" cy="{py}" rx="{prx}" ry="{pry}" fill="{black}"/><circle cx="{plx}" cy="{py}" r="{eye}" fill="#f8f8f4"/><circle cx="{prx2}" cy="{py}" r="{eye}" fill="#f8f8f4"/><ellipse cx="{cx}" cy="{ny}" rx="{nrx}" ry="{nry}" fill="{black}"/><path d="M {mx1} {my} q {mq} {md} {me} 0 M {mx2} {my} q {mq} {md} {me} 0" stroke="{black}" stroke-width="3" fill="none" stroke-linecap="round"/>"##,
cx = cx,
cy = cy,
rx = rx,
ry = ry,
white = color_hex(white),
black = color_hex(black),
lelx = cx - rx * 0.75,
rerx = cx + rx * 0.75,
eary = cy - ry * 0.75,
er = er,
plx = cx - rx * 0.33,
prx2 = cx + rx * 0.33,
py = cy - ry * 0.08,
prx = rx * (0.20 + identity.unit_f32(9) * 0.05),
pry = ry * (0.26 + identity.unit_f32(10) * 0.05),
eye = rx * 0.055,
ny = cy + ry * 0.20,
nrx = rx * 0.09,
nry = ry * 0.12,
mx1 = cx - rx * 0.08,
mx2 = cx + rx * 0.02,
my = cy + ry * 0.26,
mq = rx * 0.12,
md = ry * 0.24,
me = rx * 0.22,
)
}
fn render_cupcake_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.58;
let cup_w = w * (0.26 + identity.unit_f32(5) * 0.07);
let cup_h = h * (0.22 + identity.unit_f32(6) * 0.05);
let frx = cup_w * (0.58 + identity.unit_f32(7) * 0.10);
let fry = h * (0.13 + identity.unit_f32(8) * 0.04);
let wrapper = hsl_to_color(28.0 + identity.unit_f32(1) * 35.0, 0.46, 0.62);
let frosting = hsl_to_color(identity.unit_f32(2) * 360.0, 0.38, 0.78);
let cherry = hsl_to_color(345.0 + identity.unit_f32(4) * 22.0, 0.66, 0.50);
let by = cy - fry / 2.0;
format!(
r##"<polygon points="{x1},{cy} {x2},{cy} {x3},{yb} {x4},{yb}" fill="{wrapper}"/><line x1="{sx1}" y1="{cy}" x2="{sx2}" y2="{yb}" stroke="#fff4d6" stroke-width="3" opacity="0.45"/><line x1="{sx3}" y1="{cy}" x2="{sx4}" y2="{yb}" stroke="#fff4d6" stroke-width="3" opacity="0.45"/><ellipse cx="{cx}" cy="{f1y}" rx="{frx}" ry="{fry}" fill="{frosting}"/><ellipse cx="{cx}" cy="{f2y}" rx="{frx2}" ry="{fry2}" fill="{frosting}"/><ellipse cx="{cx}" cy="{f3y}" rx="{frx3}" ry="{fry3}" fill="{frosting}"/><rect x="{spx1}" y="{spy1}" width="{spw}" height="{sph}" fill="#f05f7e"/><rect x="{spx2}" y="{spy2}" width="{spw}" height="{sph}" fill="#5fb6f0"/><rect x="{spx3}" y="{spy3}" width="{spw}" height="{sph}" fill="#f0d15f"/><circle cx="{cx}" cy="{chy}" r="{chr}" fill="{cherry}"/>"##,
cx = cx,
cy = cy,
x1 = cx - cup_w / 2.0,
x2 = cx + cup_w / 2.0,
x3 = cx + cup_w / 3.0,
x4 = cx - cup_w / 3.0,
yb = cy + cup_h,
wrapper = color_hex(wrapper),
sx1 = cx - cup_w * 0.25,
sx2 = cx - cup_w * 0.16,
sx3 = cx + cup_w * 0.25,
sx4 = cx + cup_w * 0.16,
f1y = by,
f2y = by - fry * 0.50,
f3y = by - fry,
frx = frx,
fry = fry,
frx2 = frx * 0.78,
fry2 = fry * 0.72,
frx3 = frx * 0.56,
fry3 = fry * 0.62,
frosting = color_hex(frosting),
spx1 = cx - frx * 0.35,
spx2 = cx + frx * 0.10,
spx3 = cx - frx * 0.02,
spy1 = by - fry * 0.50,
spy2 = by - fry * 0.88,
spy3 = by - fry * 0.12,
spw = w * 0.035,
sph = h * 0.012,
chy = by - fry,
chr = w * 0.035,
cherry = color_hex(cherry),
)
}
fn render_pizza_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.53;
let half_w = w * (0.22 + identity.unit_f32(5) * 0.06);
let slice_h = h * (0.44 + identity.unit_f32(6) * 0.07);
let top_y = cy - slice_h / 2.0;
let tip_y = cy + slice_h / 2.0;
let crust = hsl_to_color(30.0 + identity.unit_f32(1) * 28.0, 0.54, 0.58);
let cheese = hsl_to_color(45.0 + identity.unit_f32(2) * 16.0, 0.74, 0.70);
let sauce = hsl_to_color(8.0 + identity.unit_f32(3) * 16.0, 0.62, 0.48);
let topping = hsl_to_color(350.0 + identity.unit_f32(4) * 22.0, 0.54, 0.46);
format!(
r##"<polygon points="{x1},{ty} {x2},{ty} {cx},{tip}" fill="{cheese}"/><polygon points="{sx1},{sy} {sx2},{sy} {cx},{stip}" fill="{sauce}" opacity="0.38"/><ellipse cx="{cx}" cy="{ty}" rx="{half_w}" ry="{crh}" fill="{crust}"/><circle cx="{p1x}" cy="{p1y}" r="{pr}" fill="{topping}"/><circle cx="{p2x}" cy="{p2y}" r="{pr2}" fill="{topping}"/><circle cx="{p3x}" cy="{p3y}" r="{pr3}" fill="{topping}"/>"##,
cx = cx,
x1 = cx - half_w,
x2 = cx + half_w,
ty = top_y,
tip = tip_y,
cheese = color_hex(cheese),
sx1 = cx - half_w * 0.78,
sx2 = cx + half_w * 0.78,
sy = top_y + slice_h * 0.20,
stip = tip_y - slice_h * 0.10,
sauce = color_hex(sauce),
half_w = half_w,
crh = h * 0.035,
crust = color_hex(crust),
p1x = cx - half_w * 0.35,
p2x = cx + half_w * 0.28,
p3x = cx,
p1y = top_y + slice_h * 0.28,
p2y = top_y + slice_h * 0.40,
p3y = top_y + slice_h * 0.62,
pr = w * (0.025 + identity.unit_f32(14) * 0.012),
pr2 = w * (0.026 + identity.unit_f32(15) * 0.012),
pr3 = w * (0.024 + identity.unit_f32(16) * 0.012),
topping = color_hex(topping),
)
}
fn render_icecream_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.55;
let scoop_r = w * (0.18 + identity.unit_f32(4) * 0.06);
let cone_w = w * (0.24 + identity.unit_f32(5) * 0.05);
let cone_h = h * (0.32 + identity.unit_f32(6) * 0.06);
let scoop_y = cy - scoop_r / 2.0;
let cone_top = scoop_y + scoop_r / 2.0;
let scoop = hsl_to_color(identity.unit_f32(1) * 360.0, 0.42, 0.76);
let cone = hsl_to_color(32.0 + identity.unit_f32(2) * 22.0, 0.50, 0.64);
let waffle = hsl_to_color(28.0 + identity.unit_f32(3) * 22.0, 0.42, 0.45);
format!(
r##"<polygon points="{x1},{ct} {x2},{ct} {cx},{cb}" fill="{cone}"/><line x1="{lx1}" y1="{ly1}" x2="{cx}" y2="{ly2}" stroke="{waffle}" stroke-width="3"/><line x1="{lx2}" y1="{ly1}" x2="{cx}" y2="{ly2}" stroke="{waffle}" stroke-width="3"/><circle cx="{cx}" cy="{sy}" r="{sr}" fill="{scoop}"/><circle cx="{d1x}" cy="{d1y}" r="{dr1}" fill="{scoop}"/><circle cx="{d2x}" cy="{d2y}" r="{dr2}" fill="{scoop}"/><circle cx="{c1x}" cy="{c1y}" r="{chip}" fill="{waffle}"/><circle cx="{c2x}" cy="{c2y}" r="{chip}" fill="{waffle}"/>"##,
cx = cx,
x1 = cx - cone_w / 2.0,
x2 = cx + cone_w / 2.0,
ct = cone_top,
cb = cone_top + cone_h,
cone = color_hex(cone),
lx1 = cx - cone_w / 3.0,
lx2 = cx + cone_w / 3.0,
ly1 = cone_top + cone_h / 8.0,
ly2 = cone_top + cone_h * 0.75,
waffle = color_hex(waffle),
sy = scoop_y,
sr = scoop_r,
scoop = color_hex(scoop),
d1x = cx - scoop_r / 2.0,
d1y = scoop_y + scoop_r / 3.0,
dr1 = scoop_r / 5.0,
d2x = cx + scoop_r / 3.0,
d2y = scoop_y + scoop_r / 2.0,
dr2 = scoop_r / 6.0,
c1x = cx - scoop_r * 0.25,
c1y = scoop_y - scoop_r * 0.18,
c2x = cx + scoop_r * 0.18,
c2y = scoop_y + scoop_r * 0.12,
chip = w * 0.010,
)
}
fn render_octopus_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * (0.54 + identity.unit_f32(5) * 0.05);
let rx = w * (0.21 + identity.unit_f32(3) * 0.06);
let ry = h * (0.20 + identity.unit_f32(4) * 0.06);
let body = hsl_to_color(identity.unit_f32(1) * 360.0, 0.42, 0.58);
let shade = hsl_to_color(identity.unit_f32(2) * 360.0, 0.34, 0.38);
format!(
r##"<ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="{body}"/><rect x="{t1x}" y="{ty}" width="{tw}" height="{th1}" rx="{tr}" fill="{body}"/><rect x="{t2x}" y="{ty}" width="{tw}" height="{th2}" rx="{tr}" fill="{body}"/><rect x="{t3x}" y="{ty}" width="{tw}" height="{th3}" rx="{tr}" fill="{body}"/><rect x="{t4x}" y="{ty}" width="{tw}" height="{th4}" rx="{tr}" fill="{body}"/><circle cx="{elx}" cy="{ey}" r="{er}" fill="#fffff8"/><circle cx="{erx}" cy="{ey}" r="{er}" fill="#fffff8"/><circle cx="{elx}" cy="{ey}" r="{pr}" fill="#1c1a26"/><circle cx="{erx}" cy="{ey}" r="{pr}" fill="#1c1a26"/><path d="M {mx1} {my} q {mq} {md} {me} 0 M {mx2} {my} q {mq} {md} {me} 0" stroke="{shade}" stroke-width="3" fill="none" stroke-linecap="round"/>"##,
cx = cx,
cy = cy,
rx = rx,
ry = ry,
body = color_hex(body),
t1x = cx - rx * 0.82,
t2x = cx - rx * 0.28,
t3x = cx + rx * 0.20,
t4x = cx + rx * 0.68,
ty = cy + ry * 0.45,
tw = rx * 0.14,
th1 = ry * (0.50 + identity.unit_f32(7) * 0.30),
th2 = ry * (0.42 + identity.unit_f32(8) * 0.30),
th3 = ry * (0.45 + identity.unit_f32(9) * 0.30),
th4 = ry * (0.50 + identity.unit_f32(10) * 0.30),
tr = rx * 0.07,
elx = cx - rx / 3.0,
erx = cx + rx / 3.0,
ey = cy - ry / 6.0,
er = rx / 9.0,
pr = rx / 20.0,
mx1 = cx - rx * 0.12,
mx2 = cx + rx * 0.02,
my = cy + ry * 0.30,
mq = rx * 0.14,
md = ry * 0.22,
me = rx * 0.24,
shade = color_hex(shade),
)
}
fn render_knight_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.55;
let rx = w * (0.20 + identity.unit_f32(4) * 0.05);
let ry = h * (0.24 + identity.unit_f32(5) * 0.05);
let steel = hsl_to_color(205.0 + identity.unit_f32(1) * 45.0, 0.12, 0.66);
let dark = hsl_to_color(215.0 + identity.unit_f32(2) * 45.0, 0.14, 0.22);
let plume = hsl_to_color(identity.unit_f32(3) * 360.0, 0.58, 0.54);
format!(
r##"<ellipse cx="{cx}" cy="{hy}" rx="{rx}" ry="{ry}" fill="{steel}"/><rect x="{x}" y="{y}" width="{rw}" height="{rh}" fill="{steel}"/><rect x="{vx}" y="{vy}" width="{vw}" height="{vh}" fill="{dark}"/><rect x="{s1x}" y="{vy}" width="{sw}" height="{vh}" fill="#ffffff" opacity="0.35"/><rect x="{s2x}" y="{vy}" width="{sw}" height="{vh}" fill="#ffffff" opacity="0.35"/><line x1="{cx}" y1="{ly1}" x2="{cx}" y2="{ly2}" stroke="#ffffff" stroke-width="3" opacity="0.5"/><polygon points="{cx},{py1} {px2},{py2} {px3},{py3}" fill="{plume}"/>"##,
cx = cx,
hy = cy - ry / 5.0,
rx = rx,
ry = ry,
steel = color_hex(steel),
x = cx - rx,
y = cy - ry / 5.0,
rw = rx * 2.0,
rh = ry * 1.20,
vx = cx - rx * 0.75,
vy = cy - ry / 5.0,
vw = rx * 1.50,
vh = ry * 0.20,
dark = color_hex(dark),
s1x = cx - rx * 0.34,
s2x = cx + rx * 0.22,
sw = rx * 0.10,
ly1 = cy - ry,
ly2 = cy + ry,
py1 = cy - ry,
px2 = cx - rx * 0.20,
py2 = cy - ry * 1.50,
px3 = cx + rx * 0.25,
py3 = cy - ry * 1.32,
plume = color_hex(plume),
)
}
fn render_bear_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.56;
let rx = w * (0.27 + identity.unit_f32(4) * 0.05);
let ry = h * (0.24 + identity.unit_f32(5) * 0.05);
let fur = hsl_to_color(24.0 + identity.unit_f32(1) * 24.0, 0.38, 0.48);
let muzzle = hsl_to_color(32.0 + identity.unit_f32(2) * 12.0, 0.22, 0.84);
let inner = hsl_to_color(18.0 + identity.unit_f32(3) * 18.0, 0.34, 0.72);
format!(
r##"<circle cx="{lel}" cy="{ey}" r="{er}" fill="{fur}"/><circle cx="{rel}" cy="{ey}" r="{er}" fill="{fur}"/><circle cx="{lel}" cy="{ey}" r="{ir}" fill="{inner}"/><circle cx="{rel}" cy="{ey}" r="{ir}" fill="{inner}"/><ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="{fur}"/><ellipse cx="{cx}" cy="{my}" rx="{mrx}" ry="{mry}" fill="{muzzle}"/><circle cx="{lx}" cy="{eye_y}" r="{pr}" fill="#2d221c"/><circle cx="{rx2}" cy="{eye_y}" r="{pr}" fill="#2d221c"/><ellipse cx="{cx}" cy="{ny}" rx="{nrx}" ry="{nry}" fill="#2d221c"/><path d="M {mx1} {mouth_y} Q {cx} {curve_y} {mx2} {mouth_y}" stroke="#2d221c" stroke-width="3" fill="none" stroke-linecap="round"/>"##,
lel = cx - rx * 0.75,
rel = cx + rx * 0.75,
ey = cy - ry,
er = rx * 0.28,
ir = rx * 0.14,
cx = cx,
cy = cy,
rx = rx,
ry = ry,
fur = color_hex(fur),
inner = color_hex(inner),
my = cy + ry * 0.25,
mrx = rx * 0.40,
mry = ry * 0.34,
muzzle = color_hex(muzzle),
lx = cx - rx / 3.0,
rx2 = cx + rx / 3.0,
eye_y = cy - ry * 0.20,
pr = rx * 0.10,
ny = cy + ry * 0.16,
nrx = rx * 0.13,
nry = ry * 0.10,
mx1 = cx - rx * 0.18,
mx2 = cx + rx * 0.18,
mouth_y = cy + ry * 0.30,
curve_y = cy + ry * 0.42,
)
}
fn render_penguin_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.56;
let rx = w * 0.25;
let ry = h * 0.34;
let black = hsl_to_color(210.0 + identity.unit_f32(1) * 30.0, 0.22, 0.18);
let white = hsl_to_color(205.0 + identity.unit_f32(2) * 25.0, 0.16, 0.94);
let orange = hsl_to_color(32.0 + identity.unit_f32(3) * 18.0, 0.72, 0.58);
let beak_points = format!(
"{},{} {},{} {},{}",
cx - rx * 0.14,
cy - ry * 0.16,
cx + rx * 0.14,
cy - ry * 0.16,
cx,
cy
);
format!(
r##"<ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="{black}"/><ellipse cx="{cx}" cy="{by}" rx="{brx}" ry="{bry}" fill="{white}"/><ellipse cx="{lx}" cy="{wy}" rx="{wrx}" ry="{wry}" fill="{black}"/><ellipse cx="{rx2}" cy="{wy}" rx="{wrx}" ry="{wry}" fill="{black}"/><circle cx="{ex1}" cy="{ey}" r="{er}" fill="#0a0f14"/><circle cx="{ex2}" cy="{ey}" r="{er}" fill="#0a0f14"/><polygon points="{bp}" fill="{orange}"/><ellipse cx="{fx1}" cy="{fy}" rx="{frx}" ry="{fry}" fill="{orange}"/><ellipse cx="{fx2}" cy="{fy}" rx="{frx}" ry="{fry}" fill="{orange}"/>"##,
cx = cx,
cy = cy,
rx = rx,
ry = ry,
black = color_hex(black),
by = cy + ry / 6.0,
brx = rx * 0.60,
bry = ry * 0.67,
white = color_hex(white),
lx = cx - rx,
rx2 = cx + rx,
wy = cy + ry * 0.10,
wrx = rx * 0.25,
wry = ry * 0.50,
ex1 = cx - rx / 3.0,
ex2 = cx + rx / 3.0,
ey = cy - ry / 3.0,
er = rx * 0.10,
bp = beak_points,
orange = color_hex(orange),
fx1 = cx - rx / 3.0,
fx2 = cx + rx / 3.0,
fy = cy + ry,
frx = rx * 0.25,
fry = ry * 0.10,
)
}
fn render_dragon_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.57;
let rx = w * 0.27;
let ry = h * 0.23;
let scale = hsl_to_color(105.0 + identity.unit_f32(1) * 70.0, 0.46, 0.46);
let belly = hsl_to_color(70.0 + identity.unit_f32(2) * 35.0, 0.42, 0.72);
let horn = hsl_to_color(40.0 + identity.unit_f32(3) * 20.0, 0.34, 0.84);
let flame = hsl_to_color(14.0 + identity.unit_f32(4) * 25.0, 0.78, 0.56);
let left_horn = format!(
"{},{} {},{} {},{}",
cx - rx / 2.0,
cy - ry,
cx - rx / 4.0,
cy - ry * 1.60,
cx - rx / 8.0,
cy - ry
);
let right_horn = format!(
"{},{} {},{} {},{}",
cx + rx / 2.0,
cy - ry,
cx + rx / 4.0,
cy - ry * 1.60,
cx + rx / 8.0,
cy - ry
);
let spike1 = format!(
"{},{} {},{} {},{}",
cx - rx * 0.27,
cy - ry,
cx - rx * 0.20,
cy - ry * 1.25,
cx - rx * 0.13,
cy - ry
);
let spike2 = format!(
"{},{} {},{} {},{}",
cx - rx * 0.07,
cy - ry,
cx,
cy - ry * 1.25,
cx + rx * 0.07,
cy - ry
);
let spike3 = format!(
"{},{} {},{} {},{}",
cx + rx * 0.13,
cy - ry,
cx + rx * 0.20,
cy - ry * 1.25,
cx + rx * 0.27,
cy - ry
);
format!(
r##"<polygon points="{lh}" fill="{horn}"/><polygon points="{rh}" fill="{horn}"/><ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="{scale}"/><ellipse cx="{cx}" cy="{my}" rx="{mrx}" ry="{mry}" fill="{belly}"/><circle cx="{lx}" cy="{ey}" r="{ew}" fill="#ffffff"/><circle cx="{rx2}" cy="{ey}" r="{ew}" fill="#ffffff"/><circle cx="{lx}" cy="{ey}" r="{pr}" fill="#183022"/><circle cx="{rx2}" cy="{ey}" r="{pr}" fill="#183022"/><circle cx="{nx1}" cy="{ny}" r="{nr}" fill="#183022"/><circle cx="{nx2}" cy="{ny}" r="{nr}" fill="#183022"/><polygon points="{s1}" fill="{flame}"/><polygon points="{s2}" fill="{flame}"/><polygon points="{s3}" fill="{flame}"/>"##,
lh = left_horn,
rh = right_horn,
horn = color_hex(horn),
cx = cx,
cy = cy,
rx = rx,
ry = ry,
scale = color_hex(scale),
my = cy + ry * 0.25,
mrx = rx * 0.50,
mry = ry * 0.34,
belly = color_hex(belly),
lx = cx - rx / 3.0,
rx2 = cx + rx / 3.0,
ey = cy - ry / 5.0,
ew = rx * 0.10,
pr = rx * 0.05,
nx1 = cx - rx / 7.0,
nx2 = cx + rx / 7.0,
ny = cy + ry / 3.0,
nr = rx * 0.04,
s1 = spike1,
s2 = spike2,
s3 = spike3,
flame = color_hex(flame),
)
}
fn render_ninja_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.56;
let r = w.min(h) * 0.28;
let cloth = hsl_to_color(220.0 + identity.unit_f32(1) * 50.0, 0.18, 0.14);
let skin = hsl_to_color(28.0 + identity.unit_f32(2) * 18.0, 0.42, 0.72);
let band = hsl_to_color(identity.unit_f32(3) * 360.0, 0.56, 0.50);
let tie = format!(
"{},{} {},{} {},{}",
cx + r * 0.80,
cy - r * 0.67,
cx + r * 1.40,
cy - r,
cx + r,
cy - r * 0.33
);
format!(
r##"<circle cx="{cx}" cy="{cy}" r="{r}" fill="{cloth}"/><rect x="{sx}" y="{sy}" width="{sw}" height="{sh}" fill="{skin}"/><rect x="{bx}" y="{by}" width="{bw}" height="{bh}" fill="{band}"/><ellipse cx="{ex1}" cy="{ey}" rx="{erx}" ry="{ery}" fill="#141820"/><ellipse cx="{ex2}" cy="{ey}" rx="{erx}" ry="{ery}" fill="#141820"/><polygon points="{tie}" fill="{band}"/>"##,
cx = cx,
cy = cy,
r = r,
cloth = color_hex(cloth),
sx = cx - r * 0.60,
sy = cy - r * 0.25,
sw = r * 1.20,
sh = r * 0.50,
skin = color_hex(skin),
bx = cx - r,
by = cy - r * 0.67,
bw = r * 2.0,
bh = r * 0.17,
band = color_hex(band),
ex1 = cx - r / 3.0,
ex2 = cx + r / 3.0,
ey = cy - r * 0.08,
erx = r * 0.11,
ery = r * 0.07,
tie = tie,
)
}
fn render_astronaut_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.54;
let r = w.min(h) * 0.28;
let suit = hsl_to_color(205.0 + identity.unit_f32(1) * 35.0, 0.16, 0.90);
let visor = hsl_to_color(195.0 + identity.unit_f32(2) * 55.0, 0.52, 0.56);
let trim = hsl_to_color(identity.unit_f32(3) * 360.0, 0.45, 0.55);
format!(
r##"<rect x="{sx}" y="{sy}" width="{sw}" height="{sh}" fill="{suit}"/><circle cx="{cx}" cy="{cy}" r="{r}" fill="{suit}" stroke="#606e80" stroke-width="3"/><ellipse cx="{cx}" cy="{cy}" rx="{vrx}" ry="{vry}" fill="{visor}"/><rect x="{glx}" y="{gly}" width="{glw}" height="{glh}" fill="#ffffff" opacity="0.35"/><circle cx="{px}" cy="{py}" r="{pr}" fill="{trim}"/>"##,
sx = cx - r * 0.50,
sy = cy + r * 0.50,
sw = r,
sh = r * 0.60,
suit = color_hex(suit),
cx = cx,
cy = cy,
r = r,
vrx = r * 0.67,
vry = r * 0.50,
visor = color_hex(visor),
glx = cx - r / 3.0,
gly = cy - r / 4.0,
glw = r / 2.0,
glh = r / 8.0,
px = cx + r / 2.0,
py = cy + r * 0.67,
pr = r * 0.10,
trim = color_hex(trim),
)
}
fn render_diamond_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.56;
let rx = w * 0.25;
let ry = h * 0.30;
let gem = hsl_to_color(180.0 + identity.unit_f32(1) * 95.0, 0.55, 0.60);
let highlight = hsl_to_color(190.0 + identity.unit_f32(2) * 70.0, 0.40, 0.82);
let shade = hsl_to_color(200.0 + identity.unit_f32(3) * 70.0, 0.42, 0.42);
let outer = format!(
"{},{} {},{} {},{} {},{} {},{}",
cx - rx,
cy - ry / 3.0,
cx - rx / 2.0,
cy - ry,
cx + rx / 2.0,
cy - ry,
cx + rx,
cy - ry / 3.0,
cx,
cy + ry
);
let left = format!(
"{},{} {},{} {},{} {},{}",
cx - rx,
cy - ry / 3.0,
cx - rx / 2.0,
cy - ry,
cx,
cy + ry,
cx - rx / 5.0,
cy - ry / 3.0
);
let right = format!(
"{},{} {},{} {},{} {},{}",
cx + rx / 2.0,
cy - ry,
cx + rx,
cy - ry / 3.0,
cx,
cy + ry,
cx + rx / 5.0,
cy - ry / 3.0
);
format!(
r##"<polygon points="{outer}" fill="{gem}"/><polygon points="{left}" fill="{highlight}"/><polygon points="{right}" fill="{shade}"/><line x1="{l1}" y1="{top}" x2="{cx}" y2="{bottom}" stroke="#ffffff" stroke-width="2" opacity="0.45"/><line x1="{cx}" y1="{top}" x2="{cx}" y2="{bottom}" stroke="#ffffff" stroke-width="2" opacity="0.45"/><line x1="{r1}" y1="{top}" x2="{cx}" y2="{bottom}" stroke="#ffffff" stroke-width="2" opacity="0.45"/>"##,
outer = outer,
gem = color_hex(gem),
left = left,
right = right,
highlight = color_hex(highlight),
shade = color_hex(shade),
l1 = cx - rx / 2.0,
r1 = cx + rx / 2.0,
top = cy - ry,
bottom = cy + ry,
cx = cx,
)
}
fn render_coffee_cup_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.58;
let cw = w * 0.38;
let ch = h * 0.32;
let cup = hsl_to_color(20.0 + identity.unit_f32(1) * 35.0, 0.42, 0.60);
let coffee = hsl_to_color(24.0 + identity.unit_f32(2) * 18.0, 0.42, 0.26);
format!(
r##"<rect x="{x}" y="{y}" width="{cw}" height="{ch}" fill="{cup}"/><ellipse cx="{cx}" cy="{top}" rx="{rx}" ry="{ery}" fill="{coffee}"/><ellipse cx="{hx}" cy="{hy}" rx="{hrx}" ry="{hry}" fill="none" stroke="{cup}" stroke-width="5"/><rect x="{px}" y="{py}" width="{pw}" height="{ph}" fill="#50372a" opacity="0.7"/><path d="M {s1} {sy} L {s1e} {sy2} M {s2} {sy} L {s2e} {sy2} M {s3} {sy} L {s3e} {sy2}" stroke="#786252" stroke-width="3" opacity="0.55" stroke-linecap="round"/>"##,
x = cx - cw / 2.0,
y = cy - ch / 2.0,
cw = cw,
ch = ch,
cup = color_hex(cup),
cx = cx,
top = cy - ch / 2.0,
rx = cw / 2.0,
ery = ch / 7.0,
coffee = color_hex(coffee),
hx = cx + cw / 2.0,
hy = cy - ch / 10.0,
hrx = cw / 4.0,
hry = ch / 4.0,
px = cx - cw * 0.60,
py = cy + ch / 2.0,
pw = cw * 1.20,
ph = ch / 8.0,
s1 = cx - cw / 5.0,
s2 = cx,
s3 = cx + cw / 5.0,
sy = cy - ch,
s1e = cx - cw / 10.0,
s2e = cx + cw / 10.0,
s3e = cx + cw * 0.30,
sy2 = cy - ch * 1.33,
)
}
fn render_shield_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let cx = w / 2.0;
let cy = h * 0.55;
let rx = w * 0.25;
let ry = h * 0.32;
let metal = hsl_to_color(210.0 + identity.unit_f32(1) * 45.0, 0.28, 0.58);
let accent = hsl_to_color(identity.unit_f32(2) * 360.0, 0.50, 0.50);
let light = hsl_to_color(210.0 + identity.unit_f32(3) * 35.0, 0.18, 0.82);
let outer = format!(
"{},{} {},{} {},{} {},{} {},{}",
cx - rx,
cy - ry,
cx + rx,
cy - ry,
cx + rx * 0.80,
cy + ry * 0.25,
cx,
cy + ry,
cx - rx * 0.80,
cy + ry * 0.25
);
let left = format!(
"{},{} {},{} {},{} {},{}",
cx - rx,
cy - ry,
cx,
cy - ry,
cx,
cy + ry,
cx - rx * 0.80,
cy + ry * 0.25
);
format!(
r##"<polygon points="{outer}" fill="{metal}"/><polygon points="{left}" fill="{light}"/><rect x="{vx}" y="{vy}" width="{vw}" height="{vh}" fill="{accent}"/><rect x="{hx}" y="{hy}" width="{hw}" height="{hh}" fill="{accent}"/>"##,
outer = outer,
metal = color_hex(metal),
left = left,
light = color_hex(light),
vx = cx - rx * 0.125,
vy = cy - ry * 0.75,
vw = rx * 0.25,
vh = ry * 1.20,
hx = cx - rx * 0.67,
hy = cy - ry * 0.20,
hw = rx * 1.34,
hh = ry * 0.25,
accent = color_hex(accent),
)
}
fn render_paws_svg(spec: AvatarSpec, identity: &AvatarIdentity) -> String {
let w = spec.width as f32;
let h = spec.height as f32;
let paw = hsl_to_color(identity.unit_f32(1) * 360.0, 0.38, 0.62);
let pad = hsl_to_color(330.0 + identity.unit_f32(3) * 20.0, 0.40, 0.74);
let cx = w * 0.52;
let cy = h * 0.60;
format!(
r##"<ellipse cx="{cx}" cy="{cy}" rx="{prx}" ry="{pry}" fill="{paw}"/><ellipse cx="{cx}" cy="{py2}" rx="{padrx}" ry="{padry}" fill="{pad}"/><ellipse cx="{t1x}" cy="{ty1}" rx="{trx}" ry="{try_}" fill="{paw}"/><ellipse cx="{t2x}" cy="{ty2}" rx="{trx}" ry="{try_}" fill="{paw}"/><ellipse cx="{t3x}" cy="{ty2}" rx="{trx}" ry="{try_}" fill="{paw}"/><ellipse cx="{t4x}" cy="{ty1}" rx="{trx}" ry="{try_}" fill="{paw}"/><ellipse cx="{t1x}" cy="{ty1a}" rx="{padrx2}" ry="{padry2}" fill="{pad}"/><ellipse cx="{t2x}" cy="{ty2a}" rx="{padrx2}" ry="{padry2}" fill="{pad}"/><ellipse cx="{t3x}" cy="{ty2a}" rx="{padrx2}" ry="{padry2}" fill="{pad}"/><ellipse cx="{t4x}" cy="{ty1a}" rx="{padrx2}" ry="{padry2}" fill="{pad}"/>"##,
cx = cx,
cy = cy,
prx = w * 0.13,
pry = h * 0.15,
paw = color_hex(paw),
py2 = cy + h * 0.015,
padrx = w * 0.09,
padry = h * 0.10,
pad = color_hex(pad),
t1x = cx - w * 0.12,
t2x = cx - w * 0.04,
t3x = cx + w * 0.04,
t4x = cx + w * 0.12,
ty1 = cy - h * 0.16,
ty2 = cy - h * 0.14,
ty1a = cy - h * 0.15,
ty2a = cy - h * 0.13,
trx = w * 0.035,
try_ = h * 0.05,
padrx2 = w * 0.022,
padry2 = h * 0.032,
)
}
fn encode_rgba_image(image: &RgbaImage, format: AvatarOutputFormat) -> ImageResult<Vec<u8>> {
let mut bytes = Vec::new();
{
let cursor = Cursor::new(&mut bytes);
encode_into_writer(image, format, cursor)?;
}
Ok(bytes)
}
fn encode_owned_rgba_image(image: RgbaImage, format: AvatarOutputFormat) -> ImageResult<Vec<u8>> {
let image = ZeroizingRgbaImage::new(image);
encode_rgba_image(image.as_image(), format)
}
struct ZeroizingRgbaImage {
image: RgbaImage,
}
impl ZeroizingRgbaImage {
fn new(image: RgbaImage) -> Self {
Self { image }
}
fn as_image(&self) -> &RgbaImage {
&self.image
}
}
impl Drop for ZeroizingRgbaImage {
fn drop(&mut self) {
zeroize_rgba_pixels(&mut self.image);
}
}
fn zeroize_rgba_pixels(image: &mut RgbaImage) {
let pixels: &mut [u8] = image.as_mut();
pixels.zeroize();
}
fn encode_into_writer<W: std::io::Write>(
image: &RgbaImage,
format: AvatarOutputFormat,
writer: W,
) -> ImageResult<()> {
match format {
AvatarOutputFormat::WebP => WebPEncoder::new_lossless(writer).write_image(
image.as_raw(),
image.width(),
image.height(),
ExtendedColorType::Rgba8,
),
#[cfg(feature = "png")]
AvatarOutputFormat::Png => {
PngEncoder::new_with_quality(writer, CompressionType::Best, FilterType::Adaptive)
.write_image(
image.as_raw(),
image.width(),
image.height(),
ExtendedColorType::Rgba8,
)
}
#[cfg(feature = "jpeg")]
AvatarOutputFormat::Jpeg => {
let rgb = Zeroizing::new(rgba_to_rgb_over_white(image));
JpegEncoder::new_with_quality(writer, 92).write_image(
rgb.as_slice(),
image.width(),
image.height(),
ExtendedColorType::Rgb8,
)
}
#[cfg(feature = "gif")]
AvatarOutputFormat::Gif => GifEncoder::new(writer).write_image(
image.as_raw(),
image.width(),
image.height(),
ExtendedColorType::Rgba8,
),
}
}
#[cfg(any(feature = "jpeg", test))]
fn rgba_to_rgb_over_white(image: &RgbaImage) -> Vec<u8> {
let mut rgb = Vec::with_capacity(image.as_raw().len() / 4 * 3);
for pixel in image.pixels() {
let [red, green, blue, alpha] = pixel.0;
let alpha = u32::from(alpha);
let inverse_alpha = 255 - alpha;
rgb.push(((u32::from(red) * alpha + 255 * inverse_alpha + 127) / 255) as u8);
rgb.push(((u32::from(green) * alpha + 255 * inverse_alpha + 127) / 255) as u8);
rgb.push(((u32::from(blue) * alpha + 255 * inverse_alpha + 127) / 255) as u8);
}
rgb
}
#[cfg(test)]
mod tests {
use super::*;
use image::ImageFormat;
use sha2::Sha512 as TestSha512;
fn valid_spec(width: u32, height: u32, seed: u64) -> AvatarSpec {
AvatarSpec::new(width, height, seed).expect("test avatar spec should be valid")
}
fn valid_namespace<'a>(tenant: &'a str, style_version: &'a str) -> AvatarNamespace<'a> {
super::AvatarNamespace::new(tenant, style_version).expect("test namespace should be valid")
}
fn valid_identity<T: AsRef<[u8]>>(input: T) -> AvatarIdentity {
super::AvatarIdentity::new(input).expect("test identity should be valid")
}
fn valid_identity_with_namespace<T: AsRef<[u8]>>(
namespace: AvatarNamespace<'_>,
input: T,
) -> AvatarIdentity {
AvatarIdentity::new_with_namespace(namespace, input).expect("test identity should be valid")
}
fn render_avatar_for_id<T: AsRef<[u8]>>(
spec: AvatarSpec,
id: T,
options: AvatarOptions,
) -> RgbaImage {
super::render_avatar_for_id(spec, id, options).expect("valid avatar spec should render")
}
fn render_avatar_svg_for_id<T: AsRef<[u8]>>(
spec: AvatarSpec,
id: T,
options: AvatarOptions,
) -> String {
super::render_avatar_svg_for_id(spec, id, options)
.expect("valid avatar spec should render as svg")
}
fn render_avatar_style_for_id<T: AsRef<[u8]>>(
spec: AvatarSpec,
id: T,
style: AvatarStyleOptions,
) -> RgbaImage {
super::render_avatar_style_for_id(spec, id, style)
.expect("valid avatar style should render")
}
fn render_avatar_svg_style_for_id<T: AsRef<[u8]>>(
spec: AvatarSpec,
id: T,
style: AvatarStyleOptions,
) -> String {
super::render_avatar_svg_style_for_id(spec, id, style)
.expect("valid avatar style should render as svg")
}
fn assert_svg_is_well_formed(svg: &str) {
let document = roxmltree::Document::parse(svg).expect("svg should be well-formed xml");
let root = document.root_element();
assert_eq!(root.tag_name().name(), "svg");
assert_eq!(
root.tag_name().namespace(),
Some("http://www.w3.org/2000/svg")
);
assert!(root.attribute("viewBox").is_some());
}
fn identity_with_digest_byte(index: usize, value: u8) -> AvatarIdentity {
let mut digest = [0_u8; 64];
digest[index] = value;
AvatarIdentity { digest }
}
fn render_cat_avatar(spec: AvatarSpec) -> RgbaImage {
super::render_cat_avatar(spec).expect("valid avatar spec should render")
}
fn render_cat_avatar_for_identity(spec: AvatarSpec, identity: &AvatarIdentity) -> RgbaImage {
super::render_cat_avatar_for_identity(spec, identity)
.expect("valid avatar spec should render")
}
fn render_cat_avatar_for_identity_with_background(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> RgbaImage {
super::render_cat_avatar_for_identity_with_background(spec, identity, background)
.expect("valid avatar spec should render")
}
fn render_dog_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> RgbaImage {
super::render_dog_avatar_for_identity(spec, identity, background)
.expect("valid avatar spec should render")
}
fn render_robot_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> RgbaImage {
super::render_robot_avatar_for_identity(spec, identity, background)
.expect("valid avatar spec should render")
}
fn render_alien_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> RgbaImage {
super::render_alien_avatar_for_identity(spec, identity, background)
.expect("valid avatar spec should render")
}
fn render_monster_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> RgbaImage {
super::render_monster_avatar_for_identity(spec, identity, background)
.expect("valid avatar spec should render")
}
fn render_paws_avatar_for_identity(
spec: AvatarSpec,
identity: &AvatarIdentity,
background: AvatarBackground,
) -> RgbaImage {
super::render_paws_avatar_for_identity(spec, identity, background)
.expect("valid avatar spec should render")
}
#[test]
fn cat_avatar_is_deterministic_for_a_seed() {
let spec = valid_spec(256, 256, 42);
let left = render_cat_avatar(spec);
let right = render_cat_avatar(spec);
assert_eq!(left.as_raw(), right.as_raw());
}
#[test]
fn cat_avatar_uses_requested_dimensions() {
let image = render_cat_avatar(valid_spec(192, 160, 7));
assert_eq!(image.width(), 192);
assert_eq!(image.height(), 160);
}
#[test]
fn cat_avatar_has_non_background_pixels() {
let spec = valid_spec(128, 128, 3);
let image = render_cat_avatar(spec);
let background = image.get_pixel(0, 0);
assert!(image.pixels().any(|pixel| pixel != background));
}
#[test]
fn avatar_identity_uses_sha512_digest() {
let identity = valid_identity("alice@example.com");
assert_eq!(identity.digest.len(), 64);
let rng_seed = identity.rng_seed();
assert_eq!(&rng_seed[..], &identity.digest[32..64]);
}
#[test]
fn avatar_identity_debug_redacts_digest() {
let identity = valid_identity("alice@example.com");
let debug = format!("{identity:?}");
assert_eq!(debug, r#"AvatarIdentity { digest: "[REDACTED]" }"#);
for byte in identity.digest {
assert!(
!debug.contains(&format!("{byte}")),
"debug output leaked digest byte {byte}"
);
}
}
#[test]
fn avatar_identity_rustdoc_mentions_clone_zeroization() {
let source = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/lib.rs"));
let before_struct = source
.split("pub struct AvatarIdentity {")
.next()
.expect("AvatarIdentity struct should exist");
let docs = before_struct
.rsplit("/// A stable avatar identity")
.next()
.expect("AvatarIdentity rustdoc should exist");
assert!(docs.contains("/// # Security"));
assert!(docs.contains("`AvatarIdentity` implements `Clone`"));
assert!(docs.contains("Each clone is independently zeroized"));
assert!(docs.contains("short-lived as possible"));
assert!(docs.contains("multiple memory locations"));
}
#[test]
#[cfg(not(any(feature = "blake3", feature = "xxh3")))]
fn sha512_hasher_state_uses_zeroize_on_drop() {
fn assert_zeroize_on_drop<T: zeroize::ZeroizeOnDrop>() {}
assert_zeroize_on_drop::<TestSha512>();
}
#[test]
#[cfg(feature = "blake3")]
fn blake3_hasher_state_can_be_zeroized() {
fn assert_zeroize<T: Zeroize>() {}
assert_zeroize::<blake3::Hasher>();
assert_zeroize::<blake3::OutputReader>();
}
#[test]
fn rng_seed_uses_second_half_of_identity_digest() {
let identity = valid_identity("alice@example.com");
let rng_seed = identity.rng_seed();
assert_eq!(rng_seed.len(), 32);
assert_eq!(&rng_seed[..], &identity.digest[32..64]);
assert_ne!(&identity.digest[..32], &rng_seed[..]);
}
#[test]
fn rng_seed_copy_is_zeroizing() {
let identity = valid_identity("alice@example.com");
let rng_seed: Zeroizing<[u8; 32]> = identity.rng_seed();
assert_eq!(rng_seed.len(), 32);
}
#[test]
fn renderer_rng_seed_copy_is_zeroized_before_rng_use() {
let source = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/lib.rs"));
let helper = source
.split("fn seeded_renderer_rng")
.nth(1)
.and_then(|after_name| {
after_name
.split("fn render_cat_avatar_with_identity")
.next()
})
.expect("seeded renderer rng helper should exist");
assert!(helper.contains("let mut rng_seed_value = *rng_seed;"));
assert!(helper.contains("drop(rng_seed);"));
assert!(helper.contains("let rng = StdRng::from_seed(rng_seed_value);"));
assert!(helper.contains("rng_seed_value.zeroize();"));
}
#[test]
fn avatar_identity_equality_compares_digest_values() {
let left = valid_identity("alice@example.com");
let same = valid_identity("alice@example.com");
let different = valid_identity("bob@example.com");
assert_eq!(left, same);
assert_ne!(left, different);
}
#[test]
fn default_identity_options_match_namespace_constructor() {
let namespace = valid_namespace("tenant-a", "v2");
let default = AvatarIdentity::new_with_namespace(namespace, "alice@example.com")
.expect("identity should be valid");
let explicit = AvatarIdentity::new_with_options(
AvatarIdentityOptions::new(namespace),
"alice@example.com",
)
.expect("explicit identity options should be valid");
assert_eq!(default.digest, explicit.digest);
}
#[test]
fn active_hash_algorithm_label_matches_enabled_feature() {
#[cfg(feature = "blake3")]
assert_eq!(ACTIVE_HASH_ALGORITHM_LABEL, b"blake3");
#[cfg(feature = "xxh3")]
assert_eq!(ACTIVE_HASH_ALGORITHM_LABEL, b"xxh3-128");
#[cfg(not(any(feature = "blake3", feature = "xxh3")))]
assert_eq!(ACTIVE_HASH_ALGORITHM_LABEL, b"sha512");
}
#[test]
#[cfg(not(any(feature = "blake3", feature = "xxh3")))]
fn default_sha512_preimage_omits_algorithm_domain_for_legacy_stability() {
let preimage =
identity_hash_preimage(AvatarIdentityOptions::default(), b"alice@example.com");
assert!(
!preimage
.windows(HASH_DOMAIN_ALGORITHM_COMPONENT.len())
.any(|window| window == HASH_DOMAIN_ALGORITHM_COMPONENT)
);
assert!(
!preimage
.windows(ACTIVE_HASH_ALGORITHM_LABEL.len())
.any(|window| window == ACTIVE_HASH_ALGORITHM_LABEL)
);
}
#[test]
#[cfg(any(feature = "blake3", feature = "xxh3"))]
fn optional_hash_modes_add_algorithm_domain_to_preimage() {
let preimage =
identity_hash_preimage(AvatarIdentityOptions::default(), b"alice@example.com");
assert!(
preimage
.windows(HASH_DOMAIN_ALGORITHM_COMPONENT.len())
.any(|window| window == HASH_DOMAIN_ALGORITHM_COMPONENT)
);
assert!(
preimage
.windows(ACTIVE_HASH_ALGORITHM_LABEL.len())
.any(|window| window == ACTIVE_HASH_ALGORITHM_LABEL)
);
}
#[test]
fn oversized_identity_is_rejected_for_active_hash_mode() {
let too_long = vec![b'a'; MAX_AVATAR_ID_BYTES + 1];
let error = AvatarIdentity::new_with_options(AvatarIdentityOptions::default(), &too_long)
.expect_err("oversized identity should fail");
assert_eq!(error.component(), AvatarIdentityComponent::Input);
assert_eq!(error.length(), MAX_AVATAR_ID_BYTES + 1);
assert_eq!(error.max(), MAX_AVATAR_ID_BYTES);
}
#[cfg(feature = "blake3")]
#[test]
fn blake3_identity_mode_renders_avatar() {
let image = render_avatar_with_identity_options(
valid_spec(96, 96, 0),
AvatarIdentityOptions::new(AvatarNamespace::default()),
"alice@example.com",
AvatarOptions::new(AvatarKind::Robot, AvatarBackground::Themed),
)
.expect("blake3-backed avatar should render");
assert_eq!(image.width(), 96);
assert_eq!(image.height(), 96);
}
#[cfg(feature = "xxh3")]
#[test]
fn xxh3_identity_mode_renders_avatar() {
let image = render_avatar_with_identity_options(
valid_spec(96, 96, 0),
AvatarIdentityOptions::new(AvatarNamespace::default()),
"alice@example.com",
AvatarOptions::new(AvatarKind::Robot, AvatarBackground::Themed),
)
.expect("xxh3-backed avatar should render");
assert_eq!(image.width(), 96);
assert_eq!(image.height(), 96);
}
#[test]
fn namespace_changes_identity_digest() {
let left =
valid_identity_with_namespace(valid_namespace("tenant-a", "v2"), "alice@example.com");
let right =
valid_identity_with_namespace(valid_namespace("tenant-b", "v2"), "alice@example.com");
assert_ne!(left.digest, right.digest);
}
#[test]
fn namespace_hashing_is_not_ambiguous_with_nul_bytes() {
let left =
valid_identity_with_namespace(valid_namespace("tenant\0v2", "v1"), "alice@example.com");
let right =
valid_identity_with_namespace(valid_namespace("tenant", "v2\0v1"), "alice@example.com");
assert_ne!(left.digest, right.digest);
}
#[test]
fn identity_construction_rejects_oversized_input() {
let too_long = vec![b'a'; MAX_AVATAR_ID_BYTES + 1];
let error = AvatarIdentity::new(&too_long).expect_err("oversized identity should fail");
assert_eq!(error.component(), AvatarIdentityComponent::Input);
assert_eq!(error.length(), MAX_AVATAR_ID_BYTES + 1);
assert_eq!(error.max(), MAX_AVATAR_ID_BYTES);
}
#[test]
fn namespace_construction_rejects_oversized_components() {
let too_long = "a".repeat(MAX_AVATAR_NAMESPACE_COMPONENT_BYTES + 1);
let error =
AvatarNamespace::new(&too_long, "v2").expect_err("oversized tenant should fail");
assert_eq!(error.component(), AvatarIdentityComponent::Tenant);
assert_eq!(error.length(), MAX_AVATAR_NAMESPACE_COMPONENT_BYTES + 1);
assert_eq!(error.max(), MAX_AVATAR_NAMESPACE_COMPONENT_BYTES);
}
#[test]
fn render_avatar_for_id_rejects_oversized_identity() {
let too_long = vec![b'a'; MAX_AVATAR_ID_BYTES + 1];
let error = super::render_avatar_for_id(
valid_spec(128, 128, 0),
&too_long,
AvatarOptions::new(AvatarKind::Cat, AvatarBackground::Themed),
)
.expect_err("oversized identity should fail");
assert!(matches!(
error,
AvatarRenderError::Identity(AvatarIdentityError {
component: AvatarIdentityComponent::Input,
..
})
));
}
#[test]
fn hashed_cat_avatar_is_deterministic_for_same_id() {
let spec = valid_spec(192, 192, 0);
let left = render_cat_avatar_for_identity(spec, &valid_identity("alice@example.com"));
let right = render_cat_avatar_for_identity(spec, &valid_identity("alice@example.com"));
assert_eq!(left.as_raw(), right.as_raw());
}
#[test]
fn hashed_cat_avatar_changes_for_different_ids() {
let spec = valid_spec(192, 192, 0);
let left = render_cat_avatar_for_identity(spec, &valid_identity("alice@example.com"));
let right = render_cat_avatar_for_identity(spec, &valid_identity("bob@example.com"));
assert_ne!(left.as_raw(), right.as_raw());
}
#[test]
fn cat_avatar_webp_export_round_trips() {
let bytes = encode_cat_avatar(valid_spec(128, 128, 11), AvatarOutputFormat::WebP)
.expect("webp encoding should succeed");
let decoded = image::load_from_memory_with_format(&bytes, ImageFormat::WebP)
.expect("webp should decode");
assert_eq!(decoded.width(), 128);
assert_eq!(decoded.height(), 128);
}
#[test]
#[cfg(feature = "png")]
fn cat_avatar_png_export_round_trips() {
let bytes = encode_cat_avatar(valid_spec(96, 96, 99), AvatarOutputFormat::Png)
.expect("png encoding should succeed");
let decoded = image::load_from_memory_with_format(&bytes, ImageFormat::Png)
.expect("png should decode");
assert_eq!(decoded.width(), 96);
assert_eq!(decoded.height(), 96);
}
#[test]
#[cfg(feature = "jpeg")]
fn cat_avatar_jpeg_export_round_trips() {
let bytes = encode_cat_avatar(valid_spec(96, 96, 99), AvatarOutputFormat::Jpeg)
.expect("jpeg encoding should succeed");
let decoded = image::load_from_memory_with_format(&bytes, ImageFormat::Jpeg)
.expect("jpeg should decode");
assert_eq!(decoded.width(), 96);
assert_eq!(decoded.height(), 96);
}
#[test]
#[cfg(not(feature = "png"))]
fn png_output_format_is_unavailable_without_feature() {
assert_eq!(
"png".parse::<AvatarOutputFormat>(),
Err("unsupported avatar output format")
);
assert!(
!AvatarOutputFormat::ALL
.iter()
.any(|format| format.as_str() == "png")
);
}
#[test]
#[cfg(not(feature = "jpeg"))]
fn jpeg_output_format_is_unavailable_without_feature() {
assert_eq!(
"jpg".parse::<AvatarOutputFormat>(),
Err("unsupported avatar output format")
);
assert_eq!(
"jpeg".parse::<AvatarOutputFormat>(),
Err("unsupported avatar output format")
);
assert!(
!AvatarOutputFormat::ALL
.iter()
.any(|format| format.as_str() == "jpg")
);
}
#[test]
#[cfg(feature = "gif")]
fn cat_avatar_gif_export_round_trips() {
let bytes = encode_cat_avatar(valid_spec(96, 96, 99), AvatarOutputFormat::Gif)
.expect("gif encoding should succeed");
let decoded = image::load_from_memory_with_format(&bytes, ImageFormat::Gif)
.expect("gif should decode");
assert_eq!(decoded.width(), 96);
assert_eq!(decoded.height(), 96);
}
#[test]
#[cfg(not(feature = "gif"))]
fn gif_output_format_is_unavailable_without_feature() {
assert_eq!(
"gif".parse::<AvatarOutputFormat>(),
Err("unsupported avatar output format")
);
assert!(
!AvatarOutputFormat::ALL
.iter()
.any(|format| format.as_str() == "gif")
);
}
#[test]
#[cfg(feature = "jpeg")]
fn jpeg_export_flattens_transparency_over_white() {
let bytes = encode_avatar_for_id(
valid_spec(96, 96, 0),
"cat@hashavatar.app",
AvatarOutputFormat::Jpeg,
AvatarOptions::new(AvatarKind::Cat, AvatarBackground::Transparent),
)
.expect("jpeg encoding should succeed");
let decoded = image::load_from_memory_with_format(&bytes, ImageFormat::Jpeg)
.expect("jpeg should decode")
.to_rgb8();
let corner = decoded.get_pixel(0, 0);
assert!(corner.0.iter().all(|channel| *channel > 245));
}
#[test]
fn webp_is_the_default_output_format() {
assert_eq!(AvatarOutputFormat::default(), AvatarOutputFormat::WebP);
}
#[test]
fn hashed_cat_avatar_webp_export_round_trips() {
let bytes = encode_cat_avatar_for_id(
valid_spec(128, 128, 0),
"alice@example.com",
AvatarOutputFormat::WebP,
)
.expect("webp encoding should succeed");
let decoded = image::load_from_memory_with_format(&bytes, ImageFormat::WebP)
.expect("webp should decode");
assert_eq!(decoded.width(), 128);
assert_eq!(decoded.height(), 128);
}
#[test]
fn white_background_mode_renders_white_corner() {
let image = render_cat_avatar_for_identity_with_background(
valid_spec(128, 128, 0),
&valid_identity("alice@example.com"),
AvatarBackground::White,
);
assert_eq!(image.get_pixel(0, 0), &Rgba([255, 255, 255, 255]));
}
#[test]
fn fixed_background_modes_render_expected_corners() {
for (background, expected) in [
(AvatarBackground::Black, Rgba([0, 0, 0, 255])),
(AvatarBackground::Dark, Rgba([17, 24, 39, 255])),
(AvatarBackground::Light, Rgba([248, 250, 247, 255])),
] {
let image = render_cat_avatar_for_identity_with_background(
valid_spec(128, 128, 0),
&valid_identity("cat@hashavatar.app"),
background,
);
assert_eq!(image.get_pixel(0, 0), &expected, "{background}");
}
}
#[test]
fn transparent_background_mode_renders_clear_corner() {
let image = render_cat_avatar_for_identity_with_background(
valid_spec(128, 128, 0),
&valid_identity("cat@hashavatar.app"),
AvatarBackground::Transparent,
);
assert_eq!(image.get_pixel(0, 0), &Rgba([255, 255, 255, 0]));
}
#[test]
fn decorative_background_modes_render_distinct_raster_canvases() {
let spec = valid_spec(128, 128, 0);
let identity = valid_identity("backgrounds@hashavatar.app");
let mut fingerprints = Vec::new();
for background in [
AvatarBackground::PolkaDot,
AvatarBackground::Striped,
AvatarBackground::Checkerboard,
AvatarBackground::Grid,
AvatarBackground::Sunrise,
AvatarBackground::Ocean,
AvatarBackground::Starry,
] {
let image = render_cat_avatar_for_identity_with_background(spec, &identity, background);
assert_eq!(image.width(), 128);
assert_eq!(image.height(), 128);
assert!(
image.pixels().any(|pixel| pixel.0[3] == 255),
"{background}"
);
fingerprints.push(image_fingerprint(&image));
}
fingerprints.sort();
fingerprints.dedup();
assert_eq!(fingerprints.len(), 7);
}
#[test]
fn decorative_svg_backgrounds_use_structured_defs() {
let spec = valid_spec(128, 128, 0);
for background in [
AvatarBackground::PolkaDot,
AvatarBackground::Striped,
AvatarBackground::Checkerboard,
AvatarBackground::Grid,
AvatarBackground::Sunrise,
AvatarBackground::Ocean,
AvatarBackground::Starry,
] {
let svg = render_avatar_svg_for_id(
spec,
"backgrounds@hashavatar.app",
AvatarOptions::new(AvatarKind::Robot, background),
);
assert_svg_is_well_formed(&svg);
assert!(svg.contains("hashavatar-bg-"), "{background}");
assert!(!svg.contains("<script"), "{background}");
}
}
#[test]
fn dog_and_robot_variants_generate_distinct_images() {
let spec = valid_spec(128, 128, 0);
let id = valid_identity("alice@example.com");
let dog = render_dog_avatar_for_identity(spec, &id, AvatarBackground::Themed);
let robot = render_robot_avatar_for_identity(spec, &id, AvatarBackground::Themed);
assert_ne!(dog.as_raw(), robot.as_raw());
}
#[test]
fn monster_variant_is_distinct_from_alien() {
let spec = valid_spec(128, 128, 0);
let id = valid_identity("alice@example.com");
let alien = render_alien_avatar_for_identity(spec, &id, AvatarBackground::Themed);
let monster = render_monster_avatar_for_identity(spec, &id, AvatarBackground::Themed);
assert_ne!(alien.as_raw(), monster.as_raw());
}
#[test]
fn paws_variant_is_distinct_from_cat() {
let spec = valid_spec(128, 128, 0);
let id = valid_identity("alice@example.com");
let cat =
render_cat_avatar_for_identity_with_background(spec, &id, AvatarBackground::Themed);
let paws = render_paws_avatar_for_identity(spec, &id, AvatarBackground::Themed);
assert_ne!(cat.as_raw(), paws.as_raw());
}
#[test]
fn generic_avatar_encoder_supports_robot_and_white_background() {
let bytes = encode_avatar_for_id(
valid_spec(96, 96, 0),
"robot@example.com",
AvatarOutputFormat::WebP,
AvatarOptions {
kind: AvatarKind::Robot,
background: AvatarBackground::White,
},
)
.expect("robot webp encoding should succeed");
let decoded = image::load_from_memory_with_format(&bytes, ImageFormat::WebP)
.expect("robot webp should decode");
assert_eq!(decoded.width(), 96);
assert_eq!(decoded.height(), 96);
}
#[test]
fn svg_export_contains_svg_root_and_kind_label() {
let svg = render_avatar_svg_for_id(
valid_spec(128, 128, 0),
"vector@example.com",
AvatarOptions::new(AvatarKind::Fox, AvatarBackground::White),
);
assert!(svg.starts_with("<svg "));
assert!(svg.contains("fox avatar"));
}
#[test]
fn svg_output_is_minimal_and_safe() {
let svg = render_avatar_svg_for_id(
valid_spec(256, 256, 0),
"ghost@example.com",
AvatarOptions::new(AvatarKind::Ghost, AvatarBackground::Themed),
);
assert!(!svg.contains("<script"));
assert!(!svg.contains("onload="));
assert!(svg.len() < 8_000);
}
#[test]
fn svg_output_is_well_formed_xml_for_all_avatar_kinds() {
let spec = valid_spec(128, 128, 0);
let identities: [&[u8]; 4] = [
b"alice@example.com",
b"\0\0\0\0",
b"<not-svg attr=\"x\">&",
b"0123456789abcdef0123456789abcdef0123456789abcdef",
];
for &kind in AvatarKind::ALL {
for &background in AvatarBackground::ALL {
for identity in identities {
let svg = render_avatar_svg_for_id(
spec,
identity,
AvatarOptions::new(kind, background),
);
assert_svg_is_well_formed(&svg);
}
}
}
}
#[test]
fn styled_svg_output_is_well_formed_xml_for_all_layer_options() {
let spec = valid_spec(96, 96, 0);
let kinds = [AvatarKind::Robot, AvatarKind::Shield];
for &kind in &kinds {
for &accessory in AvatarAccessory::ALL {
let style = AvatarStyleOptions::new(
kind,
AvatarBackground::Themed,
accessory,
AvatarColor::Default,
AvatarExpression::Default,
AvatarShape::Square,
);
assert_svg_is_well_formed(&render_avatar_svg_style_for_id(
spec,
"accessory-xml@example.com",
style,
));
}
for &color in AvatarColor::ALL {
let style = AvatarStyleOptions::new(
kind,
AvatarBackground::Themed,
AvatarAccessory::None,
color,
AvatarExpression::Default,
AvatarShape::Square,
);
assert_svg_is_well_formed(&render_avatar_svg_style_for_id(
spec,
"color-xml@example.com",
style,
));
}
for &expression in AvatarExpression::ALL {
let style = AvatarStyleOptions::new(
kind,
AvatarBackground::Themed,
AvatarAccessory::None,
AvatarColor::Default,
expression,
AvatarShape::Square,
);
assert_svg_is_well_formed(&render_avatar_svg_style_for_id(
spec,
"expression-xml@example.com",
style,
));
}
for &shape in AvatarShape::ALL {
let style = AvatarStyleOptions::new(
kind,
AvatarBackground::Themed,
AvatarAccessory::None,
AvatarColor::Default,
AvatarExpression::Default,
shape,
);
assert_svg_is_well_formed(&render_avatar_svg_style_for_id(
spec,
"shape-xml@example.com",
style,
));
}
}
}
#[test]
fn transparent_svg_output_has_no_background_rect() {
let svg = render_avatar_svg_for_id(
valid_spec(128, 128, 0),
"cat@hashavatar.app",
AvatarOptions::new(AvatarKind::Cat, AvatarBackground::Transparent),
);
assert!(!svg.contains(r#"<rect width="100%" height="100%""#));
assert!(svg.contains("cat avatar"));
}
#[test]
fn svg_radius_attributes_do_not_contain_color_values() {
let spec = valid_spec(128, 128, 0);
for &kind in AvatarKind::ALL {
let svg = render_avatar_svg_for_id(
spec,
"svg-radius@example.com",
AvatarOptions::new(kind, AvatarBackground::Themed),
);
assert!(!svg.contains(r##"rx="#"##), "{kind}");
assert!(!svg.contains(r##"ry="#"##), "{kind}");
}
}
#[test]
fn dark_svg_output_has_background_rect() {
let svg = render_avatar_svg_for_id(
valid_spec(128, 128, 0),
"cat@hashavatar.app",
AvatarOptions::new(AvatarKind::Cat, AvatarBackground::Dark),
);
assert!(svg.contains(r##"<rect width="100%" height="100%" fill="#111827"/>"##));
}
#[test]
fn parser_round_trip_supports_public_enums() {
for &kind in AvatarKind::ALL {
assert_eq!(kind.as_str().parse::<AvatarKind>().ok(), Some(kind));
assert_eq!(kind.to_string(), kind.as_str());
}
for &background in AvatarBackground::ALL {
assert_eq!(
background.as_str().parse::<AvatarBackground>().ok(),
Some(background)
);
assert_eq!(background.to_string(), background.as_str());
}
for &format in AvatarOutputFormat::ALL {
assert_eq!(
format.as_str().parse::<AvatarOutputFormat>().ok(),
Some(format)
);
assert_eq!(format.to_string(), format.as_str());
}
for &accessory in AvatarAccessory::ALL {
assert_eq!(
accessory.as_str().parse::<AvatarAccessory>().ok(),
Some(accessory)
);
assert_eq!(accessory.to_string(), accessory.as_str());
}
for &color in AvatarColor::ALL {
assert_eq!(color.as_str().parse::<AvatarColor>().ok(), Some(color));
assert_eq!(color.to_string(), color.as_str());
}
for &expression in AvatarExpression::ALL {
assert_eq!(
expression.as_str().parse::<AvatarExpression>().ok(),
Some(expression)
);
assert_eq!(expression.to_string(), expression.as_str());
}
for &shape in AvatarShape::ALL {
assert_eq!(shape.as_str().parse::<AvatarShape>().ok(), Some(shape));
assert_eq!(shape.to_string(), shape.as_str());
}
}
#[test]
fn public_enum_variant_lists_match_documented_labels() {
let kind_labels: Vec<_> = AvatarKind::ALL.iter().map(|kind| kind.as_str()).collect();
assert_eq!(
kind_labels,
[
"cat",
"dog",
"robot",
"fox",
"alien",
"monster",
"ghost",
"slime",
"bird",
"wizard",
"skull",
"paws",
"planet",
"rocket",
"mushroom",
"cactus",
"frog",
"panda",
"cupcake",
"pizza",
"icecream",
"octopus",
"knight",
"bear",
"penguin",
"dragon",
"ninja",
"astronaut",
"diamond",
"coffee-cup",
"shield",
]
);
let background_labels: Vec<_> = AvatarBackground::ALL
.iter()
.map(|background| background.as_str())
.collect();
assert_eq!(
background_labels,
[
"themed",
"white",
"black",
"dark",
"light",
"transparent",
"polka-dot",
"striped",
"checkerboard",
"grid",
"sunrise",
"ocean",
"starry",
]
);
let format_labels: Vec<_> = AvatarOutputFormat::ALL
.iter()
.map(|format| format.as_str())
.collect();
assert_eq!(
format_labels,
[
"webp",
#[cfg(feature = "png")]
"png",
#[cfg(feature = "jpeg")]
"jpg",
#[cfg(feature = "gif")]
"gif",
]
);
let accessory_labels: Vec<_> = AvatarAccessory::ALL
.iter()
.map(|accessory| accessory.as_str())
.collect();
assert_eq!(
accessory_labels,
[
"none",
"glasses",
"hat",
"headphones",
"crown",
"bowtie",
"eyepatch",
"scarf",
"halo",
"horns",
]
);
let color_labels: Vec<_> = AvatarColor::ALL
.iter()
.map(|color| color.as_str())
.collect();
assert_eq!(
color_labels,
[
"default",
"neon-mint",
"pastel-pink",
"crimson",
"gold",
"deep-sea-blue",
]
);
let expression_labels: Vec<_> = AvatarExpression::ALL
.iter()
.map(|expression| expression.as_str())
.collect();
assert_eq!(
expression_labels,
[
"default",
"happy",
"grumpy",
"surprised",
"sleepy",
"winking",
"cool",
"crying",
]
);
let shape_labels: Vec<_> = AvatarShape::ALL
.iter()
.map(|shape| shape.as_str())
.collect();
assert_eq!(
shape_labels,
["square", "circle", "squircle", "hexagon", "octagon"]
);
}
#[test]
fn byte_to_public_enum_helpers_use_variant_lists() {
for (index, &kind) in AvatarKind::ALL.iter().enumerate() {
assert_eq!(AvatarKind::from_byte(index as u8), kind);
}
assert_eq!(
AvatarKind::from_byte(AvatarKind::ALL.len() as u8),
AvatarKind::ALL[0]
);
for (index, &background) in AvatarBackground::ALL.iter().enumerate() {
assert_eq!(AvatarBackground::from_byte(index as u8), background);
}
assert_eq!(
AvatarBackground::from_byte(AvatarBackground::ALL.len() as u8),
AvatarBackground::ALL[0]
);
for (index, &format) in AvatarOutputFormat::ALL.iter().enumerate() {
assert_eq!(AvatarOutputFormat::from_byte(index as u8), format);
}
assert_eq!(
AvatarOutputFormat::from_byte(AvatarOutputFormat::ALL.len() as u8),
AvatarOutputFormat::ALL[0]
);
for (index, &accessory) in AvatarAccessory::ALL.iter().enumerate() {
assert_eq!(AvatarAccessory::from_byte(index as u8), accessory);
}
assert_eq!(
AvatarAccessory::from_byte(AvatarAccessory::ALL.len() as u8),
AvatarAccessory::ALL[0]
);
for (index, &color) in AvatarColor::ALL.iter().enumerate() {
assert_eq!(AvatarColor::from_byte(index as u8), color);
}
assert_eq!(
AvatarColor::from_byte(AvatarColor::ALL.len() as u8),
AvatarColor::ALL[0]
);
for (index, &expression) in AvatarExpression::ALL.iter().enumerate() {
assert_eq!(AvatarExpression::from_byte(index as u8), expression);
}
assert_eq!(
AvatarExpression::from_byte(AvatarExpression::ALL.len() as u8),
AvatarExpression::ALL[0]
);
for (index, &shape) in AvatarShape::ALL.iter().enumerate() {
assert_eq!(AvatarShape::from_byte(index as u8), shape);
}
assert_eq!(
AvatarShape::from_byte(AvatarShape::ALL.len() as u8),
AvatarShape::ALL[0]
);
}
#[test]
#[cfg(feature = "gif")]
fn gif_variant_has_rustdoc_security_warning() {
let source = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/lib.rs"));
let variant_docs = source
.split("Gif,")
.next()
.and_then(|before_variant| before_variant.rsplit_once("/// Optional GIF output."))
.map(|(_, docs)| docs)
.expect("gif variant docs should be present");
assert!(variant_docs.contains("# Warning"));
assert!(variant_docs.contains("256-color quantization"));
assert!(variant_docs.contains("not zeroized"));
assert!(variant_docs.contains("high-assurance deployments"));
assert!(variant_docs.contains("AvatarOutputFormat::WebP"));
assert!(variant_docs.contains("PNG output"));
}
#[test]
fn internal_render_plan_matches_direct_raster_renderer() {
let spec = valid_spec(96, 96, 0);
let options = AvatarOptions::new(AvatarKind::Robot, AvatarBackground::Dark);
let plan = AvatarRenderPlan::new(
spec,
AvatarIdentityOptions::default(),
"plan@example.com",
options,
)
.expect("render plan should be valid");
let identity = valid_identity("plan@example.com");
let direct = render_robot_avatar_for_identity(spec, &identity, AvatarBackground::Dark);
assert_eq!(
plan.render_rgba()
.expect("planned robot render should be valid")
.as_raw(),
direct.as_raw()
);
assert!(plan.render_svg().contains("robot avatar"));
}
#[test]
fn avatar_style_options_from_legacy_options_is_noop() {
let spec = valid_spec(128, 128, 0);
let options = AvatarOptions::new(AvatarKind::Robot, AvatarBackground::Themed);
let style = AvatarStyleOptions::from_options(options);
let legacy = render_avatar_for_id(spec, "style@example.com", options);
let styled = render_avatar_style_for_id(spec, "style@example.com", style);
assert_eq!(legacy.as_raw(), styled.as_raw());
let legacy_svg = render_avatar_svg_for_id(spec, "style@example.com", options);
let styled_svg = render_avatar_svg_style_for_id(spec, "style@example.com", style);
assert_eq!(legacy_svg, styled_svg);
}
#[test]
fn automatic_style_derivation_uses_distinct_digest_offsets() {
let base = AvatarStyleOptions::from_identity(&identity_with_digest_byte(63, 99));
let kind = AvatarStyleOptions::from_identity(&identity_with_digest_byte(
AVATAR_STYLE_KIND_BYTE,
1,
));
assert_ne!(kind.kind, base.kind);
assert_eq!(kind.background, base.background);
assert_eq!(kind.accessory, base.accessory);
assert_eq!(kind.color, base.color);
assert_eq!(kind.expression, base.expression);
assert_eq!(kind.shape, base.shape);
let background = AvatarStyleOptions::from_identity(&identity_with_digest_byte(
AVATAR_STYLE_BACKGROUND_BYTE,
1,
));
assert_eq!(background.kind, base.kind);
assert_ne!(background.background, base.background);
assert_eq!(background.accessory, base.accessory);
assert_eq!(background.color, base.color);
assert_eq!(background.expression, base.expression);
assert_eq!(background.shape, base.shape);
let accessory = AvatarStyleOptions::from_identity(&identity_with_digest_byte(
AVATAR_STYLE_ACCESSORY_BYTE,
1,
));
assert_eq!(accessory.kind, base.kind);
assert_eq!(accessory.background, base.background);
assert_ne!(accessory.accessory, base.accessory);
assert_eq!(accessory.color, base.color);
assert_eq!(accessory.expression, base.expression);
assert_eq!(accessory.shape, base.shape);
let color = AvatarStyleOptions::from_identity(&identity_with_digest_byte(
AVATAR_STYLE_COLOR_BYTE,
1,
));
assert_eq!(color.kind, base.kind);
assert_eq!(color.background, base.background);
assert_eq!(color.accessory, base.accessory);
assert_ne!(color.color, base.color);
assert_eq!(color.expression, base.expression);
assert_eq!(color.shape, base.shape);
let expression = AvatarStyleOptions::from_identity(&identity_with_digest_byte(
AVATAR_STYLE_EXPRESSION_BYTE,
1,
));
assert_eq!(expression.kind, base.kind);
assert_eq!(expression.background, base.background);
assert_eq!(expression.accessory, base.accessory);
assert_eq!(expression.color, base.color);
assert_ne!(expression.expression, base.expression);
assert_eq!(expression.shape, base.shape);
let shape = AvatarStyleOptions::from_identity(&identity_with_digest_byte(
AVATAR_STYLE_SHAPE_BYTE,
1,
));
assert_eq!(shape.kind, base.kind);
assert_eq!(shape.background, base.background);
assert_eq!(shape.accessory, base.accessory);
assert_eq!(shape.color, base.color);
assert_eq!(shape.expression, base.expression);
assert_ne!(shape.shape, base.shape);
}
#[test]
fn automatic_style_derivation_is_deterministic() {
let identity = valid_identity("auto-style@example.com");
assert_eq!(
AvatarStyleOptions::from_identity(&identity),
AvatarStyleOptions::from_identity(&identity)
);
assert_eq!(
super::render_avatar_auto_for_id(valid_spec(96, 96, 0), "auto-style@example.com")
.expect("automatic style should render")
.as_raw(),
super::render_avatar_auto_for_id(valid_spec(96, 96, 0), "auto-style@example.com")
.expect("automatic style should render")
.as_raw()
);
}
#[test]
fn manual_style_selection_changes_raster_and_svg() {
let spec = valid_spec(128, 128, 0);
let legacy_options = AvatarOptions::new(AvatarKind::Cat, AvatarBackground::Themed);
let layered_style = AvatarStyleOptions::new(
AvatarKind::Cat,
AvatarBackground::Themed,
AvatarAccessory::Glasses,
AvatarColor::Gold,
AvatarExpression::Happy,
AvatarShape::Circle,
);
let legacy = render_avatar_for_id(spec, "manual-style@example.com", legacy_options);
let layered = render_avatar_style_for_id(spec, "manual-style@example.com", layered_style);
assert_ne!(legacy.as_raw(), layered.as_raw());
let svg = render_avatar_svg_style_for_id(spec, "manual-style@example.com", layered_style);
assert!(svg.contains(r#"data-layer="accessory-glasses""#));
assert!(svg.contains(r#"data-layer="expression-happy""#));
assert!(svg.contains(r#"data-layer="shape-circle""#));
}
#[test]
fn avatar_kind_face_layer_support_matches_anchor_coverage() {
let supported = [
AvatarKind::Cat,
AvatarKind::Dog,
AvatarKind::Robot,
AvatarKind::Fox,
AvatarKind::Alien,
AvatarKind::Monster,
AvatarKind::Ghost,
AvatarKind::Slime,
AvatarKind::Bird,
AvatarKind::Wizard,
AvatarKind::Skull,
AvatarKind::Frog,
AvatarKind::Panda,
AvatarKind::Octopus,
AvatarKind::Knight,
AvatarKind::Bear,
AvatarKind::Penguin,
AvatarKind::Dragon,
AvatarKind::Ninja,
AvatarKind::Astronaut,
];
for &kind in AvatarKind::ALL {
assert_eq!(
kind.supports_face_layers(),
supported.contains(&kind),
"{kind}"
);
assert_eq!(
avatar_layer_anchors(kind).is_some(),
kind.supports_face_layers(),
"{kind}"
);
}
}
#[test]
fn face_layer_families_emit_accessory_and_expression_svg_layers() {
let spec = valid_spec(96, 96, 0);
for &kind in AvatarKind::ALL {
if !kind.supports_face_layers() {
continue;
}
let style = AvatarStyleOptions::new(
kind,
AvatarBackground::Themed,
AvatarAccessory::Eyepatch,
AvatarColor::Gold,
AvatarExpression::Winking,
AvatarShape::Square,
);
let image = render_avatar_style_for_id(spec, "face-layer@example.com", style);
assert_eq!(image.dimensions(), (96, 96), "{kind}");
let svg = render_avatar_svg_style_for_id(spec, "face-layer@example.com", style);
assert!(svg.contains(r#"data-layer="accessory-eyepatch""#), "{kind}");
assert!(svg.contains(r#"data-layer="expression-winking""#), "{kind}");
}
}
#[test]
fn style_layers_render_for_all_baseline_variants() {
let spec = valid_spec(96, 96, 0);
for &accessory in AvatarAccessory::ALL {
let style = AvatarStyleOptions::new(
AvatarKind::Robot,
AvatarBackground::Themed,
accessory,
AvatarColor::NeonMint,
AvatarExpression::Default,
AvatarShape::Square,
);
let image = render_avatar_style_for_id(spec, "accessory@example.com", style);
assert_eq!(image.width(), 96, "{accessory}");
assert!(
render_avatar_svg_style_for_id(spec, "accessory@example.com", style)
.starts_with("<svg ")
);
}
for &color in AvatarColor::ALL {
let style = AvatarStyleOptions::new(
AvatarKind::Robot,
AvatarBackground::Themed,
AvatarAccessory::None,
color,
AvatarExpression::Default,
AvatarShape::Square,
);
let image = render_avatar_style_for_id(spec, "color@example.com", style);
assert_eq!(image.height(), 96, "{color}");
assert!(
render_avatar_svg_style_for_id(spec, "color@example.com", style)
.starts_with("<svg ")
);
}
for &expression in AvatarExpression::ALL {
let style = AvatarStyleOptions::new(
AvatarKind::Robot,
AvatarBackground::Themed,
AvatarAccessory::None,
AvatarColor::Default,
expression,
AvatarShape::Square,
);
let image = render_avatar_style_for_id(spec, "expression@example.com", style);
assert_eq!(image.width(), 96, "{expression}");
assert!(
render_avatar_svg_style_for_id(spec, "expression@example.com", style)
.starts_with("<svg ")
);
}
for &shape in AvatarShape::ALL {
let style = AvatarStyleOptions::new(
AvatarKind::Robot,
AvatarBackground::Themed,
AvatarAccessory::None,
AvatarColor::Default,
AvatarExpression::Default,
shape,
);
let image = render_avatar_style_for_id(spec, "shape@example.com", style);
assert_eq!(image.height(), 96, "{shape}");
assert!(
render_avatar_svg_style_for_id(spec, "shape@example.com", style)
.starts_with("<svg ")
);
}
}
#[test]
fn unsupported_family_accessories_and_expressions_are_skipped() {
let spec = valid_spec(128, 128, 0);
for &kind in AvatarKind::ALL {
if kind.supports_face_layers() {
continue;
}
let baseline_options = AvatarOptions::new(kind, AvatarBackground::Themed);
let unsupported_style = AvatarStyleOptions::new(
kind,
AvatarBackground::Themed,
AvatarAccessory::Eyepatch,
AvatarColor::Default,
AvatarExpression::Winking,
AvatarShape::Square,
);
let baseline =
render_avatar_for_id(spec, "unsupported-layer@example.com", baseline_options);
let unsupported = render_avatar_style_for_id(
spec,
"unsupported-layer@example.com",
unsupported_style,
);
assert_eq!(baseline.as_raw(), unsupported.as_raw(), "{kind}");
let svg = render_avatar_svg_style_for_id(
spec,
"unsupported-layer@example.com",
unsupported_style,
);
assert!(!svg.contains("accessory-eyepatch"), "{kind}");
assert!(!svg.contains("expression-winking"), "{kind}");
}
}
#[test]
fn non_square_svg_frame_shapes_clip_content() {
let spec = valid_spec(128, 128, 0);
let shaped = AvatarStyleOptions::new(
AvatarKind::Robot,
AvatarBackground::White,
AvatarAccessory::None,
AvatarColor::Default,
AvatarExpression::Default,
AvatarShape::Hexagon,
);
let svg = render_avatar_svg_style_for_id(spec, "shape-clip@example.com", shaped);
assert!(svg.contains(r#"<defs><clipPath id="hashavatar-frame-clip">"#));
assert!(svg.contains(r#"<g clip-path="url(#hashavatar-frame-clip)">"#));
assert!(svg.contains(r#"data-layer="shape-hexagon""#));
let square = render_avatar_svg_style_for_id(
spec,
"shape-clip@example.com",
AvatarStyleOptions::from_options(AvatarOptions::new(
AvatarKind::Robot,
AvatarBackground::White,
)),
);
assert!(!square.contains("clipPath"));
assert!(!square.contains("shape-hexagon"));
}
#[test]
fn avatar_spec_validation_rejects_resource_extremes() {
assert!(AvatarSpec::new(MIN_AVATAR_DIMENSION, MIN_AVATAR_DIMENSION, 0).is_ok());
assert!(AvatarSpec::new(MAX_AVATAR_DIMENSION, MAX_AVATAR_DIMENSION, 0).is_ok());
let too_small = AvatarSpec::new(MIN_AVATAR_DIMENSION - 1, 256, 0)
.expect_err("undersized width should be rejected");
let too_large = AvatarSpec::new(256, MAX_AVATAR_DIMENSION + 1, 0)
.expect_err("oversized height should be rejected");
assert_eq!(too_small.width(), MIN_AVATAR_DIMENSION - 1);
assert_eq!(too_large.height(), MAX_AVATAR_DIMENSION + 1);
}
#[test]
fn avatar_spec_reports_raw_rgba_buffer_budget() {
let spec = valid_spec(MAX_AVATAR_DIMENSION, MAX_AVATAR_DIMENSION, 0);
assert_eq!(spec.pixel_count(), MAX_AVATAR_PIXELS);
assert_eq!(spec.rgba_buffer_len(), MAX_AVATAR_RGBA_BYTES);
assert_eq!(
MAX_AVATAR_RGBA_BYTES,
2048_usize * 2048_usize * AVATAR_RGBA_BYTES_PER_PIXEL
);
}
#[test]
fn render_resource_budget_makes_concurrency_memory_math_explicit() {
let spec = valid_spec(256, 128, 0);
let budget = spec.render_resource_budget(8);
assert_eq!(budget.spec(), spec);
assert_eq!(budget.concurrent_renders(), 8);
assert_eq!(budget.raw_rgba_bytes_per_render(), 256 * 128 * 4);
assert_eq!(
budget.raw_rgba_bytes_for_concurrent_renders(),
8 * 256 * 128 * 4
);
assert_eq!(
AvatarRenderResourceBudget::max_concurrent_renders_for_memory_budget(
spec,
16 * 256 * 128 * 4
),
16
);
}
#[test]
fn render_resource_budget_saturates_concurrent_byte_estimates() {
let spec = valid_spec(MAX_AVATAR_DIMENSION, MAX_AVATAR_DIMENSION, 0);
let budget = spec.render_resource_budget(usize::MAX);
assert_eq!(budget.raw_rgba_bytes_for_concurrent_renders(), usize::MAX);
assert_eq!(
AvatarRenderResourceBudget::max_supported_raw_rgba_bytes_for_concurrent_renders(
usize::MAX
),
usize::MAX
);
}
#[test]
fn avatar_spec_default_is_fixed_and_supported() {
let default = AvatarSpec::default();
let explicit = valid_spec(256, 256, 1);
assert_eq!(default, explicit);
assert!(default.is_supported());
assert_eq!(default.width(), 256);
assert_eq!(default.height(), 256);
assert_eq!(default.seed(), 1);
}
#[test]
fn rect_edges_saturate_on_extreme_coordinates() {
let rect = Rect {
left: i32::MAX,
top: i32::MAX,
width: 64,
height: 64,
};
assert_eq!(rect.right(), i32::MAX);
assert_eq!(rect.bottom(), i32::MAX);
}
#[test]
fn rect_intersection_size_saturates_on_extreme_coordinates() {
let rect = Rect {
left: i32::MIN,
top: i32::MIN,
width: u32::MAX,
height: u32::MAX,
};
let intersection = rect
.intersect(rect)
.expect("extreme rectangles should intersect");
assert_eq!(intersection.left(), i32::MIN);
assert_eq!(intersection.top(), i32::MIN);
assert_eq!(intersection.width(), i32::MAX as u32);
assert_eq!(intersection.height(), i32::MAX as u32);
}
#[test]
fn rect_size_builder_clamps_zero_dimensions() {
let rect = Rect::at(4, 8).of_size(0, 0);
assert_eq!(rect.width(), 1);
assert_eq!(rect.height(), 1);
}
#[test]
fn avatar_identity_implements_zeroize() {
fn assert_zeroize<T: Zeroize>() {}
assert_zeroize::<AvatarIdentity>();
}
#[test]
fn antialiased_zero_length_line_draws_single_pixel() {
let mut image = RgbaImage::new(4, 4);
draw_antialiased_line_segment_mut(
&mut image,
(1, 1),
(1, 1),
Rgba([10, 20, 30, 255]),
interpolate,
);
assert_eq!(image.get_pixel(1, 1), &Rgba([10, 20, 30, 255]));
}
#[test]
fn polygon_rasterizer_skips_unpaired_intersections() {
let mut image = RgbaImage::new(32, 32);
let triangle_with_horizontal_base =
[Point::new(0, 0), Point::new(16, 16), Point::new(31, 0)];
draw_polygon_mut(
&mut image,
&triangle_with_horizontal_base,
Rgba([255, 0, 0, 255]),
);
assert!(image.pixels().any(|pixel| pixel.0[3] == 255));
}
#[test]
fn polygon_rasterizer_skips_zero_sized_images() {
let mut zero_width = RgbaImage::new(0, 8);
let mut zero_height = RgbaImage::new(8, 0);
let color = Rgba([255, 0, 0, 255]);
let poly = [Point::new(0, 0), Point::new(4, 0), Point::new(0, 4)];
draw_polygon_mut(&mut zero_width, &poly, color);
draw_polygon_mut(&mut zero_height, &poly, color);
assert!(zero_width.is_empty());
assert!(zero_height.is_empty());
}
#[test]
fn ellipse_rasterizer_handles_max_supported_radius() {
let mut image = RgbaImage::new(1, 1);
let mut render_calls = 0;
draw_ellipse(
|_, _, _, _, _| render_calls += 1,
&mut image,
(
MAX_AVATAR_DIMENSION as i32 / 2,
MAX_AVATAR_DIMENSION as i32 / 2,
),
MAX_AVATAR_DIMENSION as i32 / 2,
MAX_AVATAR_DIMENSION as i32 / 2,
);
assert!(render_calls > 0);
}
#[test]
fn jpeg_alpha_flattening_uses_wide_intermediates() {
let image = RgbaImage::from_vec(
3,
1,
vec![
0, 0, 0, 0, // transparent black over white
0, 0, 0, 128, // half alpha black over white
10, 20, 30, 255, // opaque color
],
)
.expect("test image should be valid");
let rgb = rgba_to_rgb_over_white(&image);
assert_eq!(
rgb,
vec![
255, 255, 255, // transparent becomes white
127, 127, 127, // rounded half-alpha black over white
10, 20, 30,
]
);
}
#[test]
fn rgba_pixel_zeroizer_scrubs_owned_render_buffers() {
let mut image = RgbaImage::from_vec(
2,
1,
vec![
10, 20, 30, 255, // opaque pixel
40, 50, 60, 128, // translucent pixel
],
)
.expect("test image should be valid");
zeroize_rgba_pixels(&mut image);
assert!(image.as_raw().iter().all(|byte| *byte == 0));
}
#[test]
fn weighted_channel_sum_rejects_invalid_weights() {
assert_eq!(weighted_channel_sum(255, 0, 0.25, 0.75), 63);
assert_eq!(weighted_channel_sum(255, 0, 0.0, 0.0), 0);
assert_eq!(weighted_channel_sum(255, 0, f32::NAN, 1.0), 0);
assert_eq!(weighted_channel_sum(255, 0, f32::INFINITY, 1.0), 0);
}
#[test]
fn render_avatar_for_id_supports_all_avatar_kinds() {
let spec = valid_spec(96, 96, 0);
for &kind in AvatarKind::ALL {
let image = render_avatar_for_id(
spec,
"integration@example.com",
AvatarOptions::new(kind, AvatarBackground::Themed),
);
assert_eq!(image.width(), 96);
assert_eq!(image.height(), 96);
}
}
#[test]
fn render_avatar_svg_for_id_supports_all_avatar_kinds() {
let spec = valid_spec(96, 96, 0);
for &kind in AvatarKind::ALL {
let svg = render_avatar_svg_for_id(
spec,
"integration@example.com",
AvatarOptions::new(kind, AvatarBackground::Themed),
);
assert!(svg.contains("<svg"));
assert!(svg.contains(&format!("{kind} avatar")));
}
}
#[test]
fn lower_variation_presets_change_for_different_identities() {
let spec = valid_spec(128, 128, 0);
for kind in [
AvatarKind::Ghost,
AvatarKind::Slime,
AvatarKind::Wizard,
AvatarKind::Skull,
] {
let left = render_avatar_for_id(
spec,
"alice@example.com",
AvatarOptions::new(kind, AvatarBackground::Themed),
);
let right = render_avatar_for_id(
spec,
"bob@example.com",
AvatarOptions::new(kind, AvatarBackground::Themed),
);
assert_ne!(
image_fingerprint(&left),
image_fingerprint(&right),
"{kind}"
);
}
}
#[test]
fn lower_variation_svg_presets_change_for_different_identities() {
let spec = valid_spec(128, 128, 0);
for kind in [
AvatarKind::Ghost,
AvatarKind::Slime,
AvatarKind::Wizard,
AvatarKind::Skull,
] {
let left = render_avatar_svg_for_id(
spec,
"alice@example.com",
AvatarOptions::new(kind, AvatarBackground::Themed),
);
let right = render_avatar_svg_for_id(
spec,
"bob@example.com",
AvatarOptions::new(kind, AvatarBackground::Themed),
);
assert_ne!(left, right, "{kind}");
}
}
#[test]
#[cfg(not(any(feature = "blake3", feature = "xxh3")))]
fn visual_fingerprints_are_stable() {
for (label, options) in regression_scenarios() {
let image =
render_avatar_for_id(valid_spec(128, 128, 0), "snapshot@example.com", options);
let fingerprint = image_fingerprint(&image);
let expected =
regression_fingerprint_for(label).expect("missing golden regression fingerprint");
assert_eq!(fingerprint, expected, "fingerprint mismatch for {label}");
}
for (label, style) in style_regression_scenarios() {
let image =
render_avatar_style_for_id(valid_spec(128, 128, 0), "snapshot@example.com", style);
let fingerprint = image_fingerprint(&image);
let expected =
regression_fingerprint_for(label).expect("missing golden regression fingerprint");
assert_eq!(fingerprint, expected, "fingerprint mismatch for {label}");
}
let auto =
super::render_avatar_auto_for_id(valid_spec(128, 128, 0), "snapshot@example.com")
.expect("automatic avatar should render");
let auto_fingerprint = image_fingerprint(&auto);
let auto_expected =
regression_fingerprint_for("auto-layered").expect("missing golden auto fingerprint");
assert_eq!(
auto_fingerprint, auto_expected,
"fingerprint mismatch for auto-layered"
);
}
#[ignore]
#[test]
#[cfg(not(any(feature = "blake3", feature = "xxh3")))]
fn print_visual_fingerprints() {
for (label, options) in [
(
"cat-themed",
AvatarOptions::new(AvatarKind::Cat, AvatarBackground::Themed),
),
(
"cat-white",
AvatarOptions::new(AvatarKind::Cat, AvatarBackground::White),
),
(
"dog-themed",
AvatarOptions::new(AvatarKind::Dog, AvatarBackground::Themed),
),
(
"robot-white",
AvatarOptions::new(AvatarKind::Robot, AvatarBackground::White),
),
(
"monster-themed",
AvatarOptions::new(AvatarKind::Monster, AvatarBackground::Themed),
),
(
"ghost-themed",
AvatarOptions::new(AvatarKind::Ghost, AvatarBackground::Themed),
),
(
"slime-white",
AvatarOptions::new(AvatarKind::Slime, AvatarBackground::White),
),
(
"bird-themed",
AvatarOptions::new(AvatarKind::Bird, AvatarBackground::Themed),
),
(
"wizard-white",
AvatarOptions::new(AvatarKind::Wizard, AvatarBackground::White),
),
(
"skull-themed",
AvatarOptions::new(AvatarKind::Skull, AvatarBackground::Themed),
),
(
"paws-themed",
AvatarOptions::new(AvatarKind::Paws, AvatarBackground::Themed),
),
(
"planet-themed",
AvatarOptions::new(AvatarKind::Planet, AvatarBackground::Themed),
),
(
"rocket-themed",
AvatarOptions::new(AvatarKind::Rocket, AvatarBackground::Themed),
),
(
"mushroom-themed",
AvatarOptions::new(AvatarKind::Mushroom, AvatarBackground::Themed),
),
(
"cactus-themed",
AvatarOptions::new(AvatarKind::Cactus, AvatarBackground::Themed),
),
(
"frog-themed",
AvatarOptions::new(AvatarKind::Frog, AvatarBackground::Themed),
),
(
"panda-themed",
AvatarOptions::new(AvatarKind::Panda, AvatarBackground::Themed),
),
(
"cupcake-themed",
AvatarOptions::new(AvatarKind::Cupcake, AvatarBackground::Themed),
),
(
"pizza-themed",
AvatarOptions::new(AvatarKind::Pizza, AvatarBackground::Themed),
),
(
"icecream-themed",
AvatarOptions::new(AvatarKind::Icecream, AvatarBackground::Themed),
),
(
"octopus-themed",
AvatarOptions::new(AvatarKind::Octopus, AvatarBackground::Themed),
),
(
"knight-themed",
AvatarOptions::new(AvatarKind::Knight, AvatarBackground::Themed),
),
(
"bear-themed",
AvatarOptions::new(AvatarKind::Bear, AvatarBackground::Themed),
),
(
"penguin-themed",
AvatarOptions::new(AvatarKind::Penguin, AvatarBackground::Themed),
),
(
"dragon-themed",
AvatarOptions::new(AvatarKind::Dragon, AvatarBackground::Themed),
),
(
"ninja-themed",
AvatarOptions::new(AvatarKind::Ninja, AvatarBackground::Themed),
),
(
"astronaut-themed",
AvatarOptions::new(AvatarKind::Astronaut, AvatarBackground::Themed),
),
(
"diamond-themed",
AvatarOptions::new(AvatarKind::Diamond, AvatarBackground::Themed),
),
(
"coffee-cup-themed",
AvatarOptions::new(AvatarKind::CoffeeCup, AvatarBackground::Themed),
),
(
"shield-themed",
AvatarOptions::new(AvatarKind::Shield, AvatarBackground::Themed),
),
] {
let image =
render_avatar_for_id(valid_spec(128, 128, 0), "snapshot@example.com", options);
println!("{label}: {}", image_fingerprint(&image));
}
for (label, style) in style_regression_scenarios() {
let image =
render_avatar_style_for_id(valid_spec(128, 128, 0), "snapshot@example.com", style);
println!("{label}: {}", image_fingerprint(&image));
}
let auto =
super::render_avatar_auto_for_id(valid_spec(128, 128, 0), "snapshot@example.com")
.expect("automatic avatar should render");
println!("auto-layered: {}", image_fingerprint(&auto));
}
#[cfg(not(any(feature = "blake3", feature = "xxh3")))]
fn regression_scenarios() -> [(&'static str, AvatarOptions); 30] {
[
(
"cat-themed",
AvatarOptions::new(AvatarKind::Cat, AvatarBackground::Themed),
),
(
"cat-white",
AvatarOptions::new(AvatarKind::Cat, AvatarBackground::White),
),
(
"dog-themed",
AvatarOptions::new(AvatarKind::Dog, AvatarBackground::Themed),
),
(
"robot-white",
AvatarOptions::new(AvatarKind::Robot, AvatarBackground::White),
),
(
"monster-themed",
AvatarOptions::new(AvatarKind::Monster, AvatarBackground::Themed),
),
(
"ghost-themed",
AvatarOptions::new(AvatarKind::Ghost, AvatarBackground::Themed),
),
(
"slime-white",
AvatarOptions::new(AvatarKind::Slime, AvatarBackground::White),
),
(
"bird-themed",
AvatarOptions::new(AvatarKind::Bird, AvatarBackground::Themed),
),
(
"wizard-white",
AvatarOptions::new(AvatarKind::Wizard, AvatarBackground::White),
),
(
"skull-themed",
AvatarOptions::new(AvatarKind::Skull, AvatarBackground::Themed),
),
(
"paws-themed",
AvatarOptions::new(AvatarKind::Paws, AvatarBackground::Themed),
),
(
"planet-themed",
AvatarOptions::new(AvatarKind::Planet, AvatarBackground::Themed),
),
(
"rocket-themed",
AvatarOptions::new(AvatarKind::Rocket, AvatarBackground::Themed),
),
(
"mushroom-themed",
AvatarOptions::new(AvatarKind::Mushroom, AvatarBackground::Themed),
),
(
"cactus-themed",
AvatarOptions::new(AvatarKind::Cactus, AvatarBackground::Themed),
),
(
"frog-themed",
AvatarOptions::new(AvatarKind::Frog, AvatarBackground::Themed),
),
(
"panda-themed",
AvatarOptions::new(AvatarKind::Panda, AvatarBackground::Themed),
),
(
"cupcake-themed",
AvatarOptions::new(AvatarKind::Cupcake, AvatarBackground::Themed),
),
(
"pizza-themed",
AvatarOptions::new(AvatarKind::Pizza, AvatarBackground::Themed),
),
(
"icecream-themed",
AvatarOptions::new(AvatarKind::Icecream, AvatarBackground::Themed),
),
(
"octopus-themed",
AvatarOptions::new(AvatarKind::Octopus, AvatarBackground::Themed),
),
(
"knight-themed",
AvatarOptions::new(AvatarKind::Knight, AvatarBackground::Themed),
),
(
"bear-themed",
AvatarOptions::new(AvatarKind::Bear, AvatarBackground::Themed),
),
(
"penguin-themed",
AvatarOptions::new(AvatarKind::Penguin, AvatarBackground::Themed),
),
(
"dragon-themed",
AvatarOptions::new(AvatarKind::Dragon, AvatarBackground::Themed),
),
(
"ninja-themed",
AvatarOptions::new(AvatarKind::Ninja, AvatarBackground::Themed),
),
(
"astronaut-themed",
AvatarOptions::new(AvatarKind::Astronaut, AvatarBackground::Themed),
),
(
"diamond-themed",
AvatarOptions::new(AvatarKind::Diamond, AvatarBackground::Themed),
),
(
"coffee-cup-themed",
AvatarOptions::new(AvatarKind::CoffeeCup, AvatarBackground::Themed),
),
(
"shield-themed",
AvatarOptions::new(AvatarKind::Shield, AvatarBackground::Themed),
),
]
}
#[cfg(not(any(feature = "blake3", feature = "xxh3")))]
fn style_regression_scenarios() -> [(&'static str, AvatarStyleOptions); 4] {
[
(
"style-robot-glasses-gold-happy-circle",
AvatarStyleOptions::new(
AvatarKind::Robot,
AvatarBackground::Themed,
AvatarAccessory::Glasses,
AvatarColor::Gold,
AvatarExpression::Happy,
AvatarShape::Circle,
),
),
(
"style-fox-halo-neon-cool-squircle",
AvatarStyleOptions::new(
AvatarKind::Fox,
AvatarBackground::White,
AvatarAccessory::Halo,
AvatarColor::NeonMint,
AvatarExpression::Cool,
AvatarShape::Squircle,
),
),
(
"style-monster-horns-crimson-grumpy-hexagon",
AvatarStyleOptions::new(
AvatarKind::Monster,
AvatarBackground::Dark,
AvatarAccessory::Horns,
AvatarColor::Crimson,
AvatarExpression::Grumpy,
AvatarShape::Hexagon,
),
),
(
"style-knight-scarf-deepsea-winking-octagon",
AvatarStyleOptions::new(
AvatarKind::Knight,
AvatarBackground::Light,
AvatarAccessory::Scarf,
AvatarColor::DeepSeaBlue,
AvatarExpression::Winking,
AvatarShape::Octagon,
),
),
]
}
#[cfg(not(any(feature = "blake3", feature = "xxh3")))]
fn regression_fingerprint_for(label: &str) -> Option<&'static str> {
include_str!("../tests/golden_fingerprints.txt")
.lines()
.filter(|line| !line.trim().is_empty() && !line.trim_start().starts_with('#'))
.find_map(|line| {
let (name, value) = line.split_once('=')?;
(name.trim() == label).then_some(value.trim())
})
}
fn image_fingerprint(image: &RgbaImage) -> String {
let digest = <TestSha512 as sha2::Digest>::digest(image.as_raw());
digest[..12]
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
}
}