use std::fmt::Write as _;
use basin::{BasicState, CostFunction, Gradient, GradientDescent, Solver, State};
const GRID: usize = 16;
const X0: f64 = -3.0;
const X1: f64 = 3.0;
const Y0: f64 = -3.0;
const Y1: f64 = 3.0;
const BOWL_CX: f64 = 0.29;
const BOWL_CY: f64 = -0.15;
const BOWL_K: f64 = 0.2;
const BOWL_ASPECT: f64 = 0.65;
const BOWL_ROT: f64 = 0.35;
const RIPPLE_AMP: f64 = 0.22;
const RIPPLE_FREQ: f64 = 1.4;
const RIPPLE_FADE: f64 = 0.7;
const WATER_LEVEL: f64 = 0.02;
const TILE_W: f64 = 22.0;
const TILE_H: f64 = 16.0;
const Z_SCREEN: f64 = 96.0; const Z_WORLD: f64 = 16.0;
const LIGHT: [f64; 3] = [0.5, -0.35, 0.79];
const RIVER_START: [f64; 2] = [-3.4, -2.5];
const RIVER_ALPHA: f64 = 0.06;
const RIVER_BETA: f64 = 0.0;
const RIVER_ITERS: usize = 400;
const RIVER_POINTS: usize = 120;
const RIVER_W_SRC: f64 = 0.08;
const RIVER_W_MOUTH: f64 = 0.6;
const RIVER_SEGS: usize = 16;
const SPRING_R: f64 = 0.0;
const TREE_AT: [f64; 2] = [2.0, -0.9];
const PAD: f64 = 12.0;
#[allow(dead_code)]
const PAPER: Rgb = Rgb(244, 241, 222); const CUSTOM: [Rgb; 5] = [
hex("#5E7763"),
hex("#7C7A62"),
hex("#9C836F"),
hex("#B59A85"),
hex("#DDD5CB"),
];
const TERRAIN_RAMP: &[Rgb] = &CUSTOM;
const WATER: Rgb = Rgb(71, 153, 173); const TREE_DARK: Rgb = Rgb(54, 90, 82); const TREE_LIT: Rgb = Rgb(92, 138, 122); const TRUNK: Rgb = Rgb(52, 46, 39);
#[derive(Clone, Copy, PartialEq, Eq)]
enum Theme {
Day,
Night,
}
const MOON: Rgb = hex("#d8dcec");
fn moonlight(c: Rgb) -> Rgb {
let [l, a, b] = rgb_to_oklab(c);
oklab_to_rgb([l * 0.75 + 0.035, a * 0.76, b * 0.46 - 0.018])
}
struct Palette {
terrain: Vec<Rgb>,
water: Rgb,
tree_dark: Rgb,
tree_lit: Rgb,
trunk: Rgb,
}
impl Palette {
fn new(theme: Theme) -> Self {
match theme {
Theme::Day => Palette {
terrain: TERRAIN_RAMP.to_vec(),
water: WATER,
tree_dark: TREE_DARK,
tree_lit: TREE_LIT,
trunk: TRUNK,
},
Theme::Night => Palette {
terrain: TERRAIN_RAMP.iter().map(|&c| moonlight(c)).collect(),
water: moonlight(WATER).lerp(MOON, 0.14),
tree_dark: moonlight(TREE_DARK),
tree_lit: moonlight(TREE_LIT),
trunk: moonlight(TRUNK),
},
}
}
}
fn raw_height(x: f64, y: f64) -> f64 {
let dx = x - BOWL_CX;
let dy = y - BOWL_CY;
let (ca, sa) = (BOWL_ROT.cos(), BOWL_ROT.sin());
let u = ca * dx + sa * dy;
let v = -sa * dx + ca * dy;
let bowl = BOWL_K * (u * u + BOWL_ASPECT * v * v);
bowl + ripple(u, v, bowl)
}
fn ripple(u: f64, v: f64, bowl: f64) -> f64 {
let damp = (bowl / RIPPLE_FADE).min(1.0);
damp * RIPPLE_AMP
* ((RIPPLE_FREQ * u + 0.4).sin()
+ 0.7 * (1.7 * RIPPLE_FREQ * v - 0.7).sin()
+ 0.5 * (0.9 * RIPPLE_FREQ * (u + v)).sin())
}
struct Surface {
hmin: f64,
hmax: f64,
water: f64,
}
impl Surface {
fn new() -> Self {
let n = 96;
let mut hmin = f64::INFINITY;
let mut hmax = f64::NEG_INFINITY;
for j in 0..=n {
for i in 0..=n {
let x = X0 + (X1 - X0) * i as f64 / n as f64;
let y = Y0 + (Y1 - Y0) * j as f64 / n as f64;
let h = raw_height(x, y);
hmin = hmin.min(h);
hmax = hmax.max(h);
}
}
Surface {
hmin,
hmax,
water: WATER_LEVEL,
}
}
fn hn(&self, x: f64, y: f64) -> f64 {
((raw_height(x, y) - self.hmin) / (self.hmax - self.hmin)).clamp(0.0, 1.0)
}
fn surface_point(&self, x: f64, y: f64, lift: f64) -> (f64, f64) {
let gx = (x - X0) / (X1 - X0) * GRID as f64;
let gy = (y - Y0) / (Y1 - Y0) * GRID as f64;
project(gx, gy, self.hn(x, y).max(self.water) + lift)
}
}
fn project(gx: f64, gy: f64, h: f64) -> (f64, f64) {
let sx = (gx - gy) * TILE_W;
let sy = (gx + gy) * TILE_H - h * Z_SCREEN;
(sx, sy)
}
fn normalize3(v: [f64; 3]) -> [f64; 3] {
let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-12);
[v[0] / n, v[1] / n, v[2] / n]
}
struct Bowl;
impl CostFunction for Bowl {
type Param = Vec<f64>;
type Output = f64;
fn cost(&self, x: &Vec<f64>) -> f64 {
raw_height(x[0], x[1])
}
}
impl Gradient for Bowl {
type Param = Vec<f64>;
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Vec<f64> {
let h = 1e-4;
let mut g = vec![0.0; 2];
for (k, gk) in g.iter_mut().enumerate() {
let mut xp = x.clone();
let mut xm = x.clone();
xp[k] += h;
xm[k] -= h;
*gk = (raw_height(xp[0], xp[1]) - raw_height(xm[0], xm[1])) / (2.0 * h);
}
g
}
}
fn trace_river() -> Vec<[f64; 2]> {
let mut solver = GradientDescent::new(RIVER_ALPHA).with_momentum(RIVER_BETA);
let mut state = solver.init(&Bowl, BasicState::new(RIVER_START.to_vec()));
let mut full = vec![[state.param()[0], state.param()[1]]];
for _ in 0..RIVER_ITERS {
let (next, stop) = solver.next_iter(&Bowl, state);
state = next;
full.push([state.param()[0], state.param()[1]]);
if stop.is_some() {
break;
}
}
let stride = (full.len() / RIVER_POINTS).max(1);
let mut pts: Vec<[f64; 2]> = full.iter().step_by(stride).copied().collect();
if let Some(&last) = full.last() {
if pts.last() != Some(&last) {
pts.push(last);
}
}
pts
}
fn main() {
let mut args = std::env::args().skip(1);
let out_path = args.next().unwrap_or_else(|| "basin-favicon.svg".into());
let mode = args.next().unwrap_or_default();
let surf = Surface::new();
let river = trace_river();
let pool_center = [BOWL_CX, BOWL_CY];
let (doc, theme_name) = match mode.as_str() {
"adaptive" | "auto" => {
let day = render_scene(&surf, &river, &Palette::new(Theme::Day));
let night = render_scene(&surf, &river, &Palette::new(Theme::Night));
(compose_adaptive(day, night), "adaptive")
}
"dark" | "night" => (
render_scene(&surf, &river, &Palette::new(Theme::Night)).finish(),
"night",
),
_ => (
render_scene(&surf, &river, &Palette::new(Theme::Day)).finish(),
"day",
),
};
std::fs::write(&out_path, &doc).expect("write SVG");
let end = river.last().copied().unwrap_or(pool_center);
eprintln!(
"wrote {out_path} ({} bytes, {theme_name}); river: {} steps from {:?} to ({:.3}, {:.3}); pool at ({:.3}, {:.3})",
doc.len(),
river.len(),
RIVER_START,
end[0],
end[1],
pool_center[0],
pool_center[1],
);
}
fn render_scene(surf: &Surface, river: &[[f64; 2]], pal: &Palette) -> Svg {
let mut svg = Svg::new();
draw_terrain(&mut svg, surf, pal, river);
draw_tree(
&mut svg,
pal,
surf.surface_point(TREE_AT[0], TREE_AT[1], 0.0),
);
svg
}
fn compose_adaptive(day: Svg, night: Svg) -> String {
let (vx, vy, side) = day.viewbox(); let mut out = String::new();
let _ = write!(
out,
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{:.2} {:.2} {:.2} {:.2}" width="{:.0}" height="{:.0}">"#,
vx, vy, side, side, side, side
);
out.push('\n');
out.push_str(
"<style>#dark-icon{display:none}\
@media (prefers-color-scheme:dark){#light-icon{display:none}#dark-icon{display:inline}}</style>\n",
);
out.push_str("<g id=\"light-icon\">\n");
out.push_str(&day.body);
out.push_str("</g>\n<g id=\"dark-icon\">\n");
out.push_str(&night.body);
out.push_str("</g>\n</svg>\n");
out
}
struct Facet {
pts: Vec<(f64, f64)>,
depth: f64,
color: Rgb,
}
fn draw_terrain(svg: &mut Svg, surf: &Surface, pal: &Palette, river: &[[f64; 2]]) {
let wl = surf.water;
let ht: Vec<Vec<f64>> = (0..=GRID)
.map(|j| {
(0..=GRID)
.map(|i| {
let (x, y) = node_xy(i, j);
surf.hn(x, y)
})
.collect()
})
.collect();
let light = normalize3(LIGHT);
let water_flat = pal.water.shade(0.70 + 0.5 * light[2].max(0.0));
let shapes = river_shapes(river);
let mut terr: Vec<Facet> = Vec::with_capacity(GRID * GRID * 2);
let mut rivr: Vec<Facet> = Vec::new();
let mut lake: Vec<Facet> = Vec::new();
for j in 0..GRID {
for i in 0..GRID {
for tri in [
[(i, j), (i + 1, j), (i, j + 1)],
[(i + 1, j), (i + 1, j + 1), (i, j + 1)],
] {
let verts: [[f64; 3]; 3] = [
[tri[0].0 as f64, tri[0].1 as f64, ht[tri[0].1][tri[0].0]],
[tri[1].0 as f64, tri[1].1 as f64, ht[tri[1].1][tri[1].0]],
[tri[2].0 as f64, tri[2].1 as f64, ht[tri[2].1][tri[2].0]],
];
let depth = verts.iter().map(|p| p[0] + p[1]).sum::<f64>() / 3.0;
let scaled = |p: [f64; 3]| [p[0], p[1], p[2] * Z_WORLD];
let mut n = normalize3(cross(
sub(scaled(verts[1]), scaled(verts[0])),
sub(scaled(verts[2]), scaled(verts[0])),
));
if n[2] < 0.0 {
n = [-n[0], -n[1], -n[2]];
}
let ndotl = (n[0] * light[0] + n[1] * light[1] + n[2] * light[2]).max(0.0);
let land = clip_h(&verts, wl, true);
if land.len() >= 3 {
let elev = land.iter().map(|p| p[2]).sum::<f64>() / land.len() as f64;
let pts = land.iter().map(|p| project(p[0], p[1], p[2])).collect();
terr.push(Facet {
pts,
depth,
color: ramp_sample(&pal.terrain, elev).shade(0.70 + 0.5 * ndotl),
});
}
let below = clip_h(&verts, wl, false);
if below.len() >= 3 {
let pts = below.iter().map(|p| project(p[0], p[1], wl)).collect();
lake.push(Facet {
pts,
depth,
color: water_flat,
});
}
let tri2 = [
[verts[0][0], verts[0][1]],
[verts[1][0], verts[1][1]],
[verts[2][0], verts[2][1]],
];
let (tx0, ty0, tx1, ty1) = tri_bbox(&tri2);
let river_color = pal.water.shade(0.70 + 0.5 * ndotl);
for (poly, bb) in &shapes {
if bb[0] > tx1 || bb[2] < tx0 || bb[1] > ty1 || bb[3] < ty0 {
continue;
}
let inter = clip_convex(&tri2, poly);
if inter.len() >= 3 {
let pts = inter
.iter()
.map(|p| project(p[0], p[1], bary_height(*p, &verts)))
.collect();
rivr.push(Facet {
pts,
depth,
color: river_color,
});
}
}
}
}
}
terr.sort_by(|a, b| a.depth.partial_cmp(&b.depth).unwrap());
rivr.sort_by(|a, b| a.depth.partial_cmp(&b.depth).unwrap());
lake.sort_by(|a, b| a.depth.partial_cmp(&b.depth).unwrap());
for f in terr.iter().chain(rivr.iter()).chain(lake.iter()) {
svg.polygon(&f.pts, f.color, Some((f.color, 1.0)));
}
}
fn clip_h(verts: &[[f64; 3]; 3], wl: f64, keep_land: bool) -> Vec<[f64; 3]> {
let inside = |z: f64| if keep_land { z >= wl } else { z <= wl };
let mut out: Vec<[f64; 3]> = Vec::with_capacity(4);
for k in 0..3 {
let cur = verts[k];
let nxt = verts[(k + 1) % 3];
if inside(cur[2]) {
out.push(cur);
}
if inside(cur[2]) != inside(nxt[2]) {
let t = (wl - cur[2]) / (nxt[2] - cur[2]);
out.push([
cur[0] + (nxt[0] - cur[0]) * t,
cur[1] + (nxt[1] - cur[1]) * t,
wl,
]);
}
}
out
}
fn node_xy(i: usize, j: usize) -> (f64, f64) {
(
X0 + (X1 - X0) * i as f64 / GRID as f64,
Y0 + (Y1 - Y0) * j as f64 / GRID as f64,
)
}
fn river_shapes(river: &[[f64; 2]]) -> Vec<(Vec<[f64; 2]>, [f64; 4])> {
let n0 = river.len();
if n0 < 2 {
return Vec::new();
}
let stride = (n0 / RIVER_SEGS).max(1);
let mut path: Vec<[f64; 2]> = river.iter().step_by(stride).copied().collect();
if path.last() != river.last() {
path.push(*river.last().unwrap());
}
let n = path.len();
if n < 2 {
return Vec::new();
}
let to_grid = |p: [f64; 2]| {
[
(p[0] - X0) / (X1 - X0) * GRID as f64,
(p[1] - Y0) / (Y1 - Y0) * GRID as f64,
]
};
let half = |i: usize| -> f64 {
let t = (i as f64 / (n - 1) as f64).powf(0.75);
0.5 * (RIVER_W_SRC + (RIVER_W_MOUTH - RIVER_W_SRC) * t)
};
let (mut left, mut right) = (Vec::with_capacity(n), Vec::with_capacity(n));
for i in 0..n {
let prev = path[i.saturating_sub(1)];
let next = path[(i + 1).min(n - 1)];
let (mut tx, mut ty) = (next[0] - prev[0], next[1] - prev[1]);
let tl = (tx * tx + ty * ty).sqrt().max(1e-9);
tx /= tl;
ty /= tl;
let (nx, ny) = (-ty, tx);
let hw = half(i);
left.push([path[i][0] + nx * hw, path[i][1] + ny * hw]);
right.push([path[i][0] - nx * hw, path[i][1] - ny * hw]);
}
let with_bbox = |poly: Vec<[f64; 2]>| -> (Vec<[f64; 2]>, [f64; 4]) {
let bb = [
poly.iter().map(|p| p[0]).fold(f64::INFINITY, f64::min),
poly.iter().map(|p| p[1]).fold(f64::INFINITY, f64::min),
poly.iter().map(|p| p[0]).fold(f64::NEG_INFINITY, f64::max),
poly.iter().map(|p| p[1]).fold(f64::NEG_INFINITY, f64::max),
];
(poly, bb)
};
let mut shapes: Vec<(Vec<[f64; 2]>, [f64; 4])> = Vec::with_capacity(n);
for i in 0..n - 1 {
shapes.push(with_bbox(vec![
to_grid(left[i]),
to_grid(left[i + 1]),
to_grid(right[i + 1]),
to_grid(right[i]),
]));
}
if SPRING_R > 0.0 {
let s = river[0];
let ring: Vec<[f64; 2]> = (0..16)
.map(|k| {
let a = std::f64::consts::TAU * k as f64 / 16.0;
to_grid([s[0] + SPRING_R * a.cos(), s[1] + SPRING_R * a.sin()])
})
.collect();
shapes.push(with_bbox(ring));
}
shapes
}
fn tri_bbox(t: &[[f64; 2]; 3]) -> (f64, f64, f64, f64) {
let xs = [t[0][0], t[1][0], t[2][0]];
let ys = [t[0][1], t[1][1], t[2][1]];
(
xs.iter().copied().fold(f64::INFINITY, f64::min),
ys.iter().copied().fold(f64::INFINITY, f64::min),
xs.iter().copied().fold(f64::NEG_INFINITY, f64::max),
ys.iter().copied().fold(f64::NEG_INFINITY, f64::max),
)
}
fn clip_convex(subject: &[[f64; 2]], clip: &[[f64; 2]]) -> Vec<[f64; 2]> {
let mut cl = clip.to_vec();
if signed_area(&cl) < 0.0 {
cl.reverse();
}
let mut out = subject.to_vec();
let m = cl.len();
for e in 0..m {
if out.is_empty() {
break;
}
let a = cl[e];
let b = cl[(e + 1) % m];
let edge = [b[0] - a[0], b[1] - a[1]];
let inside = |p: [f64; 2]| edge[0] * (p[1] - a[1]) - edge[1] * (p[0] - a[0]) >= 0.0;
let input = std::mem::take(&mut out);
let k = input.len();
for i in 0..k {
let cur = input[i];
let nxt = input[(i + 1) % k];
let (ci, ni) = (inside(cur), inside(nxt));
if ci {
out.push(cur);
}
if ci != ni {
let t = segment_line_t(cur, nxt, a, b);
out.push([
cur[0] + (nxt[0] - cur[0]) * t,
cur[1] + (nxt[1] - cur[1]) * t,
]);
}
}
}
out
}
fn signed_area(poly: &[[f64; 2]]) -> f64 {
let n = poly.len();
let mut s = 0.0;
for i in 0..n {
let a = poly[i];
let b = poly[(i + 1) % n];
s += a[0] * b[1] - b[0] * a[1];
}
0.5 * s
}
fn segment_line_t(p: [f64; 2], q: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 {
let r = [q[0] - p[0], q[1] - p[1]];
let s = [b[0] - a[0], b[1] - a[1]];
let denom = r[0] * s[1] - r[1] * s[0];
if denom.abs() < 1e-12 {
return 0.0;
}
((a[0] - p[0]) * s[1] - (a[1] - p[1]) * s[0]) / denom
}
fn bary_height(p: [f64; 2], verts: &[[f64; 3]; 3]) -> f64 {
let (a, b, c) = (verts[0], verts[1], verts[2]);
let v0 = [b[0] - a[0], b[1] - a[1]];
let v1 = [c[0] - a[0], c[1] - a[1]];
let v2 = [p[0] - a[0], p[1] - a[1]];
let d00 = v0[0] * v0[0] + v0[1] * v0[1];
let d01 = v0[0] * v1[0] + v0[1] * v1[1];
let d11 = v1[0] * v1[0] + v1[1] * v1[1];
let d20 = v2[0] * v0[0] + v2[1] * v0[1];
let d21 = v2[0] * v1[0] + v2[1] * v1[1];
let denom = d00 * d11 - d01 * d01;
if denom.abs() < 1e-12 {
return a[2];
}
let v = (d11 * d20 - d01 * d21) / denom;
let w = (d00 * d21 - d01 * d20) / denom;
(1.0 - v - w) * a[2] + v * b[2] + w * c[2]
}
fn draw_tree(svg: &mut Svg, pal: &Palette, base: (f64, f64)) {
let (bx, by) = base;
let w = 13.0;
let trunk_h = 7.0;
let tier_h = 17.0;
svg.rect(bx - 1.9, by - trunk_h, 3.8, trunk_h + 1.0, pal.trunk);
let top = by - trunk_h;
for t in 0..3 {
let level = top - t as f64 * (tier_h * 0.6);
let half = w * (1.0 - t as f64 * 0.22);
let apex = (bx, level - tier_h);
svg.polygon(
&[apex, (bx - half, level), (bx, level)],
pal.tree_dark,
None,
);
svg.polygon(&[apex, (bx, level), (bx + half, level)], pal.tree_lit, None);
}
}
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
struct Svg {
body: String,
min_x: f64,
min_y: f64,
max_x: f64,
max_y: f64,
}
impl Svg {
fn new() -> Self {
Svg {
body: String::new(),
min_x: f64::INFINITY,
min_y: f64::INFINITY,
max_x: f64::NEG_INFINITY,
max_y: f64::NEG_INFINITY,
}
}
fn track(&mut self, x: f64, y: f64) {
self.min_x = self.min_x.min(x);
self.min_y = self.min_y.min(y);
self.max_x = self.max_x.max(x);
self.max_y = self.max_y.max(y);
}
fn points(pts: &[(f64, f64)]) -> String {
let mut d = String::new();
for (k, &(x, y)) in pts.iter().enumerate() {
let _ = write!(d, "{}{:.2},{:.2}", if k == 0 { "" } else { " " }, x, y);
}
d
}
fn polygon(&mut self, pts: &[(f64, f64)], fill: Rgb, stroke: Option<(Rgb, f64)>) {
for &(x, y) in pts {
self.track(x, y);
}
let s = match stroke {
Some((c, w)) => format!(
r#" stroke="{}" stroke-width="{:.2}" stroke-linejoin="round""#,
c.hex(),
w
),
None => String::new(),
};
let _ = write!(
self.body,
r#"<polygon points="{}" fill="{}"{}/>"#,
Self::points(pts),
fill.hex(),
s
);
self.body.push('\n');
}
fn rect(&mut self, x: f64, y: f64, w: f64, h: f64, fill: Rgb) {
self.track(x, y);
self.track(x + w, y + h);
let _ = write!(
self.body,
r#"<rect x="{:.2}" y="{:.2}" width="{:.2}" height="{:.2}" fill="{}"/>"#,
x,
y,
w,
h,
fill.hex()
);
self.body.push('\n');
}
fn viewbox(&self) -> (f64, f64, f64) {
let bw = self.max_x - self.min_x;
let bh = self.max_y - self.min_y;
let side = bw.max(bh) + 2.0 * PAD;
let cx = 0.5 * (self.min_x + self.max_x);
let cy = 0.5 * (self.min_y + self.max_y);
(cx - side / 2.0, cy - side / 2.0, side)
}
fn finish(self) -> String {
let (vx, vy, side) = self.viewbox();
let mut out = String::new();
let _ = write!(
out,
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{:.2} {:.2} {:.2} {:.2}" width="{:.0}" height="{:.0}">"#,
vx, vy, side, side, side, side
);
out.push('\n');
out.push_str(&self.body);
out.push_str("</svg>\n");
out
}
}
#[derive(Clone, Copy)]
struct Rgb(u8, u8, u8);
impl Rgb {
fn lerp(self, other: Rgb, t: f64) -> Rgb {
let t = t.clamp(0.0, 1.0);
let (p, q) = (rgb_to_oklab(self), rgb_to_oklab(other));
oklab_to_rgb([
p[0] + (q[0] - p[0]) * t,
p[1] + (q[1] - p[1]) * t,
p[2] + (q[2] - p[2]) * t,
])
}
fn shade(self, k: f64) -> Rgb {
let f = |c: u8| (c as f64 * k).round().clamp(0.0, 255.0) as u8;
Rgb(f(self.0), f(self.1), f(self.2))
}
fn hex(self) -> String {
format!("#{:02x}{:02x}{:02x}", self.0, self.1, self.2)
}
}
fn ramp_sample(stops: &[Rgb], t: f64) -> Rgb {
let t = t.clamp(0.0, 1.0);
if stops.len() < 2 {
return stops[0];
}
let s = t * (stops.len() - 1) as f64;
let i = (s.floor() as usize).min(stops.len() - 2);
stops[i].lerp(stops[i + 1], s - i as f64)
}
fn srgb_to_linear(c: f64) -> f64 {
if c <= 0.04045 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
}
fn linear_to_srgb(c: f64) -> f64 {
if c <= 0.003_130_8 {
12.92 * c
} else {
1.055 * c.powf(1.0 / 2.4) - 0.055
}
}
fn rgb_to_oklab(c: Rgb) -> [f64; 3] {
let r = srgb_to_linear(c.0 as f64 / 255.0);
let g = srgb_to_linear(c.1 as f64 / 255.0);
let b = srgb_to_linear(c.2 as f64 / 255.0);
let l = (0.412_221_470_8 * r + 0.536_332_536_3 * g + 0.051_445_992_9 * b).cbrt();
let m = (0.211_903_498_2 * r + 0.680_699_545_1 * g + 0.107_396_956_6 * b).cbrt();
let s = (0.088_302_461_9 * r + 0.281_718_837_6 * g + 0.629_978_700_5 * b).cbrt();
[
0.210_454_255_3 * l + 0.793_617_785_0 * m - 0.004_072_046_8 * s,
1.977_998_495_1 * l - 2.428_592_205_0 * m + 0.450_593_709_9 * s,
0.025_904_037_1 * l + 0.782_771_766_2 * m - 0.808_675_766_0 * s,
]
}
fn oklab_to_rgb(lab: [f64; 3]) -> Rgb {
let l = (lab[0] + 0.396_337_777_4 * lab[1] + 0.215_803_757_3 * lab[2]).powi(3);
let m = (lab[0] - 0.105_561_345_8 * lab[1] - 0.063_854_172_8 * lab[2]).powi(3);
let s = (lab[0] - 0.089_484_177_5 * lab[1] - 1.291_485_548_0 * lab[2]).powi(3);
let r = 4.076_741_662_1 * l - 3.307_711_591_3 * m + 0.230_969_929_2 * s;
let g = -1.268_438_004_6 * l + 2.609_757_401_1 * m - 0.341_319_396_5 * s;
let b = -0.004_196_086_3 * l - 0.703_418_614_7 * m + 1.707_614_701_0 * s;
let q = |c: f64| (linear_to_srgb(c).clamp(0.0, 1.0) * 255.0).round() as u8;
Rgb(q(r), q(g), q(b))
}
const fn hex(s: &str) -> Rgb {
let b = s.as_bytes();
let o = b.len() - 6;
Rgb(
(nibble(b[o]) << 4) | nibble(b[o + 1]),
(nibble(b[o + 2]) << 4) | nibble(b[o + 3]),
(nibble(b[o + 4]) << 4) | nibble(b[o + 5]),
)
}
const fn nibble(c: u8) -> u8 {
match c {
b'0'..=b'9' => c - b'0',
b'a'..=b'f' => c - b'a' + 10,
b'A'..=b'F' => c - b'A' + 10,
_ => 0,
}
}