UNPKG

@lightningjs/renderer

Version:
1,366 lines 55.3 kB
/* * 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 { assertTruthy, getNewId, mergeColorAlphaPremultiplied, } from '../utils.js'; import {} from './textures/Texture.js'; import { EventEmitter } from '../common/EventEmitter.js'; import { copyRect, intersectRect, createBound, boundInsideBound, boundLargeThanBound, createPreloadBounds, } from './lib/utils.js'; import { Matrix3d } from './lib/Matrix3d.js'; import { RenderCoords } from './lib/RenderCoords.js'; import { CoreAnimation } from './animations/CoreAnimation.js'; import { CoreAnimationController } from './animations/CoreAnimationController.js'; export var CoreNodeRenderState; (function (CoreNodeRenderState) { CoreNodeRenderState[CoreNodeRenderState["Init"] = 0] = "Init"; CoreNodeRenderState[CoreNodeRenderState["OutOfBounds"] = 2] = "OutOfBounds"; CoreNodeRenderState[CoreNodeRenderState["InBounds"] = 4] = "InBounds"; CoreNodeRenderState[CoreNodeRenderState["InViewport"] = 8] = "InViewport"; })(CoreNodeRenderState || (CoreNodeRenderState = {})); const CoreNodeRenderStateMap = new Map(); CoreNodeRenderStateMap.set(CoreNodeRenderState.Init, 'init'); CoreNodeRenderStateMap.set(CoreNodeRenderState.OutOfBounds, 'outOfBounds'); CoreNodeRenderStateMap.set(CoreNodeRenderState.InBounds, 'inBounds'); CoreNodeRenderStateMap.set(CoreNodeRenderState.InViewport, 'inViewport'); export var UpdateType; (function (UpdateType) { /** * Child updates */ UpdateType[UpdateType["Children"] = 1] = "Children"; /** * localTransform * * @remarks * CoreNode Properties Updated: * - `localTransform` */ UpdateType[UpdateType["Local"] = 2] = "Local"; /** * globalTransform * * * @remarks * CoreNode Properties Updated: * - `globalTransform` * - `renderBounds` * - `renderCoords` */ UpdateType[UpdateType["Global"] = 4] = "Global"; /** * Clipping rect update * * @remarks * CoreNode Properties Updated: * - `clippingRect` */ UpdateType[UpdateType["Clipping"] = 8] = "Clipping"; /** * Calculated ZIndex update * * @remarks * CoreNode Properties Updated: * - `calcZIndex` */ UpdateType[UpdateType["CalculatedZIndex"] = 16] = "CalculatedZIndex"; /** * Z-Index Sorted Children update * * @remarks * CoreNode Properties Updated: * - `children` (sorts children by their `calcZIndex`) */ UpdateType[UpdateType["ZIndexSortedChildren"] = 32] = "ZIndexSortedChildren"; /** * Premultiplied Colors update * * @remarks * CoreNode Properties Updated: * - `premultipliedColorTl` * - `premultipliedColorTr` * - `premultipliedColorBl` * - `premultipliedColorBr` */ UpdateType[UpdateType["PremultipliedColors"] = 64] = "PremultipliedColors"; /** * World Alpha update * * @remarks * CoreNode Properties Updated: * - `worldAlpha` = `parent.worldAlpha` * `alpha` */ UpdateType[UpdateType["WorldAlpha"] = 128] = "WorldAlpha"; /** * Render State update * * @remarks * CoreNode Properties Updated: * - `renderState` */ UpdateType[UpdateType["RenderState"] = 256] = "RenderState"; /** * Is Renderable update * * @remarks * CoreNode Properties Updated: * - `isRenderable` */ UpdateType[UpdateType["IsRenderable"] = 512] = "IsRenderable"; /** * Render Texture update */ UpdateType[UpdateType["RenderTexture"] = 1024] = "RenderTexture"; /** * Track if parent has render texture */ UpdateType[UpdateType["ParentRenderTexture"] = 2048] = "ParentRenderTexture"; /** * Render Bounds update */ UpdateType[UpdateType["RenderBounds"] = 4096] = "RenderBounds"; /** * RecalcUniforms */ UpdateType[UpdateType["RecalcUniforms"] = 8192] = "RecalcUniforms"; /** * None */ UpdateType[UpdateType["None"] = 0] = "None"; /** * All */ UpdateType[UpdateType["All"] = 14335] = "All"; })(UpdateType || (UpdateType = {})); /** * A visual Node in the Renderer scene graph. * * @remarks * CoreNode is an internally used class that represents a Renderer Node in the * scene graph. See INode.ts for the public APIs exposed to Renderer users * that include generic types for Shaders. */ export class CoreNode extends EventEmitter { stage; children = []; _id = getNewId(); props; hasShaderUpdater = false; hasColorProps = false; updateType = UpdateType.All; childUpdateType = UpdateType.None; globalTransform; localTransform; sceneGlobalTransform; renderCoords; sceneRenderCoords; renderBound; strictBound; preloadBound; clippingRect = { x: 0, y: 0, width: 0, height: 0, valid: false, }; textureCoords; updateTextureCoords = false; isRenderable = false; renderState = CoreNodeRenderState.Init; worldAlpha = 1; premultipliedColorTl = 0; premultipliedColorTr = 0; premultipliedColorBl = 0; premultipliedColorBr = 0; calcZIndex = 0; hasRTTupdates = false; parentHasRenderTexture = false; rttParent = null; /** * only used when rtt = true */ framebufferDimensions = null; destroyed = false; constructor(stage, props) { super(); this.stage = stage; const p = (this.props = {}); // Fast-path assign only known keys p.x = props.x; p.y = props.y; p.width = props.width; p.height = props.height; p.alpha = props.alpha; p.autosize = props.autosize; p.clipping = props.clipping; p.color = props.color; p.colorTop = props.colorTop; p.colorBottom = props.colorBottom; p.colorLeft = props.colorLeft; p.colorRight = props.colorRight; p.colorTl = props.colorTl; p.colorTr = props.colorTr; p.colorBl = props.colorBl; p.colorBr = props.colorBr; p.scaleX = props.scaleX; p.scaleY = props.scaleY; p.rotation = props.rotation; p.pivotX = props.pivotX; p.pivotY = props.pivotY; p.mountX = props.mountX; p.mountY = props.mountY; p.mount = props.mount; p.pivot = props.pivot; p.strictBounds = props.strictBounds; p.zIndex = props.zIndex; p.zIndexLocked = props.zIndexLocked; p.textureOptions = props.textureOptions; p.data = props.data; p.imageType = props.imageType; p.srcX = props.srcX; p.srcY = props.srcY; p.srcWidth = props.srcWidth; p.srcHeight = props.srcHeight; p.parent = null; p.texture = null; p.shader = null; p.src = null; p.rtt = false; p.boundsMargin = null; // Assign props to instances this.parent = props.parent; this.texture = props.texture; this.shader = props.shader; this.src = props.src; this.rtt = props.rtt; this.boundsMargin = props.boundsMargin; this.interactive = props.interactive; this.setUpdateType(UpdateType.Local | UpdateType.RenderBounds | UpdateType.RenderState); // if the default texture isn't loaded yet, wait for it to load // this only happens when the node is created before the stage is ready const dt = this.stage.defaultTexture; if (dt !== null && dt.state !== 'loaded') { dt.once('loaded', () => this.setUpdateType(UpdateType.IsRenderable)); } } //#region Textures loadTexture() { const { texture } = this.props; if (!texture) { return; } // If texture is already loaded / failed, trigger loaded event manually // so that users get a consistent event experience. // We do this in a microtask to allow listeners to be attached in the same // synchronous task after calling loadTexture() queueMicrotask(() => { if (this.textureOptions.preload === true) { this.stage.txManager.loadTexture(texture); } texture.preventCleanup = this.props.textureOptions?.preventCleanup ?? false; texture.on('loaded', this.onTextureLoaded); texture.on('failed', this.onTextureFailed); texture.on('freed', this.onTextureFreed); // If the parent is a render texture, the initial texture status // will be set to freed until the texture is processed by the // Render RTT nodes. So we only need to listen fo changes and // no need to check the texture.state until we restructure how // textures are being processed. if (this.parentHasRenderTexture) { this.notifyParentRTTOfUpdate(); return; } if (texture.state === 'loaded') { this.onTextureLoaded(texture, texture.dimensions); } else if (texture.state === 'failed') { this.onTextureFailed(texture, texture.error); } else if (texture.state === 'freed') { this.onTextureFreed(texture); } }); } unloadTexture() { if (this.texture === null) { return; } const texture = this.texture; texture.off('loaded', this.onTextureLoaded); texture.off('failed', this.onTextureFailed); texture.off('freed', this.onTextureFreed); texture.setRenderableOwner(this, false); } autosizeNode(dimensions) { if (this.autosize) { this.width = dimensions.width; this.height = dimensions.height; } } onTextureLoaded = (_, dimensions) => { this.autosizeNode(dimensions); this.setUpdateType(UpdateType.IsRenderable); // Texture was loaded. In case the RAF loop has already stopped, we request // a render to ensure the texture is rendered. this.stage.requestRender(); // If parent has a render texture, flag that we need to update if (this.parentHasRenderTexture) { this.notifyParentRTTOfUpdate(); } // ignore 1x1 pixel textures if (dimensions.width > 1 && dimensions.height > 1) { this.emit('loaded', { type: 'texture', dimensions, }); } // Trigger a local update if the texture is loaded and the resizeMode is 'contain' if (this.props.textureOptions?.resizeMode?.type === 'contain') { this.setUpdateType(UpdateType.Local); } }; onTextureFailed = (_, error) => { // immediately set isRenderable to false, so that we handle the error // without waiting for the next frame loop this.isRenderable = false; this.setUpdateType(UpdateType.IsRenderable); // If parent has a render texture, flag that we need to update if (this.parentHasRenderTexture) { this.notifyParentRTTOfUpdate(); } this.emit('failed', { type: 'texture', error, }); }; onTextureFreed = () => { // immediately set isRenderable to false, so that we handle the error // without waiting for the next frame loop this.isRenderable = false; this.setUpdateType(UpdateType.IsRenderable); // If parent has a render texture, flag that we need to update if (this.parentHasRenderTexture) { this.notifyParentRTTOfUpdate(); } this.emit('freed', { type: 'texture', }); }; //#endregion Textures /** * Change types types is used to determine the scope of the changes being applied * * @remarks * See {@link UpdateType} for more information on each type * * @param type */ setUpdateType(type) { this.updateType |= type; const parent = this.props.parent; if (!parent) return; parent.setUpdateType(UpdateType.Children); } sortChildren() { this.children.sort((a, b) => a.calcZIndex - b.calcZIndex); } updateLocalTransform() { const p = this.props; const { x, y, width, height } = p; const mountTranslateX = p.mountX * width; const mountTranslateY = p.mountY * height; if (p.rotation !== 0 || p.scaleX !== 1 || p.scaleY !== 1) { const pivotTranslateX = p.pivotX * width; const pivotTranslateY = p.pivotY * height; this.localTransform = Matrix3d.translate(x - mountTranslateX + pivotTranslateX, y - mountTranslateY + pivotTranslateY, this.localTransform) .rotate(p.rotation) .scale(p.scaleX, p.scaleY) .translate(-pivotTranslateX, -pivotTranslateY); } else { this.localTransform = Matrix3d.translate(x - mountTranslateX, y - mountTranslateY, this.localTransform); } // Handle 'contain' resize mode const texture = p.texture; if (texture && texture.dimensions && p.textureOptions.resizeMode?.type === 'contain') { let resizeModeScaleX = 1; let resizeModeScaleY = 1; let extraX = 0; let extraY = 0; const { width: tw, height: th } = texture.dimensions; const txAspectRatio = tw / th; const nodeAspectRatio = width / height; if (txAspectRatio > nodeAspectRatio) { // Texture is wider than node // Center the node vertically (shift down by extraY) // Scale the node vertically to maintain original aspect ratio const scaleX = width / tw; const scaledTxHeight = th * scaleX; extraY = (height - scaledTxHeight) / 2; resizeModeScaleY = scaledTxHeight / height; } else { // Texture is taller than node (or equal) // Center the node horizontally (shift right by extraX) // Scale the node horizontally to maintain original aspect ratio const scaleY = height / th; const scaledTxWidth = tw * scaleY; extraX = (width - scaledTxWidth) / 2; resizeModeScaleX = scaledTxWidth / width; } // Apply the extra translation and scale to the local transform this.localTransform .translate(extraX, extraY) .scale(resizeModeScaleX, resizeModeScaleY); } } /** * @todo: test for correct calculation flag * @param delta */ update(delta, parentClippingRect) { if (this.updateType === UpdateType.None) { return; } const props = this.props; const parent = props.parent; const parentHasRenderTexture = this.parentHasRenderTexture; const hasParent = props.parent !== null; let renderState = null; let updateType = this.updateType; let childUpdateType = this.childUpdateType; let updateParent = false; if (updateType & UpdateType.Local) { this.updateLocalTransform(); updateType |= UpdateType.Global; updateParent = hasParent; } // Handle specific RTT updates at this node level if (updateType & UpdateType.RenderTexture && this.rtt === true) { this.hasRTTupdates = true; } if (updateType & UpdateType.Global) { if (this.parentHasRenderTexture === true && parent?.rtt === true) { // we are at the start of the RTT chain, so we need to reset the globalTransform // for correct RTT rendering this.globalTransform = Matrix3d.identity(); // Maintain a full scene global transform for bounds detection this.sceneGlobalTransform = Matrix3d.copy(parent?.globalTransform || Matrix3d.identity()).multiply(this.localTransform); } else if (this.parentHasRenderTexture === true && parent?.rtt === false) { // we're part of an RTT chain but our parent is not the main RTT node // so we need to propogate the sceneGlobalTransform of the parent // to maintain a full scene global transform for bounds detection this.sceneGlobalTransform = Matrix3d.copy(parent?.sceneGlobalTransform || this.localTransform).multiply(this.localTransform); this.globalTransform = Matrix3d.copy(parent?.globalTransform || this.localTransform, this.globalTransform); } else { this.globalTransform = Matrix3d.copy(parent?.globalTransform || this.localTransform, this.globalTransform); } if (parent !== null) { this.globalTransform.multiply(this.localTransform); } this.calculateRenderCoords(); this.updateBoundingRect(); updateType |= UpdateType.RenderState | UpdateType.Children | UpdateType.RecalcUniforms; updateParent = hasParent; childUpdateType |= UpdateType.Global; if (this.clipping === true) { updateType |= UpdateType.Clipping | UpdateType.RenderBounds; updateParent = hasParent; childUpdateType |= UpdateType.RenderBounds; } } if (updateType & UpdateType.RenderBounds) { this.createRenderBounds(); updateType |= UpdateType.RenderState | UpdateType.Children; updateParent = hasParent; childUpdateType |= UpdateType.RenderBounds; } if (updateType & UpdateType.RenderState) { renderState = this.checkRenderBounds(); updateType |= UpdateType.IsRenderable; updateParent = hasParent; // if we're not going out of bounds, update the render state // this is done so the update loop can finish before we mark a node // as out of bounds if (renderState !== CoreNodeRenderState.OutOfBounds) { this.updateRenderState(renderState); } } if (updateType & UpdateType.WorldAlpha) { this.worldAlpha = ((parent && parent.worldAlpha) || 1) * props.alpha; updateType |= UpdateType.PremultipliedColors | UpdateType.Children | UpdateType.IsRenderable; updateParent = hasParent; childUpdateType |= UpdateType.WorldAlpha; } if (updateType & UpdateType.IsRenderable) { this.updateIsRenderable(); } if (updateType & UpdateType.Clipping) { this.calculateClippingRect(parentClippingRect); updateType |= UpdateType.Children; updateParent = hasParent; childUpdateType |= UpdateType.Clipping | UpdateType.RenderBounds; } if (updateType & UpdateType.PremultipliedColors) { const alpha = this.worldAlpha; const tl = props.colorTl; const tr = props.colorTr; const bl = props.colorBl; const br = props.colorBr; // Fast equality check (covers all 4 corners) const same = tl === tr && tl === bl && tl === br; const merged = mergeColorAlphaPremultiplied(tl, alpha, true); this.premultipliedColorTl = merged; if (same) { this.premultipliedColorTr = this.premultipliedColorBl = this.premultipliedColorBr = merged; } else { this.premultipliedColorTr = mergeColorAlphaPremultiplied(tr, alpha, true); this.premultipliedColorBl = mergeColorAlphaPremultiplied(bl, alpha, true); this.premultipliedColorBr = mergeColorAlphaPremultiplied(br, alpha, true); } } if (updateParent === true) { parent.setUpdateType(UpdateType.Children); } // No need to update zIndex if there is no parent if (updateType & UpdateType.CalculatedZIndex && parent !== null) { this.calculateZIndex(); // Tell parent to re-sort children parent.setUpdateType(UpdateType.ZIndexSortedChildren); } if (props.strictBounds === true && this.renderState === CoreNodeRenderState.OutOfBounds) { updateType &= ~UpdateType.RenderBounds; // remove render bounds update return; } if (updateType & UpdateType.RecalcUniforms && this.hasShaderUpdater === true) { //this exists because the boolean hasShaderUpdater === true this.shader.update(); } if (updateType & UpdateType.Children && this.children.length > 0) { for (let i = 0, length = this.children.length; i < length; i++) { const child = this.children[i]; child.setUpdateType(childUpdateType); if (child.updateType === 0) { continue; } let childClippingRect = this.clippingRect; if (this.rtt === true) { childClippingRect = { x: 0, y: 0, width: 0, height: 0, valid: false, }; } child.update(delta, childClippingRect); } } // If the node has an RTT parent and requires a texture re-render, inform the RTT parent // if (this.parentHasRenderTexture && updateType & UpdateType.RenderTexture) { // @TODO have a more scoped down updateType for RTT updates if (parentHasRenderTexture === true) { this.notifyParentRTTOfUpdate(); } // Sorting children MUST happen after children have been updated so // that they have the oppotunity to update their calculated zIndex. if (updateType & UpdateType.ZIndexSortedChildren) { // reorder z-index this.sortChildren(); } if (this.updateTextureCoords === true) { this.updateTextureCoords = false; this.textureCoords = this.stage.renderer.getTextureCoords(this); } // If we're out of bounds, apply the render state now // this is done so nodes can finish their entire update loop before // being marked as out of bounds if (renderState === CoreNodeRenderState.OutOfBounds) { this.updateRenderState(renderState); this.updateIsRenderable(); if (this.rtt === true && renderState === CoreNodeRenderState.OutOfBounds) { // notify children that we are going out of bounds // we have to do this now before we stop processing the render tree this.notifyChildrenRTTOfUpdate(renderState); } } // reset update type this.updateType = 0; this.childUpdateType = 0; } findParentRTTNode() { let rttNode = this.parent; while (rttNode && !rttNode.rtt) { rttNode = rttNode.parent; } return rttNode; } notifyChildrenRTTOfUpdate(renderState) { for (const child of this.children) { // force child to update render state child.updateRenderState(renderState); child.updateIsRenderable(); child.notifyChildrenRTTOfUpdate(renderState); } } notifyParentRTTOfUpdate() { if (this.parent === null) { return; } const rttNode = this.rttParent || this.findParentRTTNode(); if (!rttNode) { return; } // If an RTT node is found, mark it for re-rendering rttNode.hasRTTupdates = true; rttNode.setUpdateType(UpdateType.RenderTexture); // if rttNode is nested, also make it update its RTT parent if (rttNode.parentHasRenderTexture === true) { rttNode.notifyParentRTTOfUpdate(); } } checkRenderBounds() { if (boundInsideBound(this.renderBound, this.strictBound)) { return CoreNodeRenderState.InViewport; } if (boundInsideBound(this.renderBound, this.preloadBound)) { return CoreNodeRenderState.InBounds; } // check if we're larger then our parent, we're definitely in the viewport if (boundLargeThanBound(this.renderBound, this.strictBound)) { return CoreNodeRenderState.InViewport; } // check if we dont have dimensions, take our parent's render state if (this.parent !== null && (this.props.width === 0 || this.props.height === 0)) { return this.parent.renderState; } return CoreNodeRenderState.OutOfBounds; } updateBoundingRect() { const transform = (this.sceneGlobalTransform || this.globalTransform); const renderCoords = (this.sceneRenderCoords || this.renderCoords); if (transform.tb === 0 || transform.tc === 0) { this.renderBound = createBound(renderCoords.x1, renderCoords.y1, renderCoords.x3, renderCoords.y3, this.renderBound); } else { const { x1, y1, x2, y2, x3, y3, x4, y4 } = renderCoords; this.renderBound = createBound(Math.min(x1, x2, x3, x4), Math.min(y1, y2, y3, y4), Math.max(x1, x2, x3, x4), Math.max(y1, y2, y3, y4), this.renderBound); } } createRenderBounds() { if (this.parent !== null && this.parent.strictBound !== undefined) { // we have a parent with a valid bound, copy it const parentBound = this.parent.strictBound; this.strictBound = createBound(parentBound.x1, parentBound.y1, parentBound.x2, parentBound.y2); this.preloadBound = createPreloadBounds(this.strictBound, this.boundsMargin); } else { // no parent or parent does not have a bound, take the stage boundaries this.strictBound = this.stage.strictBound; this.preloadBound = this.stage.preloadBound; } // if clipping is disabled, we're done if (this.props.clipping === false) { return; } // only create local clipping bounds if node itself is in bounds // this can only be done if we have a render bound already if (this.renderBound === undefined) { return; } // if we're out of bounds, we're done if (boundInsideBound(this.renderBound, this.strictBound) === false) { return; } // clipping is enabled and we are in bounds create our own bounds const { x, y, width, height } = this.props; // Pick the global transform if available, otherwise use the local transform // global transform is only available if the node in an RTT chain const { tx, ty } = this.sceneGlobalTransform || this.globalTransform || {}; const _x = tx ?? x; const _y = ty ?? y; this.strictBound = createBound(_x, _y, _x + width, _y + height, this.strictBound); this.preloadBound = createPreloadBounds(this.strictBound, this.boundsMargin); } updateRenderState(renderState) { if (renderState === this.renderState) { return; } const previous = this.renderState; this.renderState = renderState; const event = CoreNodeRenderStateMap.get(renderState); assertTruthy(event); this.emit(event, { previous, current: renderState, }); } /** * Updates the `isRenderable` property based on various conditions. */ updateIsRenderable() { let newIsRenderable = false; let needsTextureOwnership = false; // If the node is out of bounds or has an alpha of 0, it is not renderable if (this.worldAlpha === 0 || this.renderState <= CoreNodeRenderState.OutOfBounds) { this.updateTextureOwnership(false); this.setRenderable(false); return; } if (this.texture !== null) { needsTextureOwnership = true; // we're only renderable if the texture state is loaded newIsRenderable = this.texture.state === 'loaded'; } else if ( // check shader (this.props.shader !== null || this.hasColorProps === true) && // check dimensions (this.props.width !== 0 && this.props.height !== 0) === true) { // This mean we have dimensions and a color set, so we can render a ColorTexture if (this.stage.defaultTexture && this.stage.defaultTexture.state === 'loaded') { newIsRenderable = true; } } this.updateTextureOwnership(needsTextureOwnership); this.setRenderable(newIsRenderable); } /** * Sets the renderable state and triggers changes if necessary. * @param isRenderable - The new renderable state */ setRenderable(isRenderable) { this.isRenderable = isRenderable; if (isRenderable === true && this.stage.calculateTextureCoord === true && this.textureCoords === undefined) { this.updateTextureCoords = true; } } /** * Changes the renderable state of the node. */ updateTextureOwnership(isRenderable) { this.texture?.setRenderableOwner(this, isRenderable); } calculateRenderCoords() { const { width, height } = this; const g = this.globalTransform; const tx = g.tx, ty = g.ty, ta = g.ta, tb = g.tb, tc = g.tc, td = g.td; if (tb === 0 && tc === 0) { const minX = tx; const maxX = tx + width * ta; const minY = ty; const maxY = ty + height * td; this.renderCoords = RenderCoords.translate( //top-left minX, minY, //top-right maxX, minY, //bottom-right maxX, maxY, //bottom-left minX, maxY, this.renderCoords); } else { this.renderCoords = RenderCoords.translate( //top-left tx, ty, //top-right tx + width * ta, ty + width * tc, //bottom-right tx + width * ta + height * tb, ty + width * tc + height * td, //bottom-left tx + height * tb, ty + height * td, this.renderCoords); } if (this.sceneGlobalTransform === undefined) { return; } const { tx: stx, ty: sty, ta: sta, tb: stb, tc: stc, td: std, } = this.sceneGlobalTransform; if (stb === 0 && stc === 0) { const minX = stx; const maxX = stx + width * sta; const minY = sty; const maxY = sty + height * std; this.sceneRenderCoords = RenderCoords.translate( //top-left minX, minY, //top-right maxX, minY, //bottom-right maxX, maxY, //bottom-left minX, maxY, this.sceneRenderCoords); } else { this.sceneRenderCoords = RenderCoords.translate( //top-left stx, sty, //top-right stx + width * sta, sty + width * stc, //bottom-right stx + width * sta + height * stb, sty + width * stc + height * std, //bottom-left stx + height * stb, sty + height * std, this.sceneRenderCoords); } } /** * This function calculates the clipping rectangle for a node. * * The function then checks if the node is rotated. If the node requires clipping and is not rotated, a new clipping rectangle is created based on the node's global transform and dimensions. * If a parent clipping rectangle exists, it is intersected with the node's clipping rectangle (if it exists), or replaces the node's clipping rectangle. * * Finally, the node's parentClippingRect and clippingRect properties are updated. */ calculateClippingRect(parentClippingRect) { const { clippingRect, props, globalTransform: gt } = this; const { clipping } = props; const isRotated = gt.tb !== 0 || gt.tc !== 0; if (clipping === true && isRotated === false) { 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 === true && clippingRect.valid === true) { // Intersect parent clipping rect with node clipping rect intersectRect(parentClippingRect, clippingRect, clippingRect); } else if (parentClippingRect.valid === true) { // Copy parent clipping rect copyRect(parentClippingRect, clippingRect); clippingRect.valid = true; } } calculateZIndex() { const props = this.props; const z = props.zIndex || 0; const p = props.parent?.zIndex || 0; let zIndex = z; if (props.parent?.zIndexLocked) { zIndex = z < p ? z : p; } this.calcZIndex = zIndex; } /** * Destroy the node and cleanup all resources */ destroy() { if (this.destroyed === true) { return; } this.removeAllListeners(); this.destroyed = true; this.unloadTexture(); this.isRenderable = false; // Kill children while (this.children.length > 0) { this.children[0].destroy(); } const parent = this.parent; if (parent !== null) { const index = parent.children.indexOf(this); parent.children.splice(index, 1); parent.setUpdateType(UpdateType.Children | UpdateType.ZIndexSortedChildren); } this.props.parent = null; this.props.texture = null; if (this.rtt === true) { this.stage.renderer.removeRTTNode(this); } } renderQuads(renderer) { if (this.parentHasRenderTexture === true) { const rtt = renderer.renderToTextureActive; if (rtt === false || this.parentRenderTexture !== renderer.activeRttNode) return; } const p = this.props; const t = this.globalTransform; const coords = this.renderCoords; const texture = p.texture || this.stage.defaultTexture; renderer.addQuad({ width: p.width, height: p.height, colorTl: this.premultipliedColorTl, colorTr: this.premultipliedColorTr, colorBl: this.premultipliedColorBl, colorBr: this.premultipliedColorBr, texture, textureOptions: p.textureOptions, textureCoords: this.textureCoords, shader: p.shader, alpha: this.worldAlpha, clippingRect: this.clippingRect, tx: t.tx, ty: t.ty, ta: t.ta, tb: t.tb, tc: t.tc, td: t.td, renderCoords: coords, rtt: p.rtt, zIndex: this.calcZIndex, parentHasRenderTexture: this.parentHasRenderTexture, framebufferDimensions: this.parentHasRenderTexture ? this.parentFramebufferDimensions : null, }); } //#region Properties get id() { return this._id; } get data() { return this.props.data; } set data(d) { this.props.data = d; } get x() { return this.props.x; } set x(value) { if (this.props.x !== value) { this.props.x = value; this.setUpdateType(UpdateType.Local); } } get absX() { return (this.props.x + -this.props.width * this.props.mountX + (this.props.parent?.absX || this.props.parent?.globalTransform?.tx || 0)); } get absY() { return (this.props.y + -this.props.height * this.props.mountY + (this.props.parent?.absY ?? 0)); } get y() { return this.props.y; } set y(value) { if (this.props.y !== value) { this.props.y = value; this.setUpdateType(UpdateType.Local); } } get width() { return this.props.width; } set width(value) { if (this.props.width !== value) { this.textureCoords = undefined; this.props.width = value; this.setUpdateType(UpdateType.Local); if (this.props.rtt === true) { this.framebufferDimensions.width = value; this.texture = this.stage.txManager.createTexture('RenderTexture', this.framebufferDimensions); this.setUpdateType(UpdateType.RenderTexture); } } } get height() { return this.props.height; } set height(value) { if (this.props.height !== value) { this.textureCoords = undefined; this.props.height = value; this.setUpdateType(UpdateType.Local); if (this.props.rtt === true) { this.framebufferDimensions.height = value; this.texture = this.stage.txManager.createTexture('RenderTexture', this.framebufferDimensions); this.setUpdateType(UpdateType.RenderTexture); } } } get scale() { // The CoreNode `scale` property is only used by Animations. // Unlike INode, `null` should never be possibility for Animations. return this.scaleX; } set scale(value) { // The CoreNode `scale` property is only used by Animations. // Unlike INode, `null` should never be possibility for Animations. this.scaleX = value; this.scaleY = value; } get scaleX() { return this.props.scaleX; } set scaleX(value) { if (this.props.scaleX !== value) { this.props.scaleX = value; this.setUpdateType(UpdateType.Local); } } get scaleY() { return this.props.scaleY; } set scaleY(value) { if (this.props.scaleY !== value) { this.props.scaleY = value; this.setUpdateType(UpdateType.Local); } } get mount() { return this.props.mount; } set mount(value) { if (this.props.mountX !== value || this.props.mountY !== value) { this.props.mountX = value; this.props.mountY = value; this.props.mount = value; this.setUpdateType(UpdateType.Local); } } get mountX() { return this.props.mountX; } set mountX(value) { if (this.props.mountX !== value) { this.props.mountX = value; this.setUpdateType(UpdateType.Local); } } get mountY() { return this.props.mountY; } set mountY(value) { if (this.props.mountY !== value) { this.props.mountY = value; this.setUpdateType(UpdateType.Local); } } get pivot() { return this.props.pivot; } set pivot(value) { if (this.props.pivotX !== value || this.props.pivotY !== value) { this.props.pivotX = value; this.props.pivotY = value; this.props.pivot = value; this.setUpdateType(UpdateType.Local); } } get pivotX() { return this.props.pivotX; } set pivotX(value) { if (this.props.pivotX !== value) { this.props.pivotX = value; this.setUpdateType(UpdateType.Local); } } get pivotY() { return this.props.pivotY; } set pivotY(value) { if (this.props.pivotY !== value) { this.props.pivotY = value; this.setUpdateType(UpdateType.Local); } } get rotation() { return this.props.rotation; } set rotation(value) { if (this.props.rotation !== value) { this.props.rotation = value; this.setUpdateType(UpdateType.Local); } } get alpha() { return this.props.alpha; } set alpha(value) { this.props.alpha = value; this.setUpdateType(UpdateType.PremultipliedColors | UpdateType.WorldAlpha | UpdateType.Children | UpdateType.IsRenderable); this.childUpdateType |= UpdateType.WorldAlpha; } get autosize() { return this.props.autosize; } set autosize(value) { this.props.autosize = value; } get boundsMargin() { const props = this.props; if (props.boundsMargin !== null) { return props.boundsMargin; } const parent = this.parent; if (parent !== null) { const margin = parent.boundsMargin; if (margin !== undefined) { return margin; } } return this.stage.boundsMargin; } set boundsMargin(value) { if (value === this.props.boundsMargin) { return; } if (value === null) { this.props.boundsMargin = value; } else { const bm = Array.isArray(value) ? value : [value, value, value, value]; this.props.boundsMargin = bm; } this.setUpdateType(UpdateType.RenderBounds); } get clipping() { return this.props.clipping; } set clipping(value) { this.props.clipping = value; this.setUpdateType(UpdateType.Clipping | UpdateType.RenderBounds | UpdateType.Children); this.childUpdateType |= UpdateType.Global | UpdateType.Clipping; } get color() { return this.props.color; } set color(value) { const p = this.props; if (p.color === value) return; p.color = value; const has = value > 0; this.hasColorProps = has; if (p.colorTop !== value) this.colorTop = value; if (p.colorBottom !== value) this.colorBottom = value; if (p.colorLeft !== value) this.colorLeft = value; if (p.colorRight !== value) this.colorRight = value; this.setUpdateType(UpdateType.PremultipliedColors); } get colorTop() { return this.props.colorTop; } set colorTop(value) { if (this.props.colorTl !== value || this.props.colorTr !== value) { this.colorTl = value; this.colorTr = value; } this.props.colorTop = value; this.hasColorProps = value > 0; this.setUpdateType(UpdateType.PremultipliedColors); } get colorBottom() { return this.props.colorBottom; } set colorBottom(value) { if (this.props.colorBl !== value || this.props.colorBr !== value) { this.colorBl = value; this.colorBr = value; } this.props.colorBottom = value; this.hasColorProps = value > 0; this.setUpdateType(UpdateType.PremultipliedColors); } get colorLeft() { return this.props.colorLeft; } set colorLeft(value) { if (this.props.colorTl !== value || this.props.colorBl !== value) { this.colorTl = value; this.colorBl = value; } this.props.colorLeft = value; this.hasColorProps = value > 0; this.setUpdateType(UpdateType.PremultipliedColors); } get colorRight() { return this.props.colorRight; } set colorRight(value) { if (this.props.colorTr !== value || this.props.colorBr !== value) { this.colorTr = value; this.colorBr = value; } this.props.colorRight = value; this.hasColorProps = value > 0; this.setUpdateType(UpdateType.PremultipliedColors); } get colorTl() { return this.props.colorTl; } set colorTl(value) { this.props.colorTl = value; this.hasColorProps = value > 0; this.setUpdateType(UpdateType.PremultipliedColors); } get colorTr() { return this.props.colorTr; } set colorTr(value) { this.props.colorTr = value; this.hasColorProps = value > 0; this.setUpdateType(UpdateType.PremultipliedColors); } get colorBl() { return this.props.colorBl; } set colorBl(value) { this.props.colorBl = value; this.hasColorProps = value > 0; this.setUpdateType(UpdateType.PremultipliedColors); } get colorBr() { return this.props.colorBr; } set colorBr(value) { this.props.colorBr = value; this.hasColorProps = value > 0; this.setUpdateType(UpdateType.PremultipliedColors); } // we're only interested in parent zIndex to test // if we should use node zIndex is higher then parent zIndex get zIndexLocked() { return this.props.zIndexLocked || 0; } set zIndexLocked(value) { this.props.zIndexLocked = value; this.setUpdateType(UpdateType.CalculatedZIndex | UpdateType.Children); for (let i = 0, length = this.children.length; i < length; i++) { this.children[i].setUpdateType(UpdateType.CalculatedZIndex); } } get zIndex() { return this.props.zIndex; } set zIndex(value) { this.props.zIndex = value; this.setUpdateType(UpdateType.CalculatedZIndex | UpdateType.Children); for (let i = 0, length = this.children.length; i < length; i++) { this.children[i].setUpdateType(UpdateType.CalculatedZIndex); } } get parent() { return this.props.parent; } set parent(newParent) { const oldParent = this.props.parent; if (oldParent === newParent) { return; } this.props.parent = newParent; if (oldParent) { const index = oldParent.children.indexOf(this); oldParent.children.splice(index, 1); oldParent.setUpdateType(UpdateType.Children | UpdateType.ZIndexSortedChildren); } if (newParent) { newParent.children.push(this); // Since this node has a new parent, to be safe, have it do a full update. this.setUpdateType(UpdateType.All); // Tell parent that it's children need to be updated and sorted. newParent.setUpdateType(UpdateType.Children | UpdateType.ZIndexSortedChildren); // If the new parent has an RTT enabled, apply RTT inheritance if (newParent.rtt || newParent.parentHasRenderTexture) { this.applyRTTInheritance(newParent); } } // fetch render bounds from parent this.setUpdateType(UpdateType.RenderBounds | UpdateType.Children); } get rtt() { return this.props.rtt; } set rtt(value) { if (this.props.rtt === value) { return; } this.props.rtt = value; if (value === true) { this.initRenderTexture(); this.markChildrenWithRTT(); } else { this.cleanupRenderTexture(); } this.setUpdateType(UpdateType.RenderTexture); if (this.parentHasRenderTexture === true) { this.notifyParentRTTOfUpdate(); } } initRenderTexture() { this.framebufferDimensions = { width: this.width, height: this.height, }; this.texture = this.stage.txManager.createTexture('RenderTexture', this.framebufferDimensions); this.stage.renderer.renderToTexture(this); } cleanupRenderTexture() { this.unloadTexture(); this.clearRTTInheritance(); this.hasRTTupdates = false; this.texture = null; this.framebufferDimensions = null; } markChildrenWithRTT(node = null) { const parent = node || this; for (const child of parent.children) { child.setUpdateType(UpdateType.All); child.parentHasRenderTexture = true; child.markChildrenWithRTT(); } } // Apply RTT inheritance when a node has an RTT-enabled parent applyRTTInheritance(parent) { if (parent.rtt) { // Only the