Onion tint: screen-blend instead of multiply (so outlines tint too)

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) <noreply@anthropic.com>
This commit is contained in:
Skyler Lehmkuhl 2026-06-20 23:06:05 -04:00
parent 53cae1bfe5
commit 4dc937f9c0
2 changed files with 13 additions and 9 deletions

View File

@ -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;

View File

@ -67,8 +67,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
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<f32>(transform.col0.w, transform.col1.w, transform.col2.w);
return vec4<f32>(c.r * inv_a * tint.r, c.g * inv_a * tint.g, c.b * inv_a * tint.b, masked_a);
let base = vec3<f32>(c.r * inv_a, c.g * inv_a, c.b * inv_a);
let tinted = base + tint - base * tint;
return vec4<f32>(tinted, masked_a);
}