use std::error::Error;
use std::f64::consts::{PI, TAU};
use std::fmt;
use tiny_skia::{
Color, FillRule, LineCap, LineJoin, Paint, Path, PathBuilder, Pixmap,
Stroke, Transform,
};
use crate::color::{Palette, Rgb};
use crate::genome::{EdgeStyle, RING_SEGMENTS, VisualGenes};
use crate::geometry::{Mesh, Role, centroid, dot, normalize};
const SOLID_SCALE: f64 = 0.27;
const CAMERA_DIST: f64 = 4.0;
const TILT_MIN_DEG: f64 = 15.0;
const TILT_RANGE_DEG: f64 = 60.0;
const LIGHT_DIR: [f64; 3] = [-0.45, 0.6, 0.65];
const AMBIENT: f64 = 0.4;
const DIFFUSE: f64 = 0.75;
const EDGE_DARK: Rgb = Rgb::new(20, 20, 28);
const EDGE_LIGHT: Rgb = Rgb::new(245, 245, 245);
const EDGE_THIN: f32 = 0.006;
const EDGE_THICK: f32 = 0.014;
const RING_RADIUS: f64 = 0.43;
const RING_TRACK_WIDTH: f64 = 0.008;
const RING_LIT_WIDTH: f64 = 0.035;
const RING_GAP_DEG: f64 = 1.5;
const ARC_STEPS_PER_TURN: f64 = 180.0;
const ANCHOR_HALF_WIDTH: f64 = 0.025;
const ANCHOR_OUTER: f64 = 0.045;
const ANCHOR_INNER: f64 = 0.02;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum View {
Solid3d,
Flat,
}
#[derive(Debug)]
pub struct RenderError {
kind: RenderErrorKind,
}
#[derive(Debug)]
enum RenderErrorKind {
InvalidSize(u32),
DegenerateFace,
Encode(Box<dyn Error + Send + Sync>),
}
impl RenderError {
#[must_use]
pub fn is_invalid_size(&self) -> bool {
matches!(self.kind, RenderErrorKind::InvalidSize(_))
}
}
impl fmt::Display for RenderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
RenderErrorKind::InvalidSize(size) => {
write!(f, "invalid image size {size}")
}
RenderErrorKind::DegenerateFace => f.write_str("degenerate face"),
RenderErrorKind::Encode(_) => f.write_str("png encoding failed"),
}
}
}
impl Error for RenderError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match &self.kind {
RenderErrorKind::Encode(e) => Some(e.as_ref()),
_ => None,
}
}
}
fn paint(c: Rgb) -> Paint<'static> {
let mut p = Paint::default();
p.set_color_rgba8(c.r, c.g, c.b, 255);
p.anti_alias = true;
p
}
fn role_color(pal: &Palette, role: Role) -> Rgb {
match role {
Role::Cap => pal.base,
Role::SideA => pal.accent1,
Role::SideB => pal.accent2,
}
}
fn edge_stroke(edge: EdgeStyle, size: f32) -> Option<(Rgb, Stroke)> {
let (color, width) = match edge {
EdgeStyle::None => return None,
EdgeStyle::ThinDark => (EDGE_DARK, EDGE_THIN),
EdgeStyle::ThinLight => (EDGE_LIGHT, EDGE_THIN),
EdgeStyle::ThickDark => (EDGE_DARK, EDGE_THICK),
};
let stroke = Stroke {
width: width * size,
line_join: LineJoin::Round,
..Stroke::default()
};
Some((color, stroke))
}
pub(crate) fn render(
g: &VisualGenes,
view: View,
size: u32,
) -> Result<Pixmap, RenderError> {
let mut pm = Pixmap::new(size, size)
.ok_or(RenderError { kind: RenderErrorKind::InvalidSize(size) })?;
let pal = Palette::from_genes(g);
let bg = pal.background;
pm.fill(Color::from_rgba8(bg.r, bg.g, bg.b, 255));
let s = f64::from(size);
let (cx, cy) = (s / 2.0, s / 2.0);
let k = SOLID_SCALE * s;
let mut mesh = Mesh::from_genes(g);
let tilt = match view {
View::Solid3d => (TILT_MIN_DEG
+ f64::from(g.pitch) * TILT_RANGE_DEG / 7.0)
.to_radians(),
View::Flat => PI / 2.0,
};
mesh.tilt(tilt);
let mesh = mesh;
let project = |v: [f64; 3]| -> (f32, f32) {
let p = match view {
View::Solid3d => CAMERA_DIST / (CAMERA_DIST - v[2]),
View::Flat => 1.0,
};
((cx + v[0] * p * k) as f32, (cy - v[1] * p * k) as f32)
};
let mut visible: Vec<(f64, usize)> = mesh
.faces
.iter()
.enumerate()
.filter(|(_, f)| {
let n = mesh.face_normal(f);
match view {
View::Solid3d => {
let v0 = mesh.verts[f.indices[0]];
dot(n, [-v0[0], -v0[1], CAMERA_DIST - v0[2]]) > 1e-9
}
View::Flat => n[2] > 1e-6,
}
})
.map(|(i, f)| (centroid(&mesh.verts, &f.indices)[2], i))
.collect();
visible.sort_by(|a, b| a.0.total_cmp(&b.0));
let light = normalize(LIGHT_DIR);
let edge = edge_stroke(g.edge, size as f32);
for (_, fi) in visible {
let f = &mesh.faces[fi];
let mut pb = PathBuilder::new();
for (j, &vi) in f.indices.iter().enumerate() {
let (x, y) = project(mesh.verts[vi]);
if j == 0 { pb.move_to(x, y) } else { pb.line_to(x, y) }
}
pb.close();
let path = pb
.finish()
.ok_or(RenderError { kind: RenderErrorKind::DegenerateFace })?;
let base = role_color(&pal, f.role);
let color = match view {
View::Solid3d => {
let lambert = dot(mesh.face_normal(f), light).max(0.0);
base.scale(AMBIENT + DIFFUSE * lambert)
}
View::Flat => base,
};
pm.fill_path(
&path,
&paint(color),
FillRule::Winding,
Transform::identity(),
None,
);
if let Some((ec, stroke)) = &edge {
pm.stroke_path(
&path,
&paint(*ec),
stroke,
Transform::identity(),
None,
);
}
}
draw_ring(&mut pm, g, &pal, s);
Ok(pm)
}
pub(crate) fn encode_png(pm: &Pixmap) -> Result<Vec<u8>, RenderError> {
pm.encode_png().map_err(|e| RenderError {
kind: RenderErrorKind::Encode(Box::new(e)),
})
}
fn arc(cx: f64, cy: f64, r: f64, a0: f64, a1: f64) -> Option<Path> {
let steps =
((a1 - a0).abs() / TAU * ARC_STEPS_PER_TURN).ceil().max(4.0) as usize;
let mut pb = PathBuilder::new();
for i in 0..=steps {
let a = a0 + (a1 - a0) * i as f64 / steps as f64;
let (x, y) = ((cx + r * a.cos()) as f32, (cy + r * a.sin()) as f32);
if i == 0 { pb.move_to(x, y) } else { pb.line_to(x, y) }
}
pb.finish()
}
fn draw_ring(pm: &mut Pixmap, g: &VisualGenes, pal: &Palette, s: f64) {
let (cx, cy, r) = (s / 2.0, s / 2.0, RING_RADIUS * s);
let track =
Stroke { width: (RING_TRACK_WIDTH * s) as f32, ..Stroke::default() };
if let Some(p) = arc(cx, cy, r, 0.0, TAU) {
pm.stroke_path(
&p,
&paint(pal.accent2),
&track,
Transform::identity(),
None,
);
}
let seg = TAU / f64::from(RING_SEGMENTS);
let gap = RING_GAP_DEG.to_radians();
let lit = Stroke {
width: (RING_LIT_WIDTH * s) as f32,
line_cap: LineCap::Butt,
..Stroke::default()
};
let pattern = g.ring_pattern();
for i in 0..RING_SEGMENTS {
if pattern >> i & 1 == 1 {
let a0 = -PI / 2.0 + f64::from(i) * seg + gap;
if let Some(p) = arc(cx, cy, r, a0, a0 + seg - 2.0 * gap) {
pm.stroke_path(
&p,
&paint(pal.accent1),
&lit,
Transform::identity(),
None,
);
}
}
}
let w = (ANCHOR_HALF_WIDTH * s) as f32;
let top = (cy - r - ANCHOR_OUTER * s) as f32;
let mut pb = PathBuilder::new();
pb.move_to(cx as f32 - w, top);
pb.line_to(cx as f32 + w, top);
pb.line_to(cx as f32, (cy - r + ANCHOR_INNER * s) as f32);
pb.close();
if let Some(p) = pb.finish() {
pm.fill_path(
&p,
&paint(pal.base),
FillRule::Winding,
Transform::identity(),
None,
);
}
}