diff --git a/examples/tests/stress-multi-level-clipping.ts b/examples/tests/stress-multi-level-clipping.ts new file mode 100644 index 00000000..42085cc0 --- /dev/null +++ b/examples/tests/stress-multi-level-clipping.ts @@ -0,0 +1,93 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Comcast Cable Communications Management, LLC. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { type INode } from '@lightningjs/renderer'; +import logo from '../assets/lightning.png'; +import type { ExampleSettings } from '../common/ExampleSettings.js'; +import robotImg from '../assets/robot/robot.png'; + +const randomIntBetween = (from: number, to: number) => + Math.floor(Math.random() * (to - from + 1) + from); + +export default async function ({ + renderer, + testRoot, + perfMultiplier, +}: ExampleSettings) { + // create nodes + const numOuterNodes = 100 * perfMultiplier; + const nodes: INode[] = []; + let totalNodes = 0; + + const bg = renderer.createNode({ + width: 1920, + height: 1080, + color: 0xff1e293b, + parent: testRoot, + }); + + for (let i = 0; i < numOuterNodes; i++) { + const container = renderer.createNode({ + x: Math.random() * 1920, + y: Math.random() * 1080, + width: 100, + height: 100, + clipping: true, + parent: bg, + }); + const node = renderer.createNode({ + mount: 0.5, + x: 50, + y: 50, + width: 200, + height: 200, + src: robotImg, + parent: container, + }); + + nodes.push(container); + totalNodes += 2; + } + + console.log( + `Created ${numOuterNodes} clipping outer nodes with an image node nested inside. Total nodes: ${totalNodes}`, + ); + + // create animations + const animate = () => { + nodes.forEach((node) => { + node + .animate( + { + x: randomIntBetween(20, 1740), + y: randomIntBetween(20, 900), + }, + { + duration: 3000, + easing: 'ease-out', + loop: true, + stopMethod: 'reverse', + }, + ) + .start(); + }); + }; + + animate(); +} diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index bb435dd5..ece81ed9 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -37,7 +37,7 @@ import type { NodeTextureLoadedPayload, } from '../common/CommonTypes.js'; import { EventEmitter } from '../common/EventEmitter.js'; -import { intersectRect, type Rect } from './lib/utils.js'; +import { copyRect, intersectRect, type RectWithValid } from './lib/utils.js'; import { Matrix3d } from './lib/Matrix3d.js'; export interface CoreNodeProps { @@ -150,7 +150,13 @@ export class CoreNode extends EventEmitter implements ICoreNode { public globalTransform?: Matrix3d; public scaleRotateTransform?: Matrix3d; public localTransform?: Matrix3d; - public clippingRect: Rect | null = null; + public clippingRect: RectWithValid = { + x: 0, + y: 0, + width: 0, + height: 0, + valid: false, + }; public isRenderable = false; public worldAlpha = 1; public premultipliedColorTl = 0; @@ -295,7 +301,7 @@ export class CoreNode extends EventEmitter implements ICoreNode { * @todo: test for correct calculation flag * @param delta */ - update(delta: number, parentClippingRect: Rect | null = null): void { + update(delta: number, parentClippingRect: RectWithValid): void { if (this.updateType & UpdateType.ScaleRotate) { this.updateScaleRotateTransform(); this.setUpdateType(UpdateType.Local); @@ -477,28 +483,31 @@ export class CoreNode extends EventEmitter implements ICoreNode { * * Finally, the node's parentClippingRect and clippingRect properties are updated. */ - calculateClippingRect(parentClippingRect: Rect | null = null) { + calculateClippingRect(parentClippingRect: RectWithValid) { assertTruthy(this.globalTransform); + const { clippingRect, props, globalTransform: gt } = this; + const { clipping } = props; - const gt = this.globalTransform; const isRotated = gt.tb !== 0 || gt.tc !== 0; - let clippingRect: Rect | null = - this.props.clipping && !isRotated - ? { - x: gt.tx, - y: gt.ty, - width: this.width * gt.ta, - height: this.height * gt.td, - } - : null; - if (parentClippingRect && clippingRect) { - clippingRect = intersectRect(parentClippingRect, clippingRect); - } else if (parentClippingRect) { - clippingRect = parentClippingRect; - } - - this.clippingRect = clippingRect; + if (clipping && !isRotated) { + clippingRect.x = gt.tx; + clippingRect.y = gt.ty; + clippingRect.width = this.width * gt.ta; + clippingRect.height = this.height * gt.td; + clippingRect.valid = true; + } else { + clippingRect.valid = false; + } + + if (parentClippingRect.valid && clippingRect.valid) { + // Intersect parent clipping rect with node clipping rect + intersectRect(parentClippingRect, clippingRect, clippingRect); + } else if (parentClippingRect.valid) { + // Copy parent clipping rect + copyRect(parentClippingRect, clippingRect); + clippingRect.valid = true; + } } calculateZIndex(): void { diff --git a/src/core/CoreTextNode.ts b/src/core/CoreTextNode.ts index 0e6362f0..cf4cb466 100644 --- a/src/core/CoreTextNode.ts +++ b/src/core/CoreTextNode.ts @@ -32,7 +32,7 @@ import type { NodeTextFailedPayload, NodeTextLoadedPayload, } from '../common/CommonTypes.js'; -import type { Rect } from './lib/utils.js'; +import type { Rect, RectWithValid } from './lib/utils.js'; import { assertTruthy } from '../utils.js'; export interface CoreTextNodeProps extends CoreNodeProps, TrProps { @@ -318,7 +318,7 @@ export class CoreTextNode extends CoreNode implements ICoreTextNode { this.textRenderer.set.debug(this.trState, value); } - override update(delta: number, parentClippingRect: Rect | null = null) { + override update(delta: number, parentClippingRect: RectWithValid) { super.update(delta, parentClippingRect); assertTruthy(this.globalTransform); diff --git a/src/core/Stage.ts b/src/core/Stage.ts index 7a5e1769..8ba4787a 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -210,7 +210,7 @@ export class Stage extends EventEmitter { // Update tree if needed if (this.root.updateType !== 0) { - this.root.update(this.deltaTime); + this.root.update(this.deltaTime, this.root.clippingRect); } // test if we need to update the scene diff --git a/src/core/lib/utils.ts b/src/core/lib/utils.ts index 4806492b..b182e636 100644 --- a/src/core/lib/utils.ts +++ b/src/core/lib/utils.ts @@ -81,6 +81,10 @@ export interface Rect { height: number; } +export interface RectWithValid extends Rect { + valid: boolean; +} + export interface Bound { x1: number; y1: number; @@ -132,12 +136,25 @@ export function intersectBound( return createBound(0, 0, 0, 0, intersection); } -export function intersectRect(a: Rect, b: Rect): Rect { +export function intersectRect(a: Rect, b: Rect): Rect; +export function intersectRect( + a: Rect, + b: Rect, + out: T, +): T; +export function intersectRect(a: Rect, b: Rect, out?: Rect): Rect { const x = Math.max(a.x, b.x); const y = Math.max(a.y, b.y); const width = Math.min(a.x + a.width, b.x + b.width) - x; const height = Math.min(a.y + a.height, b.y + b.height) - y; if (width > 0 && height > 0) { + if (out) { + out.x = x; + out.y = y; + out.width = width; + out.height = height; + return out; + } return { x, y, @@ -145,6 +162,13 @@ export function intersectRect(a: Rect, b: Rect): Rect { height, }; } + if (out) { + out.x = 0; + out.y = 0; + out.width = 0; + out.height = 0; + return out; + } return { x: 0, y: 0, @@ -153,6 +177,24 @@ export function intersectRect(a: Rect, b: Rect): Rect { }; } +export function copyRect(a: Rect): Rect; +export function copyRect(a: Rect, out: T): T; +export function copyRect(a: Rect, out?: Rect): Rect { + if (out) { + out.x = a.x; + out.y = a.y; + out.width = a.width; + out.height = a.height; + return out; + } + return { + x: a.x, + y: a.y, + width: a.width, + height: a.height, + }; +} + export function compareRect(a: Rect | null, b: Rect | null): boolean { if (a === b) { return true; diff --git a/src/core/renderers/CoreRenderer.ts b/src/core/renderers/CoreRenderer.ts index e0e1d9d5..0a56a333 100644 --- a/src/core/renderers/CoreRenderer.ts +++ b/src/core/renderers/CoreRenderer.ts @@ -20,7 +20,7 @@ import type { CoreShaderManager } from '../CoreShaderManager.js'; import type { TextureOptions } from '../CoreTextureManager.js'; import type { Stage } from '../Stage.js'; -import type { Rect } from '../lib/utils.js'; +import type { Rect, RectWithValid } from '../lib/utils.js'; import type { Texture } from '../textures/Texture.js'; import { CoreContextTexture } from './CoreContextTexture.js'; import type { CoreRenderOp } from './CoreRenderOp.js'; @@ -39,7 +39,7 @@ export interface QuadOptions { shader: CoreShader | null; shaderProps: Record | null; alpha: number; - clippingRect: Rect | null; + clippingRect: RectWithValid; tx: number; ty: number; ta: number; diff --git a/src/core/renderers/webgl/WebGlCoreRenderOp.ts b/src/core/renderers/webgl/WebGlCoreRenderOp.ts index 73ffb971..eec02256 100644 --- a/src/core/renderers/webgl/WebGlCoreRenderOp.ts +++ b/src/core/renderers/webgl/WebGlCoreRenderOp.ts @@ -23,7 +23,7 @@ import type { WebGlCoreCtxTexture } from './WebGlCoreCtxTexture.js'; import type { WebGlCoreRendererOptions } from './WebGlCoreRenderer.js'; import type { BufferCollection } from './internal/BufferCollection.js'; import type { Dimensions } from '../../../common/CommonTypes.js'; -import type { Rect } from '../../lib/utils.js'; +import type { Rect, RectWithValid } from '../../lib/utils.js'; import type { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js'; const MAX_TEXTURES = 8; // TODO: get from gl @@ -45,7 +45,7 @@ export class WebGlCoreRenderOp extends CoreRenderOp { readonly shader: WebGlCoreShader, readonly shaderProps: Record, readonly alpha: number, - readonly clippingRect: Rect | null, + readonly clippingRect: RectWithValid, readonly dimensions: Dimensions, readonly bufferIdx: number, readonly zIndex: number, @@ -82,7 +82,7 @@ export class WebGlCoreRenderOp extends CoreRenderOp { const quadIdx = (this.bufferIdx / 24) * 6 * 2; // Clipping - if (this.clippingRect) { + if (this.clippingRect.valid) { const { x, y, width, height } = this.clippingRect; const pixelRatio = options.pixelRatio; const canvasHeight = options.canvas.height; diff --git a/src/core/renderers/webgl/WebGlCoreRenderer.ts b/src/core/renderers/webgl/WebGlCoreRenderer.ts index a4ae5472..40304418 100644 --- a/src/core/renderers/webgl/WebGlCoreRenderer.ts +++ b/src/core/renderers/webgl/WebGlCoreRenderer.ts @@ -50,6 +50,7 @@ import { compareRect, getNormalizedRgbaComponents, type Rect, + type RectWithValid, } from '../../lib/utils.js'; import type { Dimensions } from '../../../common/CommonTypes.js'; import { WebGlCoreShader } from './WebGlCoreShader.js'; @@ -412,7 +413,7 @@ export class WebGlCoreRenderer extends CoreRenderer { shaderProps: Record, alpha: number, dimensions: Dimensions, - clippingRect: Rect | null, + clippingRect: RectWithValid, bufferIdx: number, ) { const curRenderOp = new WebGlCoreRenderOp( diff --git a/src/core/text-rendering/renderers/CanvasTextRenderer.ts b/src/core/text-rendering/renderers/CanvasTextRenderer.ts index 63cdbbfb..744ba5e5 100644 --- a/src/core/text-rendering/renderers/CanvasTextRenderer.ts +++ b/src/core/text-rendering/renderers/CanvasTextRenderer.ts @@ -30,6 +30,7 @@ import { getNormalizedAlphaComponent, type BoundWithValid, createBound, + type RectWithValid, } from '../../lib/utils.js'; import type { ImageTexture } from '../../textures/ImageTexture.js'; import type { TrFontFace } from '../font-face-types/TrFontFace.js'; @@ -524,7 +525,7 @@ export class CanvasTextRenderer extends TextRenderer { override renderQuads( state: CanvasTextRendererState, transform: Matrix3d, - clippingRect: Rect | null, + clippingRect: RectWithValid, alpha: number, ): void { const { stage } = this; diff --git a/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.ts b/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.ts index a60c7091..2f8c5495 100644 --- a/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.ts +++ b/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.ts @@ -25,6 +25,8 @@ import { type BoundWithValid, intersectRect, isBoundPositive, + type RectWithValid, + copyRect, } from '../../../lib/utils.js'; import { TextRenderer, @@ -86,6 +88,8 @@ export interface SdfTextRendererState extends TextRendererState { visibleWindow: BoundWithValid; + clippingRect: RectWithValid; + bufferNumFloats: number; bufferNumQuads: number; @@ -311,6 +315,13 @@ export class SdfTextRenderer extends TextRenderer { y2: 0, valid: false, }, + clippingRect: { + x: 0, + y: 0, + width: 0, + height: 0, + valid: false, + }, bufferNumFloats: 0, bufferNumQuads: 0, vertexBuffer: undefined, @@ -531,7 +542,7 @@ export class SdfTextRenderer extends TextRenderer { override renderQuads( state: SdfTextRendererState, transform: Matrix3d, - clippingRect: Rect | null, + clippingRect: Readonly, alpha: number, ): void { if (!state.vertexBuffer) { @@ -608,9 +619,17 @@ export class SdfTextRenderer extends TextRenderer { height: state.visibleWindow.y2 - state.visibleWindow.y1, }; - clippingRect = clippingRect - ? intersectRect(clippingRect, visibleWindowRect) - : visibleWindowRect; + if (clippingRect.valid) { + state.clippingRect.valid = true; + clippingRect = intersectRect( + clippingRect, + visibleWindowRect, + state.clippingRect, + ); + } else { + state.clippingRect.valid = true; + clippingRect = copyRect(visibleWindowRect, state.clippingRect); + } } const renderOp = new WebGlCoreRenderOp( diff --git a/src/core/text-rendering/renderers/TextRenderer.ts b/src/core/text-rendering/renderers/TextRenderer.ts index 04ceabb5..e6382d55 100644 --- a/src/core/text-rendering/renderers/TextRenderer.ts +++ b/src/core/text-rendering/renderers/TextRenderer.ts @@ -20,7 +20,7 @@ import type { EventEmitter } from '../../../common/EventEmitter.js'; import type { Stage } from '../../Stage.js'; import type { Matrix3d } from '../../lib/Matrix3d.js'; -import type { Rect } from '../../lib/utils.js'; +import type { Rect, RectWithValid } from '../../lib/utils.js'; import type { TrFontFace, TrFontFaceDescriptors, @@ -498,7 +498,7 @@ export abstract class TextRenderer< abstract renderQuads( state: StateT, transform: Matrix3d, - clippingRect: Rect | null, + clippingRect: RectWithValid, alpha: number, ): void; }