Skip to content

feat(shaders): EdgeFade — per-edge alpha fade shader for both backends#99

Open
chiefcll wants to merge 1 commit into
mainfrom
feat/edge-fade-shader
Open

feat(shaders): EdgeFade — per-edge alpha fade shader for both backends#99
chiefcll wants to merge 1 commit into
mainfrom
feat/edge-fade-shader

Conversation

@chiefcll

Copy link
Copy Markdown
Contributor

What

Adds an EdgeFade shader: per-edge pixel distances (left / top / right / bottom, all default 0) that multiply the node's alpha by a linear ramp inward from each edge — fading an image (or color rect) out to reveal whatever renders behind it.

renderer.createNode({
  src: 'hero.jpg',
  w: 1280, h: 720,
  shader: renderer.createShader('EdgeFade', { right: 420 }),
  parent,
});

Why

The common "fade the edge of a hero image" effect previously required either an overlay scrim (which tints instead of masking, and breaks under parent alpha fades) or splitting the image into two nodes with a SubTexture + white-to-transparent corner colors. LinearGradient doesn't cover this: it blends a gradient color over the texture's RGB and preserves the texture's alpha, so it can't reveal the background. EdgeFade does the alpha mask directly, with one node and no RTT.

How

WebGL (src/core/shaders/webgl/EdgeFade.ts)

  • Reuses the existing a_nodeCoords varying and the system u_dimensions uniform — no new attributes.
  • update() uploads fade reciprocals (1e6 sentinel for disabled edges), so the fragment shader is 4 clamps + 4 multiplies with no division and no branches.
  • Uniforms are a pure function of resolved props; node w/h enters only via the system uniform. Value keys already include node dimensions, so two same-prop / different-size nodes can't share a render op. All shader caching contracts hold.
  • Multiplying the whole premultiplied vec4 keeps it composable with tint, corner colors, and worldAlpha/parent fades.

Canvas2D (src/core/shaders/canvas/EdgeFade.ts)

  • Draws the node's content into a shared, grow-only scratch canvas via a new public CanvasRenderer.renderNodeContent(node, ctx) hook (redirects the existing draw path to an arbitrary 2D context), erases each edge with a destination-out linear gradient, and composites the result back. Sequential destination-out passes multiply — exactly the WebGL ramp product — and the background behind the node stays intact (a destination-out pass on the main canvas would erase it).

Reviewer notes

  • Batching cost: like any non-default shader, an EdgeFade node becomes its own render op (~a handful of extra GL calls/frame). Fine for hero images; for many nodes in a scroll row the two-node SubTexture approach remains the perf-optimal path.
  • Fade > dimension is well-defined: the ramp never reaches 1, so the node never reaches full opacity (covered by a visual test case).
  • Canvas2D images still take only the top-left corner color as tint — pre-existing backend limitation, visible in visual test case 5; the fade itself matches WebGL.

Testing

  • Unit tests for prop resolution (EdgeFadeTemplate.test.ts); full suite green.
  • Visual regression test examples/tests/shader-edge-fade.ts over a checkerboard background (proves the fade reveals what's behind, not just darkens): right-fade image, four-edge vignette, color rect, no-op identity vs default shader, composition with alpha + corner colors, fade-larger-than-node. Certified CI snapshot captured via the Docker runner and manually inspected.
  • Verified in-browser on WebGL and Canvas2D, in both dev and prod (vite build legacy transpile — Chrome 38 path) modes.

🤖 Generated with Claude Code

Adds an EdgeFade shader that multiplies a node's alpha by a linear ramp
inward from each edge with a non-zero pixel distance (left/top/right/
bottom). Unlike LinearGradient, which blends a gradient color over the
texture's RGB, EdgeFade masks the texture's own alpha — fading an image
out to reveal whatever renders behind it, with a single node and no
SubTexture splitting or RTT.

WebGL: reuses the existing a_nodeCoords varying and system u_dimensions
uniform; update() uploads reciprocals so the fragment adds 4 clamps + 4
multiplies with no division or branches. Uniforms are a pure function of
resolved props, preserving the shader value-key cache contract.

Canvas2D: renders the node's content into a shared grow-only scratch
canvas via a new CanvasRenderer.renderNodeContent() hook, erases edges
with destination-out gradients (sequential passes multiply, matching the
WebGL ramp product), and composites back — keeping the background behind
the node intact.

Includes a visual regression test over a checkerboard (proving reveal,
not darkening) with its certified CI snapshot, and unit tests for prop
resolution. Verified on WebGL + Canvas2D in dev and prod (legacy
transpile) modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant