Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions examples/tests/alpha-ignore-parent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import type { ExampleSettings } from '../common/ExampleSettings.js';

export async function automation(settings: ExampleSettings) {
await test(settings);
await settings.snapshot();
}

export default async function test({ renderer, testRoot }: ExampleSettings) {
// Checkerboard background so partial transparency is visible
const tile = 100;
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 13; col++) {
renderer.createNode({
x: col * tile,
y: row * tile,
w: tile,
h: tile,
color: (row + col) % 2 === 0 ? 0x113355ff : 0xddaa33ff,
parent: testRoot,
});
}
}

// 1. Parent at alpha 0.4: normal child fades with it,
// ignoreParentAlpha child renders at its own alpha (1)
const fadedParent = renderer.createNode({
x: 40,
y: 40,
w: 560,
h: 200,
alpha: 0.4,
parent: testRoot,
});
renderer.createNode({
x: 0,
y: 0,
w: 260,
h: 200,
color: 0xff0000ff,
parent: fadedParent,
});
renderer.createNode({
x: 300,
y: 0,
w: 260,
h: 200,
color: 0xff0000ff,
ignoreParentAlpha: true,
parent: fadedParent,
});

// 2. Parent at alpha 0.05: normal child is nearly invisible,
// ignoreParentAlpha child remains fully visible. (At alpha exactly 0
// the whole subtree is culled from rendering, by design.)
const invisibleParent = renderer.createNode({
x: 700,
y: 40,
w: 560,
h: 200,
alpha: 0.05,
parent: testRoot,
});
renderer.createNode({
x: 0,
y: 0,
w: 260,
h: 200,
color: 0x00ff00ff,
parent: invisibleParent,
});
renderer.createNode({
x: 300,
y: 0,
w: 260,
h: 200,
color: 0x00ff00ff,
ignoreParentAlpha: true,
parent: invisibleParent,
});

// 3. The ignoring node's own alpha still applies (0.5), and its
// descendants inherit its world alpha as usual (0.5 * 0.5 = 0.25)
const fadedParent2 = renderer.createNode({
x: 40,
y: 320,
w: 560,
h: 200,
alpha: 0.1,
parent: testRoot,
});
const halfAlphaChild = renderer.createNode({
x: 0,
y: 0,
w: 260,
h: 200,
color: 0x0000ffff,
alpha: 0.5,
ignoreParentAlpha: true,
parent: fadedParent2,
});
renderer.createNode({
x: 300,
y: 0,
w: 260,
h: 200,
color: 0x0000ffff,
alpha: 0.5,
parent: halfAlphaChild,
});

// 4. Text node child: ignoreParentAlpha keeps text readable while the
// parent fades
const textParent = renderer.createNode({
x: 700,
y: 320,
w: 560,
h: 200,
alpha: 0.1,
parent: testRoot,
});
renderer.createTextNode({
x: 0,
y: 0,
text: 'Faded with parent',
fontFamily: 'Ubuntu',
fontSize: 40,
color: 0x000000ff,
parent: textParent,
});
renderer.createTextNode({
x: 0,
y: 80,
text: 'ignoreParentAlpha',
fontFamily: 'Ubuntu',
fontSize: 40,
color: 0x000000ff,
ignoreParentAlpha: true,
parent: textParent,
});

// 5. Toggling back off behaves like a normal child again
const toggledParent = renderer.createNode({
x: 40,
y: 600,
w: 560,
h: 200,
alpha: 0.4,
parent: testRoot,
});
const toggled = renderer.createNode({
x: 0,
y: 0,
w: 260,
h: 200,
color: 0xff00ffff,
ignoreParentAlpha: true,
parent: toggledParent,
});
toggled.ignoreParentAlpha = false;
}
107 changes: 107 additions & 0 deletions src/core/CoreNode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { premultiplyColorABGR } from '../utils.js';
describe('set color()', () => {
const defaultProps = (overrides?: Partial<CoreNodeProps>): CoreNodeProps => ({
alpha: 0,
ignoreParentAlpha: false,
autosize: false,
boundsMargin: null,
clipping: false,
Expand Down Expand Up @@ -264,6 +265,112 @@ describe('set color()', () => {
});
});

describe('ignoreParentAlpha', () => {
const makeParent = (worldAlpha: number) => {
const parent = new CoreNode(stage, defaultProps());
parent.globalTransform = Matrix3d.identity();
parent.worldAlpha = worldAlpha;
return parent;
};

it('multiplies parent world alpha by default', () => {
const parent = makeParent(0.5);
const node = new CoreNode(stage, defaultProps({ parent, alpha: 0.8 }));

node.update(0, clippingRect);

expect(node.worldAlpha).toBeCloseTo(0.4);
});

it('uses own alpha only when enabled', () => {
const parent = makeParent(0.5);
const node = new CoreNode(
stage,
defaultProps({ parent, alpha: 0.8, ignoreParentAlpha: true }),
);

node.update(0, clippingRect);

expect(node.worldAlpha).toBe(0.8);
});

it('keeps its own world alpha while the parent fades toward 0', () => {
const parent = makeParent(0.01);
const node = new CoreNode(
stage,
defaultProps({ parent, alpha: 1, ignoreParentAlpha: true }),
);
node.w = 100;
node.h = 100;
node.color = 0xffffffff;

node.update(0, clippingRect);

expect(node.worldAlpha).toBe(1);
expect(node.isRenderable).toBe(true);
});

it('premultiplies colors with the node own alpha when enabled', () => {
const parent = makeParent(0.25);
const node = new CoreNode(
stage,
defaultProps({ parent, alpha: 0.8, ignoreParentAlpha: true }),
);
node.w = 100;
node.h = 100;
node.color = 0xff0000ff;

node.update(0, clippingRect);

expect(node.premultipliedColorTl).toBe(
premultiplyColorABGR(0xff0000ff, 0.8),
);
});

it('toggling the setter recomputes world alpha', () => {
const parent = makeParent(0.5);
const node = new CoreNode(stage, defaultProps({ parent, alpha: 0.8 }));

node.update(0, clippingRect);
expect(node.worldAlpha).toBeCloseTo(0.4);

node.ignoreParentAlpha = true;
node.update(1, clippingRect);
expect(node.worldAlpha).toBe(0.8);

node.ignoreParentAlpha = false;
node.update(2, clippingRect);
expect(node.worldAlpha).toBeCloseTo(0.4);
});

it('setting the same value does not flag an update', () => {
const node = new CoreNode(stage, defaultProps());
const updateTypeBefore = node.updateType;

node.ignoreParentAlpha = false;

expect(node.updateType).toBe(updateTypeBefore);
});

it('descendants inherit the node world alpha as usual', () => {
const parent = makeParent(0.5);
const node = new CoreNode(
stage,
defaultProps({ parent, alpha: 0.8, ignoreParentAlpha: true }),
);
const child = new CoreNode(
stage,
defaultProps({ parent: node, alpha: 0.5 }),
);

node.update(0, clippingRect);
child.update(0, clippingRect);

expect(node.worldAlpha).toBe(0.8);
expect(child.worldAlpha).toBeCloseTo(0.4);
});
});

describe('autosize system', () => {
it('should initialize with autosize disabled', () => {
const node = new CoreNode(stage, defaultProps());
Expand Down
47 changes: 46 additions & 1 deletion src/core/CoreNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export enum UpdateType {
* @remarks
* CoreNode Properties Updated:
* - `worldAlpha` = `parent.worldAlpha` * `alpha`
* (or just `alpha` when `ignoreParentAlpha` is enabled)
*/
WorldAlpha = 64,

Expand Down Expand Up @@ -250,6 +251,29 @@ export interface CoreNodeProps {
* @default `1`
*/
alpha: number;
/**
* When enabled, the Node's world alpha is computed from its own
* {@link alpha} only, ignoring the alpha inherited from its ancestors.
*
* @remarks
* Normally `worldAlpha = parent.worldAlpha * alpha`, so fading a parent
* fades every descendant with it. With `ignoreParentAlpha` enabled this
* Node keeps rendering at its own alpha while its parent (and the rest of
* the subtree) fades.
*
* Subtrees whose world alpha reaches exactly 0 are culled from rendering
* entirely, so this Node still disappears once an ancestor hits alpha 0 —
* the prop only has an effect while every ancestor's alpha is above 0.
* This keeps the fully-transparent subtree cull free of bookkeeping.
*
* Descendants of this Node inherit from its world alpha as usual.
*
* Has no effect inside a render-to-texture subtree: the RTT root's
* composited quad is still faded as a single unit by its own world alpha.
*
* @default `false`
*/
ignoreParentAlpha: boolean;
/**
* Autosize
*
Expand Down Expand Up @@ -1453,7 +1477,10 @@ export class CoreNode extends EventEmitter {
}

if (updateType & UpdateType.WorldAlpha) {
this.worldAlpha = parent.worldAlpha * this.props.alpha;
this.worldAlpha =
props.ignoreParentAlpha === true
? props.alpha
: parent.worldAlpha * props.alpha;
updateType |=
UpdateType.PremultipliedColors |
UpdateType.Children |
Expand Down Expand Up @@ -2523,6 +2550,24 @@ export class CoreNode extends EventEmitter {
this.childUpdateType |= UpdateType.WorldAlpha;
}

get ignoreParentAlpha(): boolean {
return this.props.ignoreParentAlpha;
}

set ignoreParentAlpha(value: boolean) {
if (this.props.ignoreParentAlpha === value) {
return;
}
this.props.ignoreParentAlpha = value;
this.setUpdateType(
UpdateType.PremultipliedColors |
UpdateType.WorldAlpha |
UpdateType.Children |
UpdateType.IsRenderable,
);
this.childUpdateType |= UpdateType.WorldAlpha;
}

get autosize(): boolean {
return this.props.autosize;
}
Expand Down
1 change: 1 addition & 0 deletions src/core/CoreTextNode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const defaultProps = (
): CoreTextNodeProps => ({
// CoreNodeProps
alpha: 1,
ignoreParentAlpha: false,
autosize: false,
boundsMargin: null,
clipping: false,
Expand Down
2 changes: 2 additions & 0 deletions src/core/Stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ export class Stage {
w: appWidth,
h: appHeight,
alpha: 1,
ignoreParentAlpha: false,
autosize: false,
boundsMargin: null,
clipping: false,
Expand Down Expand Up @@ -1039,6 +1040,7 @@ export class Stage {
w: props.w ?? 0,
h: props.h ?? 0,
alpha: props.alpha ?? 1,
ignoreParentAlpha: props.ignoreParentAlpha ?? false,
autosize: props.autosize ?? false,
boundsMargin: props.boundsMargin ?? null,
clipping: props.clipping ?? false,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading