facett-core/src/render/decor.rs
//! **The "cyber-toon" decoration kernel** — domain-agnostic, pure-CPU L0 beauty
//! helpers that turn a flat instance graph into a luminous, animated dashboard
//! **with zero GPU / vello dependency**, so they light up identically native AND
//! in the wasm demo. The graph/map skins (and the population showcase) compose
//! these over the [`prim`](super::prim) instances they already lower.
//!
//! Everything here is:
//! - **pure** — a function of its inputs (no globals, no `Instant::now()`); all
//! animation is driven by an injected `phase`/`t` so snapshots are stable (the
//! determinism LAW). A test feeds a fixed clock and the picture is reproducible.
//! - **L0-only** — emits [`QuadInstance`]/[`LineInstance`]/[`CircleInstance`]/
//! [`RingInstance`], the same vocabulary the CPU raster + GPU pipeline both draw,
//! so the effects work on the always-available CPU lane (`l1-vello` is an
//! *elevation*, never a requirement).
//! - **domain-agnostic** — it knows nothing about "Person" or "Debt"; a caller
//! decides which nodes glow, which edges flow, where a shockwave fires.
//!
//! ## The helper menu (maps onto the aesthetic spec)
//! - [`glow_halo`] — a soft colored light-bleed ring behind a node (the glossy
//! orb look). The CPU bloom substitute for an L1 gaussian glow.
//! - [`ao_shadow`] — a toon drop-shadow / ambient-occlusion disc under a node
//! (deeper where nodes bunch — clustered nodes pool a darker shadow).
//! - [`dash_line`] — an **animated dash-array** stroke (a `→` flow of segments that
//! marches along the edge as `phase` advances) — debt flowing toward enforcement.
//! - [`particles_on_edge`] — light-tracer **particles** zipping along an edge
//! (money moving Loan→Person), positions a pure function of `phase`.
//! - [`pulse`] / [`strobe`] — deterministic 0..1 envelopes for the Debt **pulse**
//! and PoliceRecord **strobe** (animation timing, injected-clock driven).
//! - [`shockwave_ring`] — a radial ring expanding from a clicked node.
//! - [`bloom_composite`] — a cheap separable box-blur **bloom** post-pass over a
//! straight-RGBA8 [`Frame`]: bright pixels bleed light into their neighbours so
//! intersecting neon lines mix into colored "explosions".
//! - [`lerp_rgba`] / [`gradient_by_scalar`] — a 2-stop (or multi-stop) colour ramp
//! keyed by a scalar in `[0,1]` (the gradient-fill-by-scalar engine primitive).
use super::Frame;
use super::prim::{CircleInstance, LineInstance, QuadInstance, RingInstance, shape};
/// τ (one full animation cycle) — phases are in **cycles** (`0.0..1.0` is one loop),
/// so a caller advances `phase += dt / period` and the look is period-independent.
const TAU: f32 = std::f32::consts::TAU;
// ───────────────────────────────────────────────────────────────────────────
// Colour ramps (gradient-by-scalar engine primitive)
// ───────────────────────────────────────────────────────────────────────────
/// Linear-interpolate two straight RGBA colours at `t ∈ [0,1]`.
#[inline]
pub fn lerp_rgba(a: [f32; 4], b: [f32; 4], t: f32) -> [f32; 4] {
let t = t.clamp(0.0, 1.0);
[
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t,
a[3] + (b[3] - a[3]) * t,
]
}
/// Sample a piecewise-linear **multi-stop** gradient (`stops` sorted by position)
/// at `t ∈ [0,1]` — the domain-agnostic "fill-by-scalar" ramp (debt-heat, degree,
/// population). Clamps below the first / above the last stop. `stops` must be
/// non-empty.
pub fn gradient_by_scalar(stops: &[(f32, [f32; 4])], t: f32) -> [f32; 4] {
debug_assert!(!stops.is_empty(), "gradient needs at least one stop");
let t = t.clamp(0.0, 1.0);
if t <= stops[0].0 {
return stops[0].1;
}
for w in stops.windows(2) {
let (p0, c0) = w[0];
let (p1, c1) = w[1];
if t <= p1 {
let span = (p1 - p0).max(1e-6);
return lerp_rgba(c0, c1, (t - p0) / span);
}
}
stops[stops.len() - 1].1
}
// ───────────────────────────────────────────────────────────────────────────
// Animation envelopes (injected-clock driven, pure)
// ───────────────────────────────────────────────────────────────────────────
/// A smooth `0..1` **pulse** envelope (a raised cosine) at `phase` cycles —
/// `0 → 1 → 0` once per cycle. The Debt/KfmCase "intense glow" breathing.
#[inline]
pub fn pulse(phase: f32) -> f32 {
0.5 - 0.5 * (phase * TAU).cos()
}
/// A hard **strobe** envelope at `phase` cycles with duty `duty ∈ (0,1)` — `1.0`
/// for the first `duty` of each cycle, else `0.0` (PoliceRecord hazard flash). A
/// pure step so snapshots at a fixed `phase` are exact.
#[inline]
pub fn strobe(phase: f32, duty: f32) -> f32 {
if phase.rem_euclid(1.0) < duty.clamp(0.0, 1.0) { 1.0 } else { 0.0 }
}
// ───────────────────────────────────────────────────────────────────────────
// Node beauty: glow halo + ambient-occlusion drop-shadow
// ───────────────────────────────────────────────────────────────────────────
/// A soft colored **glow halo** ring behind a node marker — the CPU light-bleed
/// that makes a node read as a glossy orb. `r` is the marker radius, `intensity`
/// the halo alpha (`0..1`), `spread` the outer-radius multiplier (≥ 1).
#[inline]
pub fn glow_halo(center: [f32; 2], r: f32, color: [f32; 3], intensity: f32, spread: f32) -> RingInstance {
let spread = spread.max(1.05);
RingInstance {
center,
radius: r * spread,
inner: r * 1.05,
color: [color[0], color[1], color[2], intensity.clamp(0.0, 1.0)],
aa: r * 0.5 + 1.5, // a wide AA band = a soft, blurry bleed (no GPU blur needed)
}
}
/// A toon **ambient-occlusion drop-shadow** disc under a node — a dark, soft circle
/// offset down-right, so clustered nodes pool a deeper shadow (the "ray-traced"
/// grounding). `r` the marker radius, `depth` the shadow alpha (`0..1`).
#[inline]
pub fn ao_shadow(center: [f32; 2], r: f32, depth: f32) -> CircleInstance {
CircleInstance {
center: [center[0] + r * 0.22, center[1] + r * 0.30],
radius: r * 1.18,
color: [0.0, 0.0, 0.0, depth.clamp(0.0, 1.0)],
aa: r * 0.6 + 1.5,
}
}
/// Build glow + AO for a batch of node markers in one pass (the common case): pull
/// the SQUARE/CIRCLE/DIAMOND node quads out of `quads`, and for each emit an AO
/// shadow (under) + a glow halo (also under, over the shadow). Returns
/// `(ao_quads, glow_quads)` already lowered, so a caller pushes
/// `shadows → glows → original quads` for the layered orb look. `intensity` scales
/// the glow alpha; `ao` the shadow alpha.
pub fn node_decor(
quads: &[QuadInstance],
intensity: f32,
ao: f32,
) -> (Vec<QuadInstance>, Vec<QuadInstance>) {
let mut shadows = Vec::with_capacity(quads.len());
let mut glows = Vec::with_capacity(quads.len());
for q in quads {
if q.shape == shape::SQUARE || q.shape == shape::CIRCLE || q.shape == shape::DIAMOND {
let r = q.radius;
shadows.push(ao_shadow(q.center, r, ao).lower());
glows.push(glow_halo(q.center, r, [q.color[0], q.color[1], q.color[2]], intensity, 2.1).lower());
}
}
(shadows, glows)
}
// ───────────────────────────────────────────────────────────────────────────
// Edge beauty: animated dash-array + particle tracers
// ───────────────────────────────────────────────────────────────────────────
/// An **animated dash-array** line `a → b`: the segment is chopped into `dash`-long
/// lit pieces separated by `gap`, and the whole pattern **marches** along the
/// direction by `phase` (in dash+gap units) — a directional "flow". Pure in
/// `phase`, so a fixed clock gives a fixed picture. `half_width`/`aa`/`color` as
/// for a [`LineInstance`].
#[allow(clippy::too_many_arguments)] // public signature hosts call: kept as is
pub fn dash_line(
a: [f32; 2],
b: [f32; 2],
half_width: f32,
aa: f32,
color: [f32; 4],
dash: f32,
gap: f32,
phase: f32,
) -> Vec<LineInstance> {
let dash = dash.max(0.5);
let gap = gap.max(0.0);
let period = dash + gap;
let dx = b[0] - a[0];
let dy = b[1] - a[1];
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-3 {
return Vec::new();
}
let (ux, uy) = (dx / len, dy / len);
// March offset (always backward against the flow so dashes appear to move a→b).
let mut t = -(phase.rem_euclid(1.0) * period);
let mut out = Vec::new();
while t < len {
let s = t.max(0.0);
let e = (t + dash).min(len);
if e > s {
out.push(LineInstance::round(
[a[0] + ux * s, a[1] + uy * s],
[a[0] + ux * e, a[1] + uy * e],
half_width,
aa,
color,
));
}
t += period;
}
out
}
/// **Light-tracer particles** zipping along an edge `a → b`: `n` evenly-spaced
/// discs whose fractional position is `(i/n + phase) mod 1`, so they flow a→b as
/// `phase` advances. Pure in `phase`. `radius`/`color` per particle.
pub fn particles_on_edge(
a: [f32; 2],
b: [f32; 2],
n: usize,
radius: f32,
color: [f32; 4],
phase: f32,
) -> Vec<CircleInstance> {
if n == 0 {
return Vec::new();
}
(0..n)
.map(|i| {
let f = ((i as f32 / n as f32) + phase).rem_euclid(1.0);
// Brighten toward the head of the trail (a fading comet look).
let head = 0.55 + 0.45 * f; // dim at the tail, bright at the front
let c = [color[0], color[1], color[2], (color[3] * head).clamp(0.0, 1.0)];
CircleInstance {
center: [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f],
radius: radius * (0.6 + 0.6 * f),
color: c,
aa: 1.0,
}
})
.collect()
}
// ───────────────────────────────────────────────────────────────────────────
// Interaction: shockwave
// ───────────────────────────────────────────────────────────────────────────
/// A radial **shockwave** ring expanding from `center` as `t ∈ [0,1]` runs
/// (0 = just fired, 1 = faded out): the ring grows `r0 → r0 + reach` while its
/// alpha fades `1 → 0`. Pure in `t`. The neon-dashboard "click ripple".
#[inline]
pub fn shockwave_ring(center: [f32; 2], r0: f32, reach: f32, color: [f32; 3], t: f32) -> RingInstance {
let t = t.clamp(0.0, 1.0);
let radius = r0 + reach * t;
let thickness = (reach * 0.10).max(2.0);
RingInstance {
center,
radius,
inner: (radius - thickness).max(0.0),
color: [color[0], color[1], color[2], (1.0 - t).powf(1.4)],
aa: 2.0,
}
}
// ───────────────────────────────────────────────────────────────────────────
// Bloom (CPU post-process)
// ───────────────────────────────────────────────────────────────────────────
/// A cheap separable **bloom** post-pass over a straight-RGBA8 [`Frame`]: extract
/// the pixels brighter than `threshold` (`0..1` luma), blur them with a small
/// box-blur of `radius` px (separable, two passes), and **add** that light back
/// onto the frame scaled by `intensity`. Intersecting neon lines therefore bleed
/// and mix into colored "explosions" — the spec's bloom, with no GPU.
///
/// Pure, in-place, deterministic. `radius` is clamped to a small range so a wasm
/// frame stays snappy. A no-op when `intensity <= 0` or `radius == 0`.
pub fn bloom_composite(frame: &mut Frame, threshold: f32, radius: u32, intensity: f32) {
if intensity <= 0.0 || radius == 0 || frame.width == 0 || frame.height == 0 {
return;
}
let (w, h) = (frame.width as usize, frame.height as usize);
let radius = radius.min(8) as i32; // keep the blur cheap (wasm-safe)
let n = w * h;
// 1) Bright-pass: keep only pixels whose luma exceeds the threshold.
let mut bright = vec![0f32; n * 3];
for (i, px) in frame.rgba.as_chunks::<4>().0.iter().enumerate() {
let r = px[0] as f32 / 255.0;
let g = px[1] as f32 / 255.0;
let b = px[2] as f32 / 255.0;
let luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
if luma > threshold {
// soft knee so the bloom ramps in rather than hard-clips
let k = ((luma - threshold) / (1.0 - threshold).max(1e-3)).clamp(0.0, 1.0);
bright[i * 3] = r * k;
bright[i * 3 + 1] = g * k;
bright[i * 3 + 2] = b * k;
}
}
// 2) Separable box-blur (horizontal then vertical) of the bright layer.
let win = (2 * radius + 1) as f32;
let mut tmp = vec![0f32; n * 3];
// horizontal
for y in 0..h {
for x in 0..w {
let mut acc = [0f32; 3];
for d in -radius..=radius {
let sx = (x as i32 + d).clamp(0, w as i32 - 1) as usize;
let si = (y * w + sx) * 3;
acc[0] += bright[si];
acc[1] += bright[si + 1];
acc[2] += bright[si + 2];
}
let di = (y * w + x) * 3;
tmp[di] = acc[0] / win;
tmp[di + 1] = acc[1] / win;
tmp[di + 2] = acc[2] / win;
}
}
// vertical (back into `bright`)
for y in 0..h {
for x in 0..w {
let mut acc = [0f32; 3];
for d in -radius..=radius {
let sy = (y as i32 + d).clamp(0, h as i32 - 1) as usize;
let si = (sy * w + x) * 3;
acc[0] += tmp[si];
acc[1] += tmp[si + 1];
acc[2] += tmp[si + 2];
}
let di = (y * w + x) * 3;
bright[di] = acc[0] / win;
bright[di + 1] = acc[1] / win;
bright[di + 2] = acc[2] / win;
}
}
// 3) Additive composite the blurred light back onto the frame.
for (i, px) in frame.rgba.as_chunks_mut::<4>().0.iter_mut().enumerate() {
for k in 0..3 {
let base = px[k] as f32 / 255.0;
let add = bright[i * 3 + k] * intensity;
px[k] = ((base + add) * 255.0).round().clamp(0.0, 255.0) as u8;
}
// bloom never makes a transparent pixel opaque on its own, but it lifts
// alpha where it added real light so the glow is visible over the ground.
if px[3] == 0 {
let lit = bright[i * 3] + bright[i * 3 + 1] + bright[i * 3 + 2];
if lit * intensity > 0.02 {
px[3] = (lit * intensity * 255.0).round().clamp(0.0, 255.0) as u8;
}
}
}
}
// ───────────────────────────────────────────────────────────────────────────
// The GLOW STROKE ladder — an EverQuest-II-style luminous line, on any renderer
// ───────────────────────────────────────────────────────────────────────────
/// **One pass of a layered glow stroke.** A glowing line is not one stroke, it is a
/// LADDER of them drawn widest-first over the same path: a wide, nearly transparent
/// bleed, a mid halo, and a thin core that is blended toward white so the line reads
/// as *hot* rather than merely coloured.
///
/// Every field is a MULTIPLIER or a fraction, never a pixel count, so the same ladder
/// serves a 1.5 px graph edge and a 7 m map ribbon.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GlowPass {
/// Stroke width as a multiple of the caller's core width.
pub width_mul: f32,
/// Alpha of this pass, `0..1`.
pub alpha: f32,
/// How far the ink is blended toward white, `0..1` — `0` keeps the hue, `1` is
/// pure white. Only the innermost passes carry any; that is the whole "hot core".
pub white: f32,
}
/// **The ladder** — five passes, widest and faintest first, ending on a near-white
/// core. Read once, projected by every renderer (LAW: one writer).
///
/// # Why five and why these numbers
///
/// The three-pass ladder korp shipped before this — `(9.0, α46) (4.5, α120)
/// (1.6, α255)` at the hue's own colour — did not read as a glow, and the reasons are
/// measurable, not aesthetic:
///
/// * **The widest pass was too narrow.** A halo has to be several times the core to
/// read as light leaving the line; at 5.6× core with α 0.18 it read as a second,
/// thicker line. Here the outermost pass is 14× the core at α 0.09.
/// * **The core kept the hue.** A saturated cyan at α 1.0 is a cyan line. Light is
/// white at the centre and coloured at the edge — that is what a bloom does, and
/// what an eye reads as emission. The inner two passes carry `white` 0.55 / 0.9.
/// * **The alpha budget was linear.** Stacked translucent strokes composite
/// multiplicatively, so a linear ramp piles most of the ink in the middle. The
/// ladder is geometric: 0.09 → 0.16 → 0.30 → 0.62 → 1.0.
pub const GLOW_LADDER: [GlowPass; 5] = [
GlowPass { width_mul: 14.0, alpha: 0.09, white: 0.0 },
GlowPass { width_mul: 8.0, alpha: 0.16, white: 0.0 },
GlowPass { width_mul: 4.2, alpha: 0.30, white: 0.10 },
GlowPass { width_mul: 2.0, alpha: 0.62, white: 0.55 },
GlowPass { width_mul: 1.0, alpha: 1.0, white: 0.90 },
];
/// **The ladder lowered to concrete `(width, rgba)` strokes** for one core width and
/// one ink. Widest/faintest first — a caller draws them IN ORDER over the same path.
///
/// `ink` is straight (non-premultiplied) linear RGB in `0..1`. The returned alpha is
/// `pass.alpha`, so a caller that also carries an opacity multiplies it in.
///
/// Pure and deterministic: same inputs, same ladder, on CPU and GPU alike.
#[must_use]
pub fn glow_stroke_passes(core_width: f32, ink: [f32; 3]) -> Vec<(f32, [f32; 4])> {
glow_stroke_passes_on(core_width, ink, GlowGround::Dark)
}
/// **What the glow is drawn ON.** A luminous line is a contrast effect, and the two
/// grounds need opposite cores — so this is a required input, not a style option.
///
/// MEASURED 2026-09-03 (korp's `tests/golden_trail_pixels.rs`, the same ladder on both
/// papers): over the dark pane the white-hot core reads as emission and the frame is a
/// neon filament. Over the cartographic DAY paper (`#F2EFE9`, which is the pane the owner
/// was actually looking at) the identical ladder composites to `(232, 250, 253)` — a
/// near-white line on near-white paper, i.e. **invisible** — and the low-alpha halo
/// desaturates to grey. The picture was a grey smudge with a hole down the middle.
///
/// Over light paper a glowing line is the *highlighter* look instead: the core is the
/// hue at full saturation and DARKENED, the halo is the hue at higher alpha than the
/// dark-ground ladder needs. Same widths, same reach, same ordering — only the tone
/// flips.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GlowGround {
/// A dark pane: the core burns toward white.
Dark,
/// Light paper: the core deepens toward the saturated hue.
Light,
}
/// **How far EVERY light-ground pass is pushed toward black** — the halo included.
///
/// A low-alpha saturated hue over cream composites to a pale wash, and a pale wash on
/// pale paper is grey: the first light-ground attempt (halo at the pure hue, core
/// deepened) drew a grey smudge with a blue thread in it. Deepening the whole ladder
/// turns the halo into a coloured **contact shadow** — which is what makes a bright line
/// read as luminous against a bright ground, in EQ2 and on paper alike.
const GLOW_LIGHT_DEEPEN_BASE: f32 = 0.30;
/// …and how much FURTHER the core is deepened, on top of the base — so the ladder still
/// has a hot centre, just a saturated one instead of a white one.
const GLOW_LIGHT_DEEPEN_CORE: f32 = 0.30;
/// Alpha multiplier for the light-ground ladder — a wash that reads on a dark pane is
/// nothing at all on cream.
const GLOW_LIGHT_ALPHA_GAIN: f32 = 2.2;
/// **The ladder lowered to concrete `(width, rgba)` strokes** for one core width, one
/// ink and the ground it is drawn on. Widest/faintest first — a caller draws them IN
/// ORDER over the same path.
///
/// `ink` is straight (non-premultiplied) RGB in `0..1`. The returned alpha is the
/// pass's, so a caller carrying its own opacity multiplies it in.
///
/// Pure and deterministic: same inputs, same ladder, on CPU and GPU alike.
#[must_use]
pub fn glow_stroke_passes_on(
core_width: f32,
ink: [f32; 3],
ground: GlowGround,
) -> Vec<(f32, [f32; 4])> {
let core = core_width.max(0.1);
GLOW_LADDER
.iter()
.map(|p| {
let t = p.white.clamp(0.0, 1.0);
let (rgb, alpha) = match ground {
// Toward WHITE by `white`: a hot centre inside a coloured bleed.
GlowGround::Dark => (
[
ink[0] + (1.0 - ink[0]) * t,
ink[1] + (1.0 - ink[1]) * t,
ink[2] + (1.0 - ink[2]) * t,
],
p.alpha,
),
// Toward BLACK by `white · GLOW_LIGHT_DEEPEN`, and every pass carries
// more ink: the same shape, inverted for paper.
GlowGround::Light => {
let k = 1.0 - GLOW_LIGHT_DEEPEN_BASE - t * GLOW_LIGHT_DEEPEN_CORE;
(
[ink[0] * k, ink[1] * k, ink[2] * k],
p.alpha * GLOW_LIGHT_ALPHA_GAIN,
)
}
};
(core * p.width_mul, [rgb[0], rgb[1], rgb[2], alpha.clamp(0.0, 1.0)])
})
.collect()
}
/// **The glow as a CROSS-SECTION — one ribbon, not a stack of strokes.**
///
/// Returns `(half-offset px from the centreline, straight rgba)` from the OUTER edge
/// inward to the centre, for a caller that builds a per-vertex-coloured mesh.
///
/// # Why this exists (owner, 2026-09-03)
///
/// *"the golden path is looking like shit, its not a soft spline its a flat band with
/// some smoth edges. the eqII paths where like a soul / spirit trace shiny and
/// transparent glowing"*
///
/// [`glow_stroke_passes_on`] hands back five concentric STROKES, and every renderer
/// drew them source-over on top of each other. The arithmetic is what he was looking
/// at: composite alpha at the centreline is `1 − Π(1 − αᵢ)`, and the ladder's core pass
/// is `alpha 1.0`, so the middle of the trail is **fully opaque** and only the outermost
/// halo is soft. That is a flat band with smooth edges — paint, not light. Nothing
/// shows through it, which is the opposite of a spirit trace.
///
/// The numbers were never wrong; the way they were APPLIED was. Read the same ladder as
/// a profile across the width and interpolate between the rings in ONE mesh and you get
/// a true falloff: bright at the centreline, zero at the edge, the map visible through
/// the whole thing, and no plateau anywhere because nothing is drawn twice.
///
/// So this is the same `GLOW_LADDER` — the one writer of what a glow is — projected on
/// a different axis. A width multiple becomes an OFFSET and an alpha becomes a VERTEX
/// alpha. The outermost ring is forced to `alpha 0` so the ribbon fades into the ground
/// instead of ending on an edge, which is the other half of "smooth edges": an
/// antialiased boundary is still a boundary.
#[must_use]
pub fn glow_cross_section(core_width: f32, ink: [f32; 3], ground: GlowGround) -> Vec<(f32, [f32; 4])> {
let passes = glow_stroke_passes_on(core_width, ink, ground);
let mut out: Vec<(f32, [f32; 4])> = Vec::with_capacity(passes.len() + 1);
// The widest pass defines the ribbon's edge, and the edge is TRANSPARENT.
if let Some((w, c)) = passes.first() {
out.push((w * 0.5, [c[0], c[1], c[2], 0.0]));
}
// ── The ACCUMULATION, which is the whole difference between one ribbon and five
// stacked strokes ──────────────────────────────────────────────────────────
//
// The ladder used to be painted as five separate translucent strokes on top of one
// another, so at any distance from the centre the eye saw every pass at least that
// wide, composited. The core was bright because five alphas had accumulated there,
// not because the innermost pass was strong.
//
// A ribbon is ONE draw. Handing each ring its own pass' alpha therefore throws the
// accumulation away, and the trail comes out far too faint to read — MEASURED by
// `golden_trail_pixels::the_glow_ladder_beats_the_three_pass_line`, which caught the
// first version of this function at a core departing the paper by **56** where the
// stacked ladder reached **204**. That is a trail you cannot see on a map, which is
// the same complaint the ribbon was built to answer, arrived at from the other side.
//
// So each ring carries the OVER-composite of every pass from the outermost down to
// itself, and the centreline carries all of them — the same arithmetic
// `glow_core_coverage` reports as one number, done per ring and in colour.
let mut acc: [f32; 4] = [0.0, 0.0, 0.0, 0.0];
for (w, c) in &passes {
acc = over(*c, acc);
out.push((w * 0.5, acc));
}
// …and the centreline itself, at the fully accumulated colour: the innermost pass is
// a stroke of finite width, so without this the profile would have a flat top exactly
// as wide as the core. A ribbon wants a POINT of maximum, not a plateau — that
// plateau is the defect this function exists to remove, in miniature.
out.push((0.0, acc));
out
}
/// `src` over `dst`, both premultiplied-by-nothing straight RGBA — the Porter-Duff
/// operator the stacked ladder performed implicitly by drawing one pass after another.
/// Returns straight RGBA again so the mesh can hand it to a vertex.
fn over(src: [f32; 4], dst: [f32; 4]) -> [f32; 4] {
let (sa, da) = (src[3].clamp(0.0, 1.0), dst[3].clamp(0.0, 1.0));
let out_a = sa + da * (1.0 - sa);
if out_a <= f32::EPSILON {
return [src[0], src[1], src[2], 0.0];
}
let mix = |s: f32, d: f32| (s * sa + d * da * (1.0 - sa)) / out_a;
[
mix(src[0], dst[0]),
mix(src[1], dst[1]),
mix(src[2], dst[2]),
out_a,
]
}
/// **The ink coverage a whole ladder lays down at the line's centre**, `0..1` — the
/// over-operator composite of every pass' alpha. The single number "how bright is the
/// core", so a change to [`GLOW_LADDER`] that quietly dims the line is visible as a
/// number and not only as a picture.
#[must_use]
pub fn glow_core_coverage() -> f32 {
GLOW_LADDER.iter().fold(0.0, |acc, p| acc + p.alpha.clamp(0.0, 1.0) * (1.0 - acc))
}
/// **How wide the glow reaches**, as a multiple of the core width — the outermost
/// pass. The halo's read is this number; a ladder whose widest pass is ~2× core is a
/// thick line, not a glow.
#[must_use]
pub fn glow_reach_mul() -> f32 {
GLOW_LADDER.iter().fold(0.0f32, |m, p| m.max(p.width_mul))
}
/// **The flow phase along a path**, `0..1` — where the light-tracer particles sit at
/// clock `t` seconds for a stroke that cycles every `period_s`. Pure in `t` (the
/// determinism LAW); pair it with [`particles_on_edge`] to make a glow stroke *move*.
#[must_use]
pub fn glow_flow_phase(t: f32, period_s: f32) -> f32 {
(t / period_s.max(1e-3)).rem_euclid(1.0)
}
#[cfg(test)]
mod tests {
use super::*;
/// INJECT-ASSERT: the 2-stop / multi-stop ramp lands on the right colours at
/// the ends and blends in the middle (the gradient-by-scalar primitive).
#[test]
fn gradient_by_scalar_ramps_and_clamps() {
let stops = [(0.0, [0.0, 0.0, 1.0, 1.0]), (1.0, [1.0, 0.0, 0.0, 1.0])];
assert_eq!(gradient_by_scalar(&stops, -1.0), stops[0].1, "clamps below");
assert_eq!(gradient_by_scalar(&stops, 2.0), stops[1].1, "clamps above");
let mid = gradient_by_scalar(&stops, 0.5);
assert!((mid[0] - 0.5).abs() < 1e-5 && (mid[2] - 0.5).abs() < 1e-5, "mid blends: {mid:?}");
// A three-stop ramp picks the right segment.
let three = [(0.0, [0.0; 4]), (0.5, [1.0, 1.0, 1.0, 1.0]), (1.0, [0.0; 4])];
assert!(gradient_by_scalar(&three, 0.25)[0] > 0.4, "rises to the mid stop");
assert!(gradient_by_scalar(&three, 0.75)[0] < 0.6, "falls past the mid stop");
}
/// INJECT-ASSERT: pulse/strobe are deterministic envelopes — the SAME phase
/// gives the SAME value (snapshot-stable), and they actually move.
#[test]
fn pulse_and_strobe_are_deterministic_envelopes() {
assert!(pulse(0.0).abs() < 1e-5, "pulse troughs at phase 0");
assert!((pulse(0.5) - 1.0).abs() < 1e-5, "pulse peaks at half-cycle");
assert_eq!(pulse(0.25), pulse(0.25), "deterministic");
assert_eq!(strobe(0.05, 0.5), 1.0, "strobe lit in the duty window");
assert_eq!(strobe(0.75, 0.5), 0.0, "strobe dark past the duty window");
assert_eq!(strobe(1.05, 0.5), 1.0, "strobe is periodic (phase wraps)");
}
/// INJECT-ASSERT: the animated dash marches — at a different phase the lit
/// segments sit at different offsets (a real flow, not a static dash).
#[test]
fn dash_line_marches_with_phase() {
let a = [0.0, 0.0];
let b = [100.0, 0.0];
let d0 = dash_line(a, b, 2.0, 1.0, [1.0; 4], 8.0, 6.0, 0.0);
let d1 = dash_line(a, b, 2.0, 1.0, [1.0; 4], 8.0, 6.0, 0.5);
assert!(!d0.is_empty() && !d1.is_empty(), "the dash produced segments");
// The pattern marches: the first lit segment ENDS at a different offset
// (its start may clamp to 0 at both phases, but the lit length shifts).
assert_ne!(d0[0].b[0], d1[0].b[0], "dashes moved along the edge with phase");
// Determinism: same phase, same geometry.
let d0b = dash_line(a, b, 2.0, 1.0, [1.0; 4], 8.0, 6.0, 0.0);
assert_eq!(d0[0].a, d0b[0].a, "same phase → same dash (start)");
assert_eq!(d0[0].b, d0b[0].b, "same phase → same dash (end)");
}
/// INJECT-ASSERT: particles ride the edge — at phase 0 they sit at i/n, and a
/// phase bump slides every particle along the segment by the same fraction.
#[test]
fn particles_flow_along_the_edge() {
let a = [0.0, 0.0];
let b = [200.0, 0.0];
let p0 = particles_on_edge(a, b, 4, 3.0, [1.0; 4], 0.0);
let p1 = particles_on_edge(a, b, 4, 3.0, [1.0; 4], 0.25);
assert_eq!(p0.len(), 4);
// The first particle starts at the tail and advances a quarter-edge.
assert!((p0[0].center[0] - 0.0).abs() < 1e-3, "particle 0 at the tail at phase 0");
assert!((p1[0].center[0] - 50.0).abs() < 1e-3, "particle 0 advanced to x=50 at phase 0.25");
}
/// INJECT-ASSERT: the shockwave grows + fades with t (a real ripple), staying
/// a valid annulus.
#[test]
fn shockwave_expands_and_fades() {
let early = shockwave_ring([50.0, 50.0], 10.0, 100.0, [1.0, 0.4, 0.9], 0.1);
let late = shockwave_ring([50.0, 50.0], 10.0, 100.0, [1.0, 0.4, 0.9], 0.9);
assert!(late.radius > early.radius, "the ring expands with t");
assert!(late.color[3] < early.color[3], "and fades out");
assert!(early.inner < early.radius, "valid annulus");
}
/// INJECT-ASSERT: node_decor emits a shadow + a glow under each node marker
/// (one of each per drawable quad), AND the glow carries the node's colour.
#[test]
fn node_decor_emits_shadow_and_glow_per_node() {
let q = QuadInstance {
center: [40.0, 40.0],
radius: 8.0,
inner: 2.0,
color: [0.2, 0.8, 1.0, 1.0],
aa: 1.0,
shape: shape::SQUARE,
_pad: [0.0, 0.0],
};
let (shadows, glows) = node_decor(&[q], 0.5, 0.4);
assert_eq!(shadows.len(), 1, "one AO shadow under the node");
assert_eq!(glows.len(), 1, "one glow halo behind the node");
// The glow is the node's hue at the requested intensity; the shadow is dark.
assert!((glows[0].color[2] - 1.0).abs() < 1e-5, "glow carries the node blue");
assert!((glows[0].color[3] - 0.5).abs() < 1e-5, "glow at the requested intensity");
assert!(shadows[0].color[0] < 0.05, "shadow is black");
assert!(glows[0].radius > q.radius, "glow is larger than the marker (a bleed)");
}
/// INJECT-ASSERT: bloom actually spreads light — a single bright pixel on a
/// dark frame lights up its neighbours after the pass (and the source stays
/// lit), proving the box-blur bleed ran (not "didn't panic").
#[test]
fn bloom_spreads_light_to_neighbours() {
let (w, h) = (9u32, 9u32);
let mut rgba = vec![0u8; (w * h * 4) as usize];
// One opaque white pixel dead centre on a transparent black ground.
let c = ((4 * w + 4) * 4) as usize;
rgba[c..c + 4].copy_from_slice(&[255, 255, 255, 255]);
let mut frame = Frame { width: w, height: h, rgba };
let neighbour = ((4 * w + 5) * 4) as usize; // one px to the right
assert_eq!(frame.rgba[neighbour], 0, "neighbour starts dark");
bloom_composite(&mut frame, 0.5, 2, 1.0);
assert!(frame.rgba[neighbour] > 0, "bloom bled light onto the neighbour");
assert!(frame.rgba[neighbour + 3] > 0, "and lifted its alpha so the glow shows");
// The original bright pixel stays lit.
assert!(frame.rgba[c] > 200, "the source pixel stays bright");
}
/// INJECT-ASSERT: bloom is a no-op when disabled (intensity 0 / radius 0) — the
/// frame is byte-identical, so toggling bloom off truly removes it.
#[test]
fn bloom_disabled_is_a_noop() {
let (w, h) = (5u32, 5u32);
let mut rgba = vec![0u8; (w * h * 4) as usize];
rgba[((2 * w + 2) * 4) as usize..((2 * w + 2) * 4) as usize + 4]
.copy_from_slice(&[255, 255, 255, 255]);
let before = rgba.clone();
let mut frame = Frame { width: w, height: h, rgba };
bloom_composite(&mut frame, 0.5, 0, 1.0);
assert_eq!(frame.rgba, before, "radius 0 → no-op");
bloom_composite(&mut frame, 0.5, 2, 0.0);
assert_eq!(frame.rgba, before, "intensity 0 → no-op");
}
// ── the GLOW STROKE ladder ────────────────────────────────────────────
/// The ladder that shipped in korp before this lane — the one whose pixels
/// The owner said were "not glowing souline like in everquest-II yet". Widths in
/// px over a 1.6 px core, alphas 0..1. Used ONLY as this module's red control.
fn the_old_three_pass_ladder() -> Vec<(f32, [f32; 4])> {
let ink = [0.0, 0.95, 1.0];
[(9.0f32, 46.0f32), (4.5, 120.0), (1.6, 255.0)]
.iter()
.map(|&(w, a)| (w, [ink[0], ink[1], ink[2], a / 255.0]))
.collect()
}
/// **RED FIRST — the old ladder fails every gate the new one passes.** Reach,
/// hot core and coverage are each measured on the LOWERED strokes, so a ladder
/// edit that quietly dims the line fails here rather than in a screenshot.
#[test]
fn the_old_three_pass_ladder_is_not_a_glow() {
let old = the_old_three_pass_ladder();
let core_w = 1.6f32;
let reach = old.iter().fold(0.0f32, |m, &(w, _)| m.max(w)) / core_w;
assert!(reach < 6.0, "the old halo reached only {reach:.1}x the core — a thick line");
// Its core keeps the hue: the red channel never lifts off the ink's 0.0.
let core = old.last().expect("three passes").1;
assert!(core[0] < 0.2, "the old core is saturated cyan (r={}), not hot white", core[0]);
// And its coverage is dominated by one opaque pass rather than a ramp.
let alphas: Vec<f32> = old.iter().map(|p| p.1[3]).collect();
assert!(alphas[0] > 0.15, "the old outer pass was too opaque to read as bleed: {}", alphas[0]);
}
/// **The new ladder IS a glow** — wide bleed, geometric alpha ramp, white-hot core.
#[test]
fn the_glow_ladder_reaches_wide_and_burns_white_at_the_core() {
let ink = [0.0, 0.95, 1.0];
let passes = glow_stroke_passes(1.6, ink);
assert_eq!(passes.len(), GLOW_LADDER.len());
// 1. WIDTH: widest first, and the halo reaches far past the core.
let widths: Vec<f32> = passes.iter().map(|p| p.0).collect();
for w in widths.windows(2) {
assert!(w[0] > w[1], "the ladder is drawn widest-first: {widths:?}");
}
assert!((widths[widths.len() - 1] - 1.6).abs() < 1e-4, "the last pass IS the core width");
assert!(glow_reach_mul() >= 10.0, "the halo reaches {}x the core", glow_reach_mul());
// 2. ALPHA: a monotone geometric ramp, faint outside, opaque at the core.
let alphas: Vec<f32> = passes.iter().map(|p| p.1[3]).collect();
for a in alphas.windows(2) {
assert!(a[1] > a[0], "alpha rises inward: {alphas:?}");
}
assert!(alphas[0] <= 0.10, "the outer bleed is nearly transparent: {}", alphas[0]);
assert!(alphas[alphas.len() - 1] >= 0.99, "the core is opaque: {:?}", alphas.last());
// geometric, not linear: each step is a bigger jump than the last.
let steps: Vec<f32> = alphas.windows(2).map(|w| w[1] - w[0]).collect();
for s in steps.windows(2) {
assert!(s[1] > s[0], "the alpha ramp accelerates inward: {steps:?}");
}
// 3. COLOUR: the outer passes keep the hue, the core burns toward white.
let outer = passes[0].1;
assert!((outer[0] - ink[0]).abs() < 1e-6, "the bleed is the pure hue");
let core = passes[passes.len() - 1].1;
assert!(core[0] > 0.85, "the core is white-hot: r={}", core[0]);
assert!(core[2] > 0.95, "…and still carries the hue's blue: b={}", core[2]);
// 4. COVERAGE: the stacked passes actually reach opacity at the centre.
let cov = glow_core_coverage();
assert!(cov > 0.99, "the ladder covers {cov} at the centre — a solid core");
}
/// The flow phase is pure in the clock and wraps — a glow that MOVES without a
/// hidden `Instant::now()`.
#[test]
fn the_flow_phase_is_pure_and_wraps() {
assert!((glow_flow_phase(0.0, 2.0) - 0.0).abs() < 1e-6);
assert!((glow_flow_phase(1.0, 2.0) - 0.5).abs() < 1e-6);
assert!((glow_flow_phase(2.0, 2.0) - 0.0).abs() < 1e-6, "one period wraps to 0");
assert!(
(glow_flow_phase(3.7, 2.0) - glow_flow_phase(5.7, 2.0)).abs() < 1e-5,
"pure and periodic",
);
// A zero period cannot panic or divide by zero.
assert!(glow_flow_phase(1.0, 0.0).is_finite());
}
}
#[cfg(test)]
mod glow_cross_section_tests {
use super::*;
/// **RED-WHEN-BROKEN: the cross-section is a PEAK that fades to nothing, and it is
/// the same ladder the stroke stack reads** (owner, 2026-09-03).
///
/// Three properties, and each one is a thing the stroke stack got wrong:
///
/// 1. **The edge is transparent.** A stack ends on an antialiased boundary; a glow
/// ends on nothing. `alpha == 0` at the widest offset.
/// 2. **Alpha rises monotonically toward the centreline.** No plateau, anywhere —
/// the plateau IS the flat band.
/// 3. **It is the SAME ladder.** The ring alphas are `GLOW_LADDER`'s, in order, so
/// this cannot drift into a second opinion about what a glow looks like (LAW 5).
#[test]
fn the_cross_section_peaks_at_the_centre_and_fades_to_nothing() {
for ground in [GlowGround::Dark, GlowGround::Light] {
let xs = glow_cross_section(1.6, [0.2, 0.8, 1.0], ground);
assert!(xs.len() >= 3, "a profile needs an edge, some rings and a centre");
let (edge_off, edge_c) = xs[0];
assert_eq!(edge_c[3], 0.0, "{ground:?}: the ribbon fades OUT, it does not end on an edge");
assert!(edge_off > 0.0, "{ground:?}: the edge is off the centreline");
let (centre_off, centre_c) = *xs.last().expect("a centreline");
assert_eq!(centre_off, 0.0, "{ground:?}: the profile ends ON the centreline");
// Offsets march inward; alpha never falls on the way in.
let mut prev_off = f32::INFINITY;
let mut prev_a = -1.0f32;
for (off, c) in &xs {
assert!(*off <= prev_off, "{ground:?}: offsets go outer → inner, got {off} after {prev_off}");
assert!(
c[3] >= prev_a - 1e-6,
"{ground:?}: alpha DIPS on the way in ({} after {prev_a}) — a dip is a ring, \
and rings are what made it read as a band",
c[3],
);
prev_off = *off;
prev_a = c[3];
}
assert!(
centre_c[3] > edge_c[3],
"{ground:?}: the centreline must be brighter than the edge",
);
// NON-VACUITY: a profile of one flat colour would satisfy "never falls".
let alphas: Vec<f32> = xs.iter().map(|(_, c)| c[3]).collect();
let distinct = {
let mut v = alphas.clone();
v.sort_by(f32::total_cmp);
v.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
v.len()
};
assert!(distinct >= 4, "{ground:?}: a real falloff has steps, got {alphas:?}");
}
}
/// **RED-WHEN-BROKEN: it is a PROJECTION of the stroke ladder, not a second one.**
///
/// The graph tracers read `glow_stroke_passes_on` and the map trail reads this. If
/// the two ever disagreed about the ladder, one surface would drift from the other
/// and nobody would notice until a screenshot.
///
/// The colours are NOT the passes' own any more, and that is the point: five
/// stacked translucent strokes composite, so a single ribbon has to carry the
/// accumulation or it comes out far fainter than the stack it replaces (measured —
/// core departure 56 against the stack's 204, which is a trail you cannot see).
/// The accumulation is recomputed HERE, independently, so a cross-section that
/// invented its own ladder would still be caught.
#[test]
fn the_cross_section_carries_the_stroke_ladders_own_rings() {
let ink = [0.2, 0.8, 1.0];
let passes = glow_stroke_passes_on(1.6, ink, GlowGround::Dark);
let xs = glow_cross_section(1.6, ink, GlowGround::Dark);
let mut want: [f32; 4] = [0.0, 0.0, 0.0, 0.0];
for (i, (w, c)) in passes.iter().enumerate() {
// The independent over-composite, written out rather than called, so this
// test does not simply re-run the code it is checking.
let (sa, da) = (c[3], want[3]);
let oa = sa + da * (1.0 - sa);
want = [
(c[0] * sa + want[0] * da * (1.0 - sa)) / oa,
(c[1] * sa + want[1] * da * (1.0 - sa)) / oa,
(c[2] * sa + want[2] * da * (1.0 - sa)) / oa,
oa,
];
let (off, xc) = xs[i + 1];
assert!(
(off - w * 0.5).abs() < 1e-6,
"ring {i}: the offset is the PASS's half-width — the widths come from \
the ladder even though the colours accumulate",
);
for k in 0..4 {
assert!(
(xc[k] - want[k]).abs() < 1e-5,
"ring {i} channel {k}: {} vs the composite {}",
xc[k],
want[k],
);
}
assert!(
xc[3] >= c[3] - 1e-6,
"ring {i}: accumulating can only make the ribbon MORE opaque, not less",
);
}
// And the centreline is the whole ladder — the same number `glow_core_coverage`
// publishes, which is what makes that function and this one one statement.
let core = xs.last().expect("a centreline");
assert!((core.0 - 0.0).abs() < 1e-6, "the centreline sits at offset 0");
assert!(
(core.1[3] - glow_core_coverage()).abs() < 1e-5,
"the centre's alpha is the ladder's own coverage: {} vs {}",
core.1[3],
glow_core_coverage(),
);
}
}