Skip to content

feat(core): ignoreParentAlpha — render a node at its own alpha while ancestors fade#100

Merged
chiefcll merged 2 commits into
mainfrom
feat/ignore-parent-alpha
Jun 12, 2026
Merged

feat(core): ignoreParentAlpha — render a node at its own alpha while ancestors fade#100
chiefcll merged 2 commits into
mainfrom
feat/ignore-parent-alpha

Conversation

@chiefcll

@chiefcll chiefcll commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

What

Adds an ignoreParentAlpha boolean prop to nodes. When enabled, the node's world alpha is computed from its own alpha only — instead of parent.worldAlpha * alpha — so the node keeps rendering while its parent (and the rest of the subtree) fades.

renderer.createNode({
  // ...
  ignoreParentAlpha: true,
  parent: fadingParent,  // animate fadingParent.alpha freely
});
  • The node's own alpha still applies, and can still be animated.
  • Descendants of the ignoring node inherit its world alpha as usual.
  • Works for text nodes too; surfaces automatically through INode / INodeProps.

Why

A child's effective alpha is always multiplied by its ancestors', so there was no way to keep an overlay (scrim gradient, badge, focus ring) at full strength while its container fades. The earlier workaround — restructuring the overlay as a sibling of the fading container — forces awkward scene-graph layouts and breaks encapsulation in component frameworks.

How

A 3-line branch in the WorldAlpha update plus a getter/setter raising the same update flags as alpha. Render-list invalidation needed no new triggers: toggling the prop flips the node's isRenderable, which already requests a rebuild.

Reviewer notes

  • Alpha-0 boundary (deliberate): Stage.buildRenderList culls whole subtrees at worldAlpha === 0, unconditionally. So the prop only has an effect while every ancestor's alpha is above 0 — once an ancestor reaches exactly 0, the ignoring node is culled with the subtree. This keeps the fully-transparent cull free of bookkeeping (an earlier revision tracked a subtree counter to render through alpha-0 ancestors; it was dropped in favor of the simpler cull). Documented in the prop's TSDoc and pinned by the visual test.
  • RTT subtrees: no effect inside them — the RTT root's composited quad still fades as one unit. Documented in the prop's TSDoc.
  • Inspector overlay: the DOM mirror can't represent escaping ancestor opacity (CSS opacity composes with no escape hatch); debug-cosmetic only, prop passes through untouched.
  • Backend-agnostic by construction: the behavior lives in core (worldAlpha + premultiplied colors), so WebGL and Canvas2D agree.

Testing

  • Unit tests in CoreNode.test.ts: default multiply, ignore behavior, own world alpha while the parent fades toward 0, premultiplied colors from own alpha, toggle recompute, no-op set short-circuit, and descendant inheritance. Full suite 225/225 green; lint clean.
  • Visual regression test examples/tests/alpha-ignore-parent.ts with certified CI snapshot (captured via the Docker runner, manually inspected): faded parent vs ignoring child, near-zero parent (0.05) with a solid ignoring child, own-alpha + descendant inheritance, text nodes, and toggle-back-off.
  • Verified in-browser on WebGL and Canvas2D, in dev and prod (legacy transpile — Chrome 38 path) modes.

🤖 Generated with Claude Code

chiefcll and others added 2 commits June 11, 2026 21:52
…ancestors fade

Adds an ignoreParentAlpha boolean prop to CoreNode. When enabled, the
node's world alpha is computed from its own alpha only instead of
parent.worldAlpha * alpha, so the node stays visible while its parent
(and the rest of the subtree) fades — including at parent alpha 0. The
node's own alpha still applies and its descendants inherit its world
alpha as usual.

The render list builder culls whole subtrees at worldAlpha === 0, which
would make an ignoring child disappear exactly when a fade-out
completes. To keep that cull correct, each node now maintains an
ignoreParentAlphaCount (nodes in its subtree, itself included, with the
prop enabled), updated on the cold paths — prop setter, addChild/
removeChild, and construction — so the hot-path cull only gains one
comparison that short-circuits unless worldAlpha is already 0. Counts
err toward traversal (conservative skip).

No effect inside RTT subtrees (the composited quad still fades as one
unit); documented on the prop.

Includes unit tests for the alpha math, renderability under an alpha-0
parent, premultiplied colors, toggling, and counter propagation through
attach/toggle/reparent, plus a visual regression test with certified
snapshot covering rects and text nodes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…worldAlpha 0

Removes the subtree counter that let the render-list builder traverse
into fully transparent subtrees containing ignoreParentAlpha nodes. The
worldAlpha === 0 cull now applies unconditionally: ignoreParentAlpha
only has an effect while every ancestor's alpha is above 0, and the
node disappears with the subtree once an ancestor reaches exactly 0.

This trades the alpha-0 edge case for zero bookkeeping on the prop
setter, attach/detach, and construction paths, and restores the
original single-comparison cull. Documented on the prop's TSDoc; unit
and visual tests updated to pin the new semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chiefcll chiefcll merged commit d178f8f into main Jun 12, 2026
1 check passed
@chiefcll chiefcll deleted the feat/ignore-parent-alpha branch June 12, 2026 02:25
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