From 0fd7d030ceeb6c7fff4927316ce9a9cfffae31ed Mon Sep 17 00:00:00 2001 From: Mat Groves Date: Thu, 21 Nov 2024 12:51:59 +0000 Subject: [PATCH] feat: Add some new functions to get global tint / alpha / transform (#11057) * add some new functions to get global tint / alpha / transform * fix import * move to mixin --------- Co-authored-by: Zyie <24736175+Zyie@users.noreply.github.com> --- src/scene/SceneMixins.d.ts | 2 + src/scene/container/Container.ts | 8 +- .../container-mixins/getGlobalMixin.ts | 147 +++++++++++++++ .../container-mixins/toLocalGlobalMixin.ts | 33 ++-- .../particle-container/shared/Particle.ts | 5 +- tests/scene/getGlobalAlpha.tests.ts | 118 ++++++++++++ tests/scene/getGlobalTint.tests.ts | 134 +++++++++++++ tests/scene/getGlobalTransform.tests.ts | 178 ++++++++++++++++++ 8 files changed, 595 insertions(+), 30 deletions(-) create mode 100644 src/scene/container/container-mixins/getGlobalMixin.ts create mode 100644 tests/scene/getGlobalAlpha.tests.ts create mode 100644 tests/scene/getGlobalTint.tests.ts create mode 100644 tests/scene/getGlobalTransform.tests.ts diff --git a/src/scene/SceneMixins.d.ts b/src/scene/SceneMixins.d.ts index 3a6cac5b1e..e007d9f112 100644 --- a/src/scene/SceneMixins.d.ts +++ b/src/scene/SceneMixins.d.ts @@ -3,6 +3,7 @@ import type { CacheAsTextureMixin, CacheAsTextureMixinConstructor } from './cont import type { ChildrenHelperMixin } from './container/container-mixins/childrenHelperMixin'; import type { EffectsMixin, EffectsMixinConstructor } from './container/container-mixins/effectsMixin'; import type { FindMixin, FindMixinConstructor } from './container/container-mixins/findMixin'; +import type { GetGlobalMixin } from './container/container-mixins/getGlobalMixin'; import type { MeasureMixin, MeasureMixinConstructor } from './container/container-mixins/measureMixin'; import type { OnRenderMixin, OnRenderMixinConstructor } from './container/container-mixins/onRenderMixin'; import type { SortMixin, SortMixinConstructor } from './container/container-mixins/sortMixin'; @@ -21,6 +22,7 @@ declare global EffectsMixin, FindMixin, SortMixin, + GetGlobalMixin, CacheAsTextureMixin {} // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/src/scene/container/Container.ts b/src/scene/container/Container.ts index 15540f0453..ebcd1e3d64 100644 --- a/src/scene/container/Container.ts +++ b/src/scene/container/Container.ts @@ -11,6 +11,7 @@ import { cacheAsTextureMixin } from './container-mixins/cacheAsTextureMixin'; import { childrenHelperMixin } from './container-mixins/childrenHelperMixin'; import { effectsMixin } from './container-mixins/effectsMixin'; import { findMixin } from './container-mixins/findMixin'; +import { bgr2rgb, getGlobalMixin } from './container-mixins/getGlobalMixin'; import { measureMixin } from './container-mixins/measureMixin'; import { onRenderMixin } from './container-mixins/onRenderMixin'; import { sortMixin } from './container-mixins/sortMixin'; @@ -852,8 +853,6 @@ export class Container extends EventE return this._worldTransform; } - // / ////// transform related stuff - /** * The position of the container on the x axis relative to the local coordinates of the parent. * An alias to position.x @@ -1213,10 +1212,8 @@ export class Container extends EventE */ get tint(): number { - const bgr = this.localColor; // convert bgr to rgb.. - - return ((bgr & 0xFF) << 16) + (bgr & 0xFF00) + ((bgr >> 16) & 0xFF); + return bgr2rgb(this.localColor); } // / //////////////// blend related stuff @@ -1392,3 +1389,4 @@ Container.mixin(findMixin); Container.mixin(sortMixin); Container.mixin(cullingMixin); Container.mixin(cacheAsTextureMixin); +Container.mixin(getGlobalMixin); diff --git a/src/scene/container/container-mixins/getGlobalMixin.ts b/src/scene/container/container-mixins/getGlobalMixin.ts new file mode 100644 index 0000000000..35d361104e --- /dev/null +++ b/src/scene/container/container-mixins/getGlobalMixin.ts @@ -0,0 +1,147 @@ +import { updateTransformBackwards } from '../bounds/getGlobalBounds'; +import { matrixPool } from '../bounds/utils/matrixAndBoundsPool'; +import { multiplyColors } from '../utils/multiplyColors'; + +import type { Matrix } from '../../../maths/matrix/Matrix'; +import type { Container } from '../Container'; + +export function bgr2rgb(color: number): number +{ + return ((color & 0xFF) << 16) + (color & 0xFF00) + ((color >> 16) & 0xFF); +} + +export interface GetGlobalMixin +{ + getGlobalAlpha(skipUpdate: boolean): number; + getGlobalTransform(matrix: Matrix, skipUpdate: boolean): Matrix; + getGlobalTint(skipUpdate?: boolean): number; +} + +export const getGlobalMixin: Partial = { + /** + * Returns the global (compound) alpha of the container within the scene. + * @param skipUpdate - Performance optimization flag: + * - If false (default): Recalculates the entire alpha chain through parents for accuracy + * - If true: Uses cached worldAlpha from the last render pass for better performance + * @returns The resulting alpha value (between 0 and 1) + * @example + * // Accurate but slower - recalculates entire alpha chain + * const preciseAlpha = container.getGlobalAlpha(); + * + * // Faster but may be outdated - uses cached alpha + * const cachedAlpha = container.getGlobalAlpha(true); + */ + getGlobalAlpha(skipUpdate: boolean): number + { + if (skipUpdate) + { + if (this.renderGroup) + { + return this.renderGroup.worldAlpha; + } + + if (this.parentRenderGroup) + { + return this.parentRenderGroup.worldAlpha * this.alpha; + } + + return this.alpha; + } + + let alpha = this.alpha; + let current = this.parent; + + while (current) + { + alpha *= current.alpha; + current = current.parent; + } + + return alpha; + }, + + /** + * Returns the global transform matrix of the container within the scene. + * @param matrix - Optional matrix to store the result. If not provided, a new Matrix will be created. + * @param skipUpdate - Performance optimization flag: + * - If false (default): Recalculates the entire transform chain for accuracy + * - If true: Uses cached worldTransform from the last render pass for better performance + * @returns The resulting transformation matrix (either the input matrix or a new one) + * @example + * // Accurate but slower - recalculates entire transform chain + * const preciseTransform = container.getGlobalTransform(); + * + * // Faster but may be outdated - uses cached transform + * const cachedTransform = container.getGlobalTransform(undefined, true); + * + * // Reuse existing matrix + * const existingMatrix = new Matrix(); + * container.getGlobalTransform(existingMatrix); + */ + getGlobalTransform(matrix: Matrix, skipUpdate: boolean): Matrix + { + if (skipUpdate) + { + return matrix.copyFrom(this.worldTransform); + } + + this.updateLocalTransform(); + + if (!this.parent) + { + return matrix.copyFrom(this.localTransform); + } + + const parentTransform = updateTransformBackwards(this, matrixPool.get().identity()); + + matrix.appendFrom(parentTransform, this.localTransform); + matrixPool.return(parentTransform); + + return matrix; + }, + + /** + * Returns the global (compound) tint color of the container within the scene. + * @param skipUpdate - Performance optimization flag: + * - If false (default): Recalculates the entire tint chain through parents for accuracy + * - If true: Uses cached worldColor from the last render pass for better performance + * @returns The resulting tint color as a 24-bit RGB number (0xRRGGBB) + * @example + * // Accurate but slower - recalculates entire tint chain + * const preciseTint = container.getGlobalTint(); + * + * // Faster but may be outdated - uses cached tint + * const cachedTint = container.getGlobalTint(true); + */ + getGlobalTint(skipUpdate?: boolean): number + { + if (skipUpdate) + { + if (this.renderGroup) + { + return bgr2rgb(this.renderGroup.worldColor); + } + + if (this.parentRenderGroup) + { + return bgr2rgb( + multiplyColors(this.localColor, this.parentRenderGroup.worldColor) + ); + } + + return this.tint; + } + + let color = this.localColor; + let parent = this.parent; + + while (parent) + { + color = multiplyColors(color, parent.localColor); + parent = parent.parent; + } + + return bgr2rgb(color); + } + +} as Container; diff --git a/src/scene/container/container-mixins/toLocalGlobalMixin.ts b/src/scene/container/container-mixins/toLocalGlobalMixin.ts index 993a24a26c..ff35ede44f 100644 --- a/src/scene/container/container-mixins/toLocalGlobalMixin.ts +++ b/src/scene/container/container-mixins/toLocalGlobalMixin.ts @@ -1,6 +1,5 @@ -import { Matrix } from '../../../maths/matrix/Matrix'; import { Point } from '../../../maths/point/Point'; -import { updateTransformBackwards } from '../bounds/getGlobalBounds'; +import { matrixPool } from '../bounds/utils/matrixAndBoundsPool'; import type { PointData } from '../../../maths/point/PointData'; import type { Container } from '../Container'; @@ -46,19 +45,14 @@ export const toLocalGlobalMixin: Partial = { */ toGlobal

(position: PointData, point?: P, skipUpdate = false): P { - if (!skipUpdate) - { - this.updateLocalTransform(); - - const globalMatrix = updateTransformBackwards(this, new Matrix()); + const globalMatrix = this.getGlobalTransform(matrixPool.get(), skipUpdate); - globalMatrix.append(this.localTransform); + // simply apply the matrix.. + point = globalMatrix.apply(position, point); - return globalMatrix.apply

(position, point); - } + matrixPool.return(globalMatrix); - // simply apply the matrix.. - return this.worldTransform.apply

(position, point); + return point; }, /** @@ -78,18 +72,13 @@ export const toLocalGlobalMixin: Partial = { position = from.toGlobal(position, point, skipUpdate); } - if (!skipUpdate) - { - this.updateLocalTransform(); - - const globalMatrix = updateTransformBackwards(this, new Matrix()); + const globalMatrix = this.getGlobalTransform(matrixPool.get(), skipUpdate); - globalMatrix.append(this.localTransform); + // simply apply the matrix.. + point = globalMatrix.applyInverse(position, point); - return globalMatrix.applyInverse

(position, point); - } + matrixPool.return(globalMatrix); - // simply apply the matrix.. - return this.worldTransform.applyInverse

(position, point); + return point; } } as Container; diff --git a/src/scene/particle-container/shared/Particle.ts b/src/scene/particle-container/shared/Particle.ts index 350b69f756..3936514355 100644 --- a/src/scene/particle-container/shared/Particle.ts +++ b/src/scene/particle-container/shared/Particle.ts @@ -1,5 +1,6 @@ import { Color } from '../../../color/Color'; import { Texture } from '../../../rendering/renderers/shared/texture/Texture'; +import { bgr2rgb } from '../../container/container-mixins/getGlobalMixin'; import { assignWithIgnore } from '../../container/utils/assignWithIgnore'; import type { ColorSource } from '../../../color/Color'; @@ -140,9 +141,7 @@ export class Particle implements IParticle /** Gets or sets the tint color of the particle. */ get tint(): number { - const bgr = this._tint; - - return ((bgr & 0xFF) << 16) + (bgr & 0xFF00) + ((bgr >> 16) & 0xFF); + return bgr2rgb(this._tint); } set tint(value: ColorSource) diff --git a/tests/scene/getGlobalAlpha.tests.ts b/tests/scene/getGlobalAlpha.tests.ts new file mode 100644 index 0000000000..ebd05a74f0 --- /dev/null +++ b/tests/scene/getGlobalAlpha.tests.ts @@ -0,0 +1,118 @@ +import { Container } from '../../src/scene/container/Container'; + +describe('getGlobalAlpha', () => +{ + describe('with skipUpdateTransform = false', () => + { + it('should return container alpha when no parent exists', () => + { + const container = new Container(); + + container.alpha = 0.5; + + expect(container.getGlobalAlpha(false)).toBe(0.5); + }); + + it('should multiply alpha with single parent', () => + { + const parent = new Container(); + const container = new Container(); + + parent.alpha = 0.5; + + container.alpha = 0.5; + container.parent = parent; + + expect(container.getGlobalAlpha(false)).toBe(0.25); // 0.5 * 0.5 + }); + + it('should multiply alpha through multiple parents', () => + { + const container = new Container(); + const grandParent = new Container(); + const parent = new Container(); + + grandParent.alpha = 0.5; + parent.alpha = 0.5; + container.alpha = 0.5; + + parent.parent = grandParent; + container.parent = parent; + + expect(container.getGlobalAlpha(false)).toBe(0.125); // 0.5 * 0.5 * 0.5 + }); + + it('should return renderGroup worldAlpha when container has renderGroup', () => + { + const container = new Container({ + alpha: 0.75, + isRenderGroup: true + }); + + expect(container.getGlobalAlpha(false)).toBe(0.75); + }); + }); + + describe('with skipUpdateTransform = true', () => + { + it('should multiply parentRenderGroup worldAlpha with container alpha', () => + { + const parent = new Container(); + + const container = new Container(); + + container.alpha = 0.5; + + parent.addChild(container); + + parent.alpha = 0.5; + + expect(container.getGlobalAlpha(true)).toBe(0.5); // 0.8 * 0.5 + }); + }); + + describe('edge cases', () => + { + it('should handle alpha value of 0', () => + { + const container = new Container(); + + container.alpha = 0; + + expect(container.getGlobalAlpha(false)).toBe(0); + }); + + it('should handle alpha value of 1', () => + { + const container = new Container(); + + container.alpha = 1; + + expect(container.getGlobalAlpha(false)).toBe(1); + }); + + it('should handle deeply nested containers', () => + { + const container = new Container(); + let current = new Container(); + + // Create a chain of 10 containers + for (let i = 0; i < 10; i++) + { + const parent = new Container(); + + parent.alpha = 0.9; + current.addChild(parent); + current = parent; + } + + container.alpha = 0.9; + + current.addChild(container); + + const expectedAlpha = Math.pow(0.9, 11); + + expect(container.getGlobalAlpha(false)).toBeCloseTo(expectedAlpha, 3); + }); + }); +}); diff --git a/tests/scene/getGlobalTint.tests.ts b/tests/scene/getGlobalTint.tests.ts new file mode 100644 index 0000000000..cf0ea2a4f4 --- /dev/null +++ b/tests/scene/getGlobalTint.tests.ts @@ -0,0 +1,134 @@ +import { Container } from '../../src/scene/container/Container'; + +describe('getGlobalTint', () => +{ + describe('with skipUpdate = false', () => + { + it('should return container tint when no parent exists', () => + { + const container = new Container(); + + container.tint = 0xFF0000; // Red + + expect(container.getGlobalTint(false)).toBe(0xFF0000); + }); + + it('should multiply tint with single parent', () => + { + const parent = new Container(); + const container = new Container(); + + parent.tint = 0xFF0000; // Red + container.tint = 0x00FF00; // Green + + container.parent = parent; + + // Result should be black (0x000000) as multiplying different colors results in darkness + expect(container.getGlobalTint(false)).toBe(0x000000); + }); + + it('should multiply tint through multiple parents', () => + { + const container = new Container(); + const grandParent = new Container(); + const parent = new Container(); + + grandParent.tint = 0xFFFFFF; // White + parent.tint = 0x808080; // Gray + container.tint = 0xFF0000; // Red + + parent.parent = grandParent; + container.parent = parent; + + // Result should be darker red due to gray parent + expect(container.getGlobalTint(false)).toBe(0x800000); + }); + + it('should return renderGroup worldColor when container has renderGroup', () => + { + const container = new Container({ + tint: 0xFF0000, + isRenderGroup: true + }); + + expect(container.getGlobalTint(false)).toBe(0xFF0000); + }); + }); + + describe('with skipUpdate = true', () => + { + it('should return renderGroup worldColor when container has renderGroup', () => + { + const container = new Container(); + + container.renderGroup = { + worldColor: 0x0000FF // BGR format + } as any; + + expect(container.getGlobalTint(true)).toBe(0xFF0000); // RGB format + }); + + it('should multiply parentRenderGroup worldColor with container localColor', () => + { + const container = new Container(); + + container.tint = 0xFF0000; // Red + container.parentRenderGroup = { + worldColor: 0xFFFFFF // White in BGR + } as any; + + expect(container.getGlobalTint(true)).toBe(0xFF0000); + }); + + it('should return container tint when no render groups exist', () => + { + const container = new Container(); + + container.tint = 0x00FF00; // Green + + expect(container.getGlobalTint(true)).toBe(0x00FF00); + }); + }); + + describe('edge cases', () => + { + it('should handle black tint (0x000000)', () => + { + const container = new Container(); + + container.tint = 0x000000; + + expect(container.getGlobalTint(false)).toBe(0x000000); + }); + + it('should handle white tint (0xFFFFFF)', () => + { + const container = new Container(); + + container.tint = 0xFFFFFF; + + expect(container.getGlobalTint(false)).toBe(0xFFFFFF); + }); + + it('should handle deeply nested containers', () => + { + const container = new Container(); + let current = new Container(); + + // Create a chain of 10 containers + for (let i = 0; i < 10; i++) + { + const parent = new Container(); + + parent.tint = 0xFFFFFF; // White doesn't affect the chain + current.addChild(parent); + current = parent; + } + + container.tint = 0xFF0000; // Red + current.addChild(container); + + expect(container.getGlobalTint(false)).toBe(0xFF0000); + }); + }); +}); diff --git a/tests/scene/getGlobalTransform.tests.ts b/tests/scene/getGlobalTransform.tests.ts new file mode 100644 index 0000000000..13c14563df --- /dev/null +++ b/tests/scene/getGlobalTransform.tests.ts @@ -0,0 +1,178 @@ +import { Matrix } from '../../src/maths/matrix/Matrix'; +import { Container } from '../../src/scene/container/Container'; + +describe('getGlobalTransform', () => +{ + let outputMatrix: Matrix; + + beforeEach(() => + { + outputMatrix = new Matrix(); + }); + + describe('with skipUpdate = false', () => + { + it('should return local transform when no parent exists', () => + { + const container = new Container(); + + container.x = 100; + container.y = 100; + + const result = container.getGlobalTransform(outputMatrix, false); + + expect(result).toBe(outputMatrix); + expect(result.tx).toBe(100); + expect(result.ty).toBe(100); + }); + + it('should combine transforms with single parent', () => + { + const parent = new Container(); + const container = new Container(); + + parent.x = 100; + container.x = 50; + + parent.addChild(container); + + const result = container.getGlobalTransform(outputMatrix, false); + + expect(result.tx).toBe(150); // 100 + 50 + }); + + it('should combine transforms through multiple parents', () => + { + const grandParent = new Container(); + const parent = new Container(); + const container = new Container(); + + grandParent.x = 100; + parent.x = 100; + container.x = 100; + + parent.parent = grandParent; + grandParent.addChild(parent); + parent.addChild(container); + + const result = container.getGlobalTransform(outputMatrix, false); + + expect(result.tx).toBe(300); // 100 + 100 + 100 + }); + + it('should handle rotation', () => + { + const parent = new Container(); + const container = new Container(); + + parent.rotation = Math.PI / 2; // 90 degrees + container.x = 100; + + parent.addChild(container); + + const result = container.getGlobalTransform(outputMatrix, false); + + // After 90-degree rotation + expect(result.a).toBeCloseTo(0); + expect(result.b).toBeCloseTo(1); + expect(result.c).toBeCloseTo(-1); + expect(result.d).toBeCloseTo(0); + expect(result.tx).toBeCloseTo(100); + expect(result.ty).toBeCloseTo(0); + }); + + it('should handle scale', () => + { + const parent = new Container(); + const container = new Container(); + + container.scale.x = 2; + container.scale.y = 3; + + container.addChild(parent); + + const result = container.getGlobalTransform(outputMatrix, false); + + expect(result.a).toBe(2); + expect(result.d).toBe(3); + }); + }); + + describe('with skipUpdate = true', () => + { + it('should return worldTransform directly', () => + { + const container = new Container(); + + // Manually set worldTransform + container.worldTransform.tx = 200; + container.worldTransform.ty = 300; + + const result = container.getGlobalTransform(outputMatrix, true); + + expect(result.tx).toBe(200); + expect(result.ty).toBe(300); + }); + + it('should copy to provided matrix', () => + { + const container = new Container(); + const outMatrix = new Matrix(); + + container.worldTransform.tx = 100; + + const result = container.getGlobalTransform(outMatrix, true); + + expect(result).toBe(outMatrix); + expect(result.tx).toBe(100); + }); + }); + + describe('edge cases', () => + { + it('should handle identity transforms', () => + { + const container = new Container(); + + const result = container.getGlobalTransform(outputMatrix, false); + + expect(result.a).toBe(1); + expect(result.d).toBe(1); + expect(result.tx).toBe(0); + expect(result.ty).toBe(0); + }); + + it('should handle scale of 0', () => + { + const container = new Container(); + + container.scale.x = 0; + container.scale.y = 0; + + const result = container.getGlobalTransform(outputMatrix, false); + + expect(result.a).toBe(0); + expect(result.d).toBe(0); + }); + + it('should handle deeply nested containers', () => + { + const container = new Container(); + let current = container; + + // Create a chain of 10 containers + for (let i = 0; i < 10; i++) + { + const parent = new Container(); + + parent.x = 10; // Each adds 10 to x + parent.addChild(current); + current = parent; + } + + const result = container.getGlobalTransform(outputMatrix, false); + + expect(result.tx).toBe(100); // 10 * 10 + }); + }); +});