Triangle LED Hero
Analytic edge-glow triangle with LED emitters, floor radiance, and interactive color deploy. Canvas-scoped pointer input drives the lighting while an accessible mode selector chooses highlighted edges.
index.tsxcontrols.tsxtypes.tsrenderer.tsscene-renderer.tslight-sources-raw.tslight-sources-pass.tsled-buffer.tssettings.tshero-frame-state.tsdirect-triangle-raycast.tsvalue-noise.tstriangle-hit.tssim-sizing.tsshaders/light-sources.wgslshaders/led-emitters.wgslshaders/direct-triangle-raycast.wgslshaders/floor-noise.wgslshaders/color-utils.wgslshaders/geometry.wgslshaders/floor-falloff.wgslshaders/hash.wgslshaders/themes/dark/main-scene-floor.wgslshaders/themes/light/main-scene-floor.wgsl
// Light theme floor material. In light mode the LEDs emit white; this shader builds a
// dark-style glow from the near/middle/far SDF falloffs and tints it with the per-edge
// brand colors. The color output is white or the edge-tinted color (blended by
// colorMix); the glow alpha fades it into the ground (premultiplied output).
// AO shadow is computed here (replaces the old SVG drop-shadow): black + alpha darkens
// the ground near the triangle via premultiplied compositing (page * (1 - a)).
import { oklab_to_rgb } from "../../color-utils.wgsl";
import { sdf_triangle_vertices } from "../../geometry.wgsl";
import { middle_falloff, far_falloff, value_map01 } from "../../floor-falloff.wgsl";
// Smooth max: like max(a,b) but rounds the transition over a width k, removing the hard crease
// where two edge planes meet (otherwise visible as 3D-looking ridges from the triangle corners).
fn ao_smax(a: f32, b: f32, k: f32) -> f32 {
let h = clamp(0.5 + 0.5 * (a - b) / max(k, 1e-4), 0.0, 1.0);
return mix(b, a, h) + k * h * (1.0 - h);
}
const AO_CORNER_SMOOTH: f32 = 0.25; // corner-crease smoothing, as a fraction of the triangle circumradius
// Packed uniforms from main-scene-pass.ts:
// - screen/light_sources: render and simulation texture dimensions + AO controls
// - tunables: LED intensity, floor albedo, light-mode radiance/AO parameters
// - triangle: triangle center/radius/half-width in screen pixels
// - culling/radiance_fit: probe culling and screen-to-radiance mapping
// - light_ao/radiance_debug: contact AO, highlight, and debug visualization controls
struct Config { screen: vec4f, light_sources: vec4f, tunables: vec4f, triangle: vec4f, culling: vec4f, radiance_fit: vec4f, light_ao: vec4f, radiance_debug: vec4f, sim_transform: vec4f };
@group(0) @binding(0) var<uniform> cfg: Config;
// Low-frequency colored light generated by the cascade/radiance passes.
@group(0) @binding(1) var radiance_tex: texture_2d<f32>;
// Direct light-source/SDF texture. RGB stores source color; alpha stores SDF data.
@group(0) @binding(2) var light_sources_tex: texture_2d<f32>;
@group(0) @binding(3) var linear_samp: sampler;
// Light-glow settings (separate uniform — see main-scene-pass.ts). Packs the
// near/middle/far falloff params + contrast so light builds the dark-style glow and
// tunes it independently of dark.
// The MONO set (grayscale/idle floor) and the COLOR set (colored/deploy floor) carry two
// independent copies of the per-layer glow SHAPE params. The shader blends mono→color per layer by
// colorMix (= glow.y): near_p = mix(near, nearColor, colorMix), same for middle/glow/params/toggles.
// near/middle contribution stays gated by colorMix (as on main), so the colorMix=0 floor is exactly
// main's far-only grayscale; the blended COLOR params shape near/middle as they ramp in.
// near: (distanceScale, intensity, mapMin, mapMax)
// middle: (power, intensity, highlightNoise, _) — highlightNoise (.z) is MONO-only, never blended
// glow: (far intensity, colorMix, mapMin, mapMax)
// params: (nearPower, middleOuterScale, farPower, fadeInner)
// toggles: (nearEnabled, middleEnabled, farEnabled, contrast)
// glow.y (colorMix) blends the floor tint from white light (0) to fully colored (1);
// the renderer drives it from the hover/deploy factor.
// edgeRed/Green/Blue carry the per-edge floor colors (sRGB in .xyz, .w unused), GUI-editable.
// nearColor/middleColor/glowColor/paramsColor/togglesColor: the COLOR copy of the matching slots
// (glowColor.y unused — colorMix stays authoritative on glow.y).
struct LightGlow { near: vec4f, middle: vec4f, glow: vec4f, params: vec4f, toggles: vec4f, ao: vec4f, ao2: vec4f, ao3: vec4f, edgeRed: vec4f, edgeGreen: vec4f, edgeBlue: vec4f, nearColor: vec4f, middleColor: vec4f, glowColor: vec4f, paramsColor: vec4f, togglesColor: vec4f };
@group(0) @binding(4) var<uniform> lg: LightGlow;
// Cheap per-pixel hashes (Dave Hoskins hash12 + a reseeded sibling) → [0,1], ported from the dark
// floor. Used to add grain that EXAGGERATES the highlight mask (amount = lg.middle.z = highlightNoise).
fn hash12(p: vec2f) -> f32 {
var p3 = fract(vec3f(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
fn hash12b(p: vec2f) -> f32 {
var p3 = fract(vec3f(p.xyx) * 0.1531);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
struct VSOut { @builtin(position) pos: vec4f };
// Draw a fullscreen triangle for the floor material pass.
@vertex fn vs_main(@builtin(vertex_index) vi: u32) -> VSOut {
var p = array<vec2f, 3>(vec2f(-1.0, -3.0), vec2f(-1.0, 1.0), vec2f(3.0, 1.0));
var out: VSOut;
out.pos = vec4f(p[vi], 0.0, 1.0);
return out;
}
fn catmull_rom_weight(x: f32) -> f32 {
let ax = abs(x);
if (ax <= 1.0) {
return 1.5 * ax * ax * ax - 2.5 * ax * ax + 1.0;
}
if (ax < 2.0) {
return -0.5 * ax * ax * ax + 2.5 * ax * ax - 4.0 * ax + 2.0;
}
return 0.0;
}
// Catmull-Rom 4x4 sampling for the half-resolution direct radiance target.
// Taps are clamped in texel space so edge pixels do not smear wrapped values.
fn sample_radiance_cubic(uv: vec2f) -> vec3f {
let dims_u = textureDimensions(radiance_tex);
let dims = vec2f(dims_u);
let texel = clamp(uv, vec2f(0.0), vec2f(1.0)) * dims - vec2f(0.5);
let base = floor(texel);
let f = texel - base;
var sum = vec3f(0.0);
var weight_sum = 0.0;
for (var y: i32 = -1; y <= 2; y = y + 1) {
let wy = catmull_rom_weight(f.y - f32(y));
for (var x: i32 = -1; x <= 2; x = x + 1) {
let wx = catmull_rom_weight(f.x - f32(x));
let w = wx * wy;
let tap = clamp(vec2i(base) + vec2i(x, y), vec2i(0), vec2i(dims_u) - vec2i(1));
sum += textureLoad(radiance_tex, tap, 0).rgb * w;
weight_sum += w;
}
}
return max(sum / max(weight_sum, 1e-5), vec3f(0.0));
}
fn sample_radiance_at(pixel_screen: vec2f) -> vec3f {
let fitted_uv = (pixel_screen - cfg.radiance_fit.xy) / cfg.radiance_fit.zw;
let inside_fit =
fitted_uv.x >= 0.0 && fitted_uv.x <= 1.0 &&
fitted_uv.y >= 0.0 && fitted_uv.y <= 1.0;
var radiance = sample_radiance_cubic(fitted_uv);
if (!inside_fit) {
radiance = vec3f(0.0);
}
return radiance;
}
// Light-mode floor tint. In light mode the LEDs emit white, so the floor itself colors
// the light. Each triangle edge maps to a brand color; a per-pixel inverse-distance
// blend of the three edge centers builds the tint (matching dark mode's CPU edge
// mapping: red → top-left, green → bottom, blue → top-right edge midpoints). The per-edge
// colors are GUI-editable sRGB, carried in lg.edgeRed/edgeGreen/edgeBlue (.xyz).
// Triangular angular weight: 1 at theta_i, falling linearly to 0 at 120° away. For three
// stops 120° apart this yields exact linear interpolation between the two nearest colors.
fn angular_weight(theta: f32, theta_i: f32) -> f32 {
let d = theta - theta_i;
let dist = abs(atan2(sin(d), cos(d))); // shortest wrapped angular distance, [0, π]
return max(0.0, 1.0 - dist / 2.0943951); // 2.0943951 = 120° (2π/3)
}
fn floor_edge_tint(pixel: vec2f, top: vec2f, left: vec2f, right: vec2f) -> vec3f {
// Conic gradient: the tint depends only on the angle around the triangle center, so the
// three edge colors interpolate at a constant rate all the way around (no localized
// "spots" like the old inverse-distance weighting produced). Each edge center is 120°
// apart, and angular_weight gives exact linear interpolation between the two nearest.
let center = cfg.triangle.xy;
let theta = atan2(pixel.y - center.y, pixel.x - center.x);
let red_center = (top + left) * 0.5;
let green_center = (left + right) * 0.5;
let blue_center = (right + top) * 0.5;
// Overlap control (lg.edgeRed.w = 1/overlap): raise the angular weights before normalizing.
// 1 = unchanged; >1 (overlap<1) sharpens each edge color; <1 (overlap>1) blends them more.
let inv_overlap = lg.edgeRed.w;
let wr = pow(angular_weight(theta, atan2(red_center.y - center.y, red_center.x - center.x)), inv_overlap);
let wg = pow(angular_weight(theta, atan2(green_center.y - center.y, green_center.x - center.x)), inv_overlap);
let wb = pow(angular_weight(theta, atan2(blue_center.y - center.y, blue_center.x - center.x)), inv_overlap);
let wsum = max(wr + wg + wb, 1e-4);
// Vivid OKLCh-style blend. Averaging OKLab a/b directly lets opposing hues cancel,
// collapsing chroma into muddy/gray midpoints. Instead, average lightness and chroma
// (magnitude) independently, and take the hue from the weighted sum of the a/b vectors
// — the short-arc circular mean. Renormalizing that direction back to the averaged
// chroma keeps transitions saturated (red→blue stays vivid through violet, not gray).
// rgb↔oklab round-trips exactly, so each corner stays its true brand color.
// Precomputed CPU-side (cbrt-LMS of the GUI-editable edge colors) and carried in
// edgeRed/Green/Blue.xyz — hoists the three rgb_to_oklab cube-root triples out of the per-pixel
// loop, since they depend only on the edge-color uniforms.
let lab_r = lg.edgeRed.xyz;
let lab_g = lg.edgeGreen.xyz;
let lab_b = lg.edgeBlue.xyz;
let l = (lab_r.x * wr + lab_g.x * wg + lab_b.x * wb) / wsum;
let chroma =
(length(lab_r.yz) * wr + length(lab_g.yz) * wg + length(lab_b.yz) * wb) / wsum;
let ab_sum = lab_r.yz * wr + lab_g.yz * wg + lab_b.yz * wb;
let ab_len = length(ab_sum);
// Below the guard the hues cancel (the neutral point where all edges meet equally) —
// leave it near-gray rather than amplifying numerical noise into a random hue.
var ab = vec2f(0.0);
if (ab_len > 1e-5) {
ab = (ab_sum / ab_len) * chroma;
}
return clamp(oklab_to_rgb(vec3f(l, ab.x, ab.y)), vec3f(0.0), vec3f(1.0));
}
// Smooth screen-edge envelope: 1 across the interior, easing to 0 near ALL FOUR edges. The fade
// width (packed into cfg.sim_transform.w — .w is free; the shader only reads .xyz for the sim→screen
// transform) is a fraction of the canvas HEIGHT, used as the same pixel band on every edge so the
// border is uniform (corners fade fastest, where both factors drop). Applied to ALPHA (not color)
// because light composites premultiplied over the background; scaling alpha fades the glow into the
// ground at the edges — left/right too, so any color reaching a side edge eases out.
fn edge_fade(pixel_screen: vec2f) -> f32 {
let w = max(cfg.sim_transform.w * cfg.screen.y, 1.0);
let dy = min(pixel_screen.y, cfg.screen.y - pixel_screen.y);
let dx = min(pixel_screen.x, cfg.screen.x - pixel_screen.x);
return smoothstep(0.0, w, dy) * smoothstep(0.0, w, dx);
}
// Signed distance to one triangle edge's outward half-plane (positive outside the triangle).
fn ao_half_plane_distance(p: vec2f, e0: vec2f, e1: vec2f, center: vec2f) -> f32 {
let edge = e1 - e0;
var n = normalize(vec2f(-edge.y, edge.x));
if (dot(n, e0 - center) < 0.0) { n = -n; } // force outward (away from centroid)
return dot(p - e0, n);
}
// Triangle distance from its three edge half-planes (sharp/miter corners) instead of the true
// SDF. The SDF rounds at the vertices, which reads as unrealistic circular arcs in the AO; the
// smooth-max of the outward edge-plane distances keeps the AO isolines triangle-shaped (straight
// edges) while blending the crease at each corner so the hard-max ridges/pyramid lines disappear.
fn ao_triangle_plane_distance(p: vec2f, a: vec2f, b: vec2f, c: vec2f) -> f32 {
let center = (a + b + c) * (1.0 / 3.0);
let k = cfg.triangle.z * AO_CORNER_SMOOTH; // cfg.triangle.z = circumradius (screen px)
let dab = ao_half_plane_distance(p, a, b, center);
let dbc = ao_half_plane_distance(p, b, c, center);
let dca = ao_half_plane_distance(p, c, a, center);
return ao_smax(ao_smax(dab, dbc, k), dca, k);
}
@fragment fn fs_main(in: VSOut) -> @location(0) vec4f {
// Convert fragment position into normalized screen UVs and screen-pixel space.
let uv = in.pos.xy / cfg.screen.xy;
let pixel_screen = in.pos.xy;
// colorMix: 0 = grayscale/idle (white light), 1 = colored/deploy-active.
// Defined once here; reused to gate the middle falloff and the AO block.
let colorMix = clamp(lg.glow.y, 0.0, 1.0);
// Rebuild the triangle vertices in screen space so this pass can compute SDF
// masks/falloffs that align with the mesh drawn over the floor.
let top = vec2f(cfg.triangle.x, cfg.triangle.y - cfg.triangle.z);
let left = vec2f(cfg.triangle.x - cfg.triangle.w, cfg.triangle.y + cfg.triangle.z * 0.5);
let right = vec2f(cfg.triangle.x + cfg.triangle.w, cfg.triangle.y + cfg.triangle.z * 0.5);
let triangle_sdf = sdf_triangle_vertices(pixel_screen, top, left, right);
// Map screen pixels into the fitted radiance texture. Outside the fitted rect we
// zero radiance to avoid edge smearing from clamped texture samples.
let radiance = sample_radiance_at(pixel_screen);
// Existing raw-radiance debug mode.
if (cfg.radiance_debug.x > 0.5) {
return vec4f(radiance, 1.0);
}
// Combine the three falloffs the same way dark mode does. NOT a plain sum:
// screen-blend the in-range [0,1] parts (preserves the tuned look), then add any
// over-1 overflow additively (a plain screen blend goes non-monotonic past 1, so
// the overflow is split off to keep brightness increasing). `brightness_factor`
// is the combined result.
let near_light = dot(radiance, vec3f(0.2126, 0.7152, 0.0722));
// Per-layer MONO→COLOR param blend by colorMix (0 = grayscale/idle, 1 = colored/deploy): each
// falloff reads the blended shape params, so the mono set drives the white floor and the color
// set drives the colored floor. highlightNoise (lg.middle.z) is NOT blended — it is read straight
// from the mono middle below. The near/middle CONTRIBUTION stays gated by colorMix (as on main):
// at colorMix=0 only `far` contributes, so the grayscale floor is byte-for-byte main's; near and
// middle (using their now-blended COLOR params) ramp in only as colorMix → 1.
let near_p = mix(lg.near, lg.nearColor, colorMix);
let middle_p = mix(lg.middle, lg.middleColor, colorMix);
let glow_p = mix(lg.glow, lg.glowColor, colorMix);
let params_p = mix(lg.params, lg.paramsColor, colorMix);
let toggles_p = mix(lg.toggles, lg.togglesColor, colorMix);
let fade_inner = params_p.w;
// NEAR (colored only): a PURE LUMINANCE remap — NO SDF band. value_map01(near_light, mapMin,
// mapMax) shaped by nearPower (a contrast curve on the luminance, not a band), scaled by
// intensity. Gated by colorMix like before, so the grayscale floor (far-only) is unchanged.
// nearDistanceScale + fadeInner no longer affect near (the SDF band is gone).
let near = pow(value_map01(near_light, near_p.z, near_p.w), max(params_p.x, 0.001))
* near_p.y * toggles_p.x * colorMix;
let middle = middle_falloff(
triangle_sdf,
fade_inner,
cfg.triangle.z,
params_p.y,
middle_p,
toggles_p.y,
) * colorMix;
let far = far_falloff(
near_light,
glow_p,
params_p.z,
toggles_p.z,
);
let screen_blend =
1.0 - (1.0 - min(near, 1.0)) * (1.0 - min(middle, 1.0)) * (1.0 - min(far, 1.0));
let overflow =
max(near - 1.0, 0.0) + max(middle - 1.0, 0.0) + max(far - 1.0, 0.0);
let brightness_factor = screen_blend + overflow;
var alpha = 1.;
// Foreground triangle occluder, drawn analytically from the same SDF (the SDF gradient length
// gives a true ~1px edge AA), matching dark mode — no separate geometry pass.
// cfg.culling.z is the show-triangle flag (0/1). length(dpdx, dpdy) is used instead of fwidth's
// Manhattan sum |dpdx|+|dpdy|, which over-widens diagonal edges (like the triangle's) by up to sqrt(2).
let occluder_edge = max(length(vec2f(dpdx(triangle_sdf), dpdy(triangle_sdf))), 1e-4);
let occluder = clamp(0.5 - triangle_sdf / occluder_edge, 0.0, 1.0) * cfg.culling.z;
alpha = mix(alpha, 0.0, occluder);
var highlight = near;
highlight = clamp(highlight, 0.0, 1.0);
highlight = pow(highlight, 0.5);
highlight *= 1. - occluder; // Don't highlight where the triangle occluder is.
// EXAGGERATE the highlight mask with per-pixel hash grain (lg.middle.z = highlightNoise): a
// multiplicative ±variance that breaks the smooth highlight into a punchy, grainy mask.
let hl_grain = hash12b(pixel_screen) + 0.5 * hash12(pixel_screen); // ~[0, 1.5]
highlight = clamp(highlight * (1.0 + (hl_grain - 0.75) * lg.middle.z), 0.0, 1.0);
// DEBUG (toggle in Glow (color) = lg.glowColor.y): render the raw `highlight` variable as opaque
// grayscale, bypassing the composite.
if (lg.glowColor.y > 0.5) {
return vec4f(vec3f(highlight), 1.0);
}
// highlight = 0.;
// Apply dark-style contrast to the combined glow, then use it as the alpha.
// `alpha` carries the occluder cut. Contrast is the colorMix-blended toggles_p.w.
let light_glow = (brightness_factor - 0.5) * toggles_p.w + 0.5;
var glow_alpha = clamp(light_glow, 0.0, 1.0) * alpha;
// contrast when light is white. The old mix(pow(a,1.2)*1.2, a*1.5, 1.0) selected the second
// arg verbatim (blend factor 1.0), so the pow branch was dead — it is just a * 1.5.
glow_alpha = glow_alpha * 1.5;
glow_alpha = clamp(glow_alpha, 0.0, 1.0);
// Vertical screen-edge fade (mirrors dark edge_fade but multiplies ALPHA, not color:
// light composites premultiplied over the ground, so scaling alpha fades the glow into
// the background at top/bottom without pushing color toward black).
glow_alpha = glow_alpha * edge_fade(pixel_screen);
// Color output: white light or the per-edge brand tint, blended by colorMix
// (lg.glow.y). The glow alpha then fades it into the ground (premultiplied output).
var edge_tint = floor_edge_tint(pixel_screen, top, left, right);
edge_tint += edge_tint * highlight;
edge_tint = clamp(edge_tint, vec3f(0.0), vec3f(1.0));
edge_tint = mix(edge_tint, vec3f(1.0), pow(highlight, 2.2) * 0.7);
var color = mix(vec3f(1.0), edge_tint, clamp(lg.glow.y, 0.0, 1.0));
// Brightness boost only on the tinted floor: ×1 at colorMix=0 (white stays white, no idle
// blowout) ramping to ×2 at colorMix=1 (fully colored).
color *= mix(1.0, 2.0, colorMix);
// Three-layer AO: all layers darken the ground near the triangle via premultiplied compositing.
// ao (layer 1): broad ambient shadow. ao2 (layer 2): smaller contact AO. ao3 (layer 3): tightest contact.
// Gated to no-light areas (1 - colorMix) and outside the triangle occluder. Combined darkness
// terms are summed and clamped before the shared gate and single composite step.
let ao_outside = max(ao_triangle_plane_distance(pixel_screen, top, left, right), 0.0);
// Layer 1 (the larger broad ambient shadow) uses the true triangle SDF, so its wide isolines
// round off at the corners; the tighter contact layers (ao2/ao3) keep the straight-edged
// half-plane distance.
let ao_outside_sdf = max(triangle_sdf, 0.0);
let ao1 = lg.ao.x * pow(1.0 - clamp(ao_outside_sdf / max(cfg.triangle.z * lg.ao.y, 1e-4), 0.0, 1.0), max(lg.ao.z, 0.01)) * step(0.5, lg.ao.w);
// ao2 is a second (smaller/contact) AO layer that stacks on ao.
let ao2 = lg.ao2.x * pow(1.0 - clamp(ao_outside / max(cfg.triangle.z * lg.ao2.y, 1e-4), 0.0, 1.0), max(lg.ao2.z, 0.01)) * step(0.5, lg.ao2.w);
// ao3 is the tightest contact AO layer (smallest radius, sharpest power).
let ao3 = lg.ao3.x * pow(1.0 - clamp(ao_outside / max(cfg.triangle.z * lg.ao3.y, 1e-4), 0.0, 1.0), max(lg.ao3.z, 0.01)) * step(0.5, lg.ao3.w);
let ao_dark = clamp(ao1 + ao2 + ao3, 0.0, 1.0) * (1.0 - colorMix) * (1.0 - occluder);
let out_alpha = glow_alpha + ao_dark * (1.0 - glow_alpha);
// Paint the foreground triangle as opaque black in the canvas itself (premultiplied "over"
// the floor), rather than cutting a transparent hole for the SVG triangle behind it. The
// occluder mask and the SVG triangle don't line up to the sub-pixel, which left a thin ring
// of uncovered page (white) along the edge. Compositing the opaque-black triangle here means
// the canvas fully covers that region itself, so there's no seam (works in light + color).
let final_rgb = color * glow_alpha * (1.0 - occluder);
let final_alpha = out_alpha * (1.0 - occluder) + occluder;
return vec4f(final_rgb, final_alpha);
}