From 4dc937f9c0a63221be1012229b08f1bd345d6294 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sat, 20 Jun 2026 23:06:05 -0400 Subject: [PATCH] Onion tint: screen-blend instead of multiply (so outlines tint too) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multiplicative tint left blacks black, so outlines (the main thing to ghost in line art) never picked up the warm/cool color. Switched the canvas-blit tint to a screen blend (out = base + tint - base*tint): black → tint color, white unchanged, and a clean no-op at tint=(0,0,0) for all normal blits. Reverted the default .w slots to 0. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../lightningbeam-editor/src/gpu_brush.rs | 13 +++++++------ .../src/panes/shaders/canvas_blit.wgsl | 9 ++++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/gpu_brush.rs b/lightningbeam-ui/lightningbeam-editor/src/gpu_brush.rs index b1b5e40..0c83c56 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/gpu_brush.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/gpu_brush.rs @@ -2030,16 +2030,17 @@ impl BlitTransform { // Column-major 3×3: col0=(a,b,0), col1=(c,d,0), col2=(e,f,1) let [a, b, c, d, e, f] = combined.as_coeffs(); // The .w slots are unused by the 3×3 matrix; the shader reads them as an RGB - // tint (1,1,1 = no tint — the default for all normal blits). + // screen-blend tint ((0,0,0) = no tint — the default for all normal blits). Self { - col0: [a as f32, b as f32, 0.0, 1.0], - col1: [c as f32, d as f32, 0.0, 1.0], - col2: [e as f32, f as f32, 1.0, 1.0], + col0: [a as f32, b as f32, 0.0, 0.0], + col1: [c as f32, d as f32, 0.0, 0.0], + col2: [e as f32, f as f32, 1.0, 0.0], } } - /// Multiply the sampled color by an RGB tint (for onion-skin ghosts). Packed into - /// the matrix uniform's `.w` slots, so no extra binding is needed. + /// Screen-blend the sampled color toward an RGB tint (for onion-skin ghosts), so + /// blacks/outlines pick up the tint. Packed into the matrix uniform's `.w` slots, + /// so no extra binding is needed. pub fn with_tint(mut self, r: f32, g: f32, b: f32) -> Self { self.col0[3] = r; self.col1[3] = g; diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/canvas_blit.wgsl b/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/canvas_blit.wgsl index 37c56ee..e6e1db9 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/canvas_blit.wgsl +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/canvas_blit.wgsl @@ -67,8 +67,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let mask = textureSample(mask_tex, mask_sampler, canvas_uv).r; let masked_a = c.a * mask; let inv_a = select(0.0, 1.0 / c.a, c.a > 1e-6); - // RGB tint packed into the matrix uniform's unused .w slots (1,1,1 = no tint). - // Used by onion-skin ghosts (warm = past, cool = future). + // RGB tint packed into the matrix uniform's unused .w slots ((0,0,0) = no tint). + // Screen-blended so it recolors blacks too (outlines → warm/cool ghosts) and + // leaves white alone: out = base + tint - base*tint. Used by onion-skin ghosts. let tint = vec3(transform.col0.w, transform.col1.w, transform.col2.w); - return vec4(c.r * inv_a * tint.r, c.g * inv_a * tint.g, c.b * inv_a * tint.b, masked_a); + let base = vec3(c.r * inv_a, c.g * inv_a, c.b * inv_a); + let tinted = base + tint - base * tint; + return vec4(tinted, masked_a); }