use egui::{Color32, Mesh, Painter, Pos2, Rect, Stroke, Vec2};
use std::f32::consts::TAU;
pub const PIN_RISE: f32 = 18.0;
pub const PIN_GRIP: f32 = 16.0;
const BULB_RADIUS: f32 = 4.6;
const BRONZE_SHADOW: [f32; 3] = [34.0, 28.0, 19.0];
const BRONZE_BODY: [f32; 3] = [104.0, 86.0, 58.0];
const BRONZE_GLINT: [f32; 3] = [196.0, 170.0, 124.0];
const LIGHT_Y: f32 = -0.5;
const LIGHT_Z: f32 = 0.866_025_4;
const HALF_Y: f32 = -0.258_819_04;
const HALF_Z: f32 = 0.965_925_8;
pub fn pin_bulb(anchor: Pos2) -> Pos2 {
anchor - Vec2::new(0.0, PIN_RISE)
}
pub fn pin_grip(anchor: Pos2) -> Rect {
Rect::from_center_size(pin_bulb(anchor), Vec2::splat(PIN_GRIP))
}
pub fn pin(painter: &Painter, anchor: Pos2, seized: bool) {
let bulb = pin_bulb(anchor);
let heat = if seized { 0.07 } else { 0.0 };
let left = bulb + Vec2::new(-2.5, 2.2);
let right = bulb + Vec2::new(2.5, 2.2);
let shadow = vec![
left + Vec2::splat(0.8),
right + Vec2::splat(0.8),
anchor + Vec2::new(0.0, 1.0),
];
let _shadow = painter.add(egui::Shape::convex_polygon(
shadow,
Color32::from_black_alpha(96),
Stroke::NONE,
));
stamp(
painter,
vec![left, right, anchor],
&[[right, anchor]],
&[[anchor, left]],
heat,
);
sphere(painter, bulb, heat);
let _rim = painter.circle_stroke(bulb, BULB_RADIUS, Stroke::new(0.75_f32, bronze(0.16)));
}
fn stamp(
painter: &Painter,
silhouette: Vec<Pos2>,
crowns: &[[Pos2; 2]],
soles: &[[Pos2; 2]],
heat: f32,
) {
let _body = painter.add(egui::Shape::convex_polygon(
silhouette,
bronze(0.56 + heat),
Stroke::NONE,
));
for edge in crowns {
let _crown = painter.line_segment(*edge, Stroke::new(0.8_f32, bronze(0.80 + heat)));
}
for edge in soles {
let _sole = painter.line_segment(*edge, Stroke::new(0.8_f32, bronze(0.18 + heat)));
}
}
fn sphere(painter: &Painter, center: Pos2, heat: f32) {
const RINGS: u32 = 4;
const SECTORS: u32 = 24;
let mut mesh = Mesh::default();
mesh.reserve_vertices((1 + RINGS * SECTORS) as usize);
mesh.reserve_triangles((SECTORS * (2 * RINGS - 1)) as usize);
mesh.colored_vertex(center, sphere_bronze(0.0, 0.0, heat));
for ring in 1..=RINGS {
let radius = ring as f32 / RINGS as f32;
for sector in 0..SECTORS {
let angle = TAU * sector as f32 / SECTORS as f32;
let x = radius * angle.cos();
let y = radius * angle.sin();
mesh.colored_vertex(
center + Vec2::new(x, y) * BULB_RADIUS,
sphere_bronze(y, (1.0 - radius * radius).sqrt(), heat),
);
}
}
for sector in 0..SECTORS {
mesh.add_triangle(0, 1 + sector, 1 + (sector + 1) % SECTORS);
}
for ring in 2..=RINGS {
let inner = 1 + (ring - 2) * SECTORS;
let outer = inner + SECTORS;
for sector in 0..SECTORS {
let next = (sector + 1) % SECTORS;
mesh.add_triangle(inner + sector, outer + sector, inner + next);
mesh.add_triangle(inner + next, outer + sector, outer + next);
}
}
let _bulb = painter.add(egui::Shape::mesh(mesh));
}
fn sphere_bronze(ny: f32, nz: f32, heat: f32) -> Color32 {
let diffuse = (ny * LIGHT_Y + nz * LIGHT_Z).max(0.0);
let specular = (ny * HALF_Y + nz * HALF_Z).max(0.0).powf(14.0);
bronze(0.12 + 0.42 * diffuse + 0.38 * specular + heat)
}
fn bronze(tone: f32) -> Color32 {
let tone = tone.clamp(0.0, 1.0);
let (lo, hi, t) = if tone < 0.6 {
(BRONZE_SHADOW, BRONZE_BODY, tone / 0.6)
} else {
(BRONZE_BODY, BRONZE_GLINT, (tone - 0.6) / 0.4)
};
let channel = |i: usize| (lo[i] + (hi[i] - lo[i]) * t).round() as u8;
Color32::from_rgb(channel(0), channel(1), channel(2))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pin_die_keeps_the_bulb_above_its_map_coordinate() {
let anchor = Pos2::new(30.0, 40.0);
assert_eq!(pin_bulb(anchor), Pos2::new(30.0, 40.0 - PIN_RISE));
assert!(pin_grip(anchor).contains(pin_bulb(anchor)));
assert!(!pin_grip(anchor).contains(anchor));
}
#[test]
fn bronze_charge_preserves_its_three_fixed_swatch_anchors() {
assert_eq!(bronze(0.0), Color32::from_rgb(34, 28, 19));
assert_eq!(bronze(0.6), Color32::from_rgb(104, 86, 58));
assert_eq!(bronze(1.0), Color32::from_rgb(196, 170, 124));
}
}