@lightningjs/renderer
Version:
Lightning 3 Renderer
1,358 lines • 64.6 kB
JavaScript
/*
* 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';
import { AutosizeMode, Autosizer } from './Autosizer.js';
import { bucketSortByZIndex, removeChild } from './lib/collectionUtils.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 NO_CLIPPING_RECT = {
x: 0,
y: 0,
w: 0,
h: 0,
valid: false,
};
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";
/**
* Sort Z-Index Children update
*
* @remarks
* CoreNode Properties Updated:
* - `children` (sorts children by their `calcZIndex`)
*/
UpdateType[UpdateType["SortZIndexChildren"] = 16] = "SortZIndexChildren";
/**
* Premultiplied Colors update
*
* @remarks
* CoreNode Properties Updated:
* - `premultipliedColorTl`
* - `premultipliedColorTr`
* - `premultipliedColorBl`
* - `premultipliedColorBr`
*/
UpdateType[UpdateType["PremultipliedColors"] = 32] = "PremultipliedColors";
/**
* World Alpha update
*
* @remarks
* CoreNode Properties Updated:
* - `worldAlpha` = `parent.worldAlpha` * `alpha`
*/
UpdateType[UpdateType["WorldAlpha"] = 64] = "WorldAlpha";
/**
* Render State update
*
* @remarks
* CoreNode Properties Updated:
* - `renderState`
*/
UpdateType[UpdateType["RenderState"] = 128] = "RenderState";
/**
* Is Renderable update
*
* @remarks
* CoreNode Properties Updated:
* - `isRenderable`
*/
UpdateType[UpdateType["IsRenderable"] = 256] = "IsRenderable";
/**
* Render Texture update
*/
UpdateType[UpdateType["RenderTexture"] = 512] = "RenderTexture";
/**
* Track if parent has render texture
*/
UpdateType[UpdateType["ParentRenderTexture"] = 1024] = "ParentRenderTexture";
/**
* Render Bounds update
*/
UpdateType[UpdateType["RenderBounds"] = 2048] = "RenderBounds";
/**
* RecalcUniforms
*/
UpdateType[UpdateType["RecalcUniforms"] = 4096] = "RecalcUniforms";
/**
* Autosize update
*/
UpdateType[UpdateType["Autosize"] = 8192] = "Autosize";
/**
* None
*/
UpdateType[UpdateType["None"] = 0] = "None";
/**
* All
*/
UpdateType[UpdateType["All"] = 16383] = "All";
})(UpdateType || (UpdateType = {}));
/**
* Bitmask of UpdateType flags that represent a visually significant change
* within a node. Used to gate notifyParentRTTOfUpdate() so that RTT surfaces
* are only marked dirty when something actually visible changed, rather than
* on every update() cycle that merely propagates child traversal.
*
* Excluded flags (non-visual cascade/bookkeeping):
* Children, RenderBounds, RenderState, ParentRenderTexture, Autosize
*/
const RTT_NOTIFY_MASK = UpdateType.Local |
UpdateType.Global |
UpdateType.Clipping |
UpdateType.SortZIndexChildren |
UpdateType.PremultipliedColors |
UpdateType.WorldAlpha |
UpdateType.IsRenderable |
UpdateType.RenderTexture |
UpdateType.RecalcUniforms;
/**
* 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;
isCoreNode = true;
// WebGL Render Op State
renderOpBufferIdx = 0;
numQuads = 0;
renderOpTextures = [];
hasShaderUpdater = false;
hasShaderTimeFn = false;
hasColorProps = false;
zIndexMin = 0;
zIndexMax = 0;
previousZIndex = -1;
updateType = UpdateType.All;
childUpdateType = UpdateType.None;
globalTransform;
localTransform;
sceneGlobalTransform;
renderCoords;
sceneRenderCoords;
renderBound;
strictBound;
preloadBound;
clippingRect = {
x: 0,
y: 0,
w: 0,
h: 0,
valid: false,
};
textureCoords;
updateShaderUniforms = 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;
/**Autosize properties */
autosizer = null;
parentAutosizer = null;
destroyed = false;
constructor(stage, props) {
super();
this.stage = stage;
const p = (this.props = {});
// Initialize the renderOpTextures array with a capacity of 16 (typical max textures)
this.renderOpTextures = [];
//inital update type
let initialUpdateType = UpdateType.Local | UpdateType.RenderBounds | UpdateType.RenderState;
// Fast-path assign only known keys
p.x = props.x;
p.y = props.y;
p.w = props.w;
p.h = props.h;
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;
//check if any color props are set for premultiplied color updates
if (props.color > 0 ||
props.colorTop > 0 ||
props.colorBottom > 0 ||
props.colorLeft > 0 ||
props.colorRight > 0 ||
props.colorTl > 0 ||
props.colorTr > 0 ||
props.colorBl > 0 ||
props.colorBr > 0) {
this.hasColorProps = true;
initialUpdateType |= UpdateType.PremultipliedColors;
}
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.zIndex = props.zIndex;
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.autosize = props.autosize;
p.parent = props.parent;
p.texture = null;
p.shader = null;
p.src = null;
p.rtt = false;
p.boundsMargin = null;
// Only set non-default values
if (props.zIndex !== 0) {
this.zIndex = props.zIndex;
}
if (props.parent !== null) {
props.parent.addChild(this);
}
// Assign props to instances
this.texture = props.texture;
this.shader = props.shader;
this.src = props.src;
this.rtt = props.rtt;
this.boundsMargin = props.boundsMargin;
this.interactive = props.interactive;
// Initialize autosize if enabled
if (p.autosize === true) {
this.autosizer = new Autosizer(this);
}
this.setUpdateType(initialUpdateType);
// 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 = stage.defaultTexture;
if (dt !== null && dt.state !== 'loaded') {
dt.once('loaded', () => this.setUpdateType(UpdateType.IsRenderable));
}
}
//#region Textures
loadTexture() {
if (this.props.texture === null) {
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(this.loadTextureTask);
}
/**
* Task for queueMicrotask to loadTexture
*
* @remarks
* This method is called in a microtask to release the texture.
*/
loadTextureTask = () => {
const texture = this.props.texture;
//it is possible that texture is null here if user sets the texture to null right after loadTexture call
if (texture === null) {
return;
}
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 (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._id, false);
}
onTextureLoaded = (_, dimensions) => {
if (this.autosizer !== null) {
this.autosizer.update();
}
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.w > 1 && dimensions.h > 1) {
this.emit('loaded', {
type: 'texture',
dimensions,
});
}
if (this.stage.calculateTextureCoord === true &&
this.props.textureOptions !== null) {
this.textureCoords = this.stage.renderer.getTextureCoords(this);
}
// 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.updateTextureOwnership(false);
this.setUpdateType(UpdateType.IsRenderable);
// If parent has a render texture, flag that we need to update
if (this.parentHasRenderTexture) {
this.notifyParentRTTOfUpdate();
}
if (this.texture !== null &&
this.texture.retryCount > this.texture.maxRetryCount) {
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.updateTextureOwnership(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);
}
updateLocalTransform() {
const p = this.props;
const { x, y, w, h } = p;
const mountTranslateX = p.mountX * w;
const mountTranslateY = p.mountY * h;
if (p.rotation !== 0 || p.scaleX !== 1 || p.scaleY !== 1) {
const scaleRotate = Matrix3d.rotate(p.rotation).scale(p.scaleX, p.scaleY);
const pivotTranslateX = p.pivotX * w;
const pivotTranslateY = p.pivotY * h;
this.localTransform = Matrix3d.translate(x - mountTranslateX + pivotTranslateX, y - mountTranslateY + pivotTranslateY, this.localTransform)
.multiply(scaleRotate)
.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 { w: tw, h: th } = texture.dimensions;
const txAspectRatio = tw / th;
const nodeAspectRatio = w / h;
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 = w / tw;
const scaledTxHeight = th * scaleX;
extraY = (h - scaledTxHeight) / 2;
resizeModeScaleY = scaledTxHeight / h;
}
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 = h / th;
const scaledTxWidth = tw * scaleY;
extraX = (w - scaledTxWidth) / 2;
resizeModeScaleX = scaledTxWidth / w;
}
// 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) {
const props = this.props;
const parent = props.parent;
const parentHasRenderTexture = this.parentHasRenderTexture;
const hasParent = props.parent !== null;
let newRenderState = null;
let updateType = this.updateType;
let childUpdateType = this.childUpdateType;
let updateParent = false;
//this needs to be handled before setting updateTypes are reset
if (updateType & UpdateType.Autosize && this.autosizer !== null) {
this.autosizer.update();
}
// reset update type
this.updateType = 0;
this.childUpdateType = 0;
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.RecalcUniforms;
updateParent = hasParent;
//only propagate children updates if not autosizing
if ((updateType & UpdateType.Autosize) === 0) {
updateType |= UpdateType.Children;
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) {
newRenderState = 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 (newRenderState !== CoreNodeRenderState.OutOfBounds) {
this.updateRenderState(newRenderState);
}
}
if (updateType & UpdateType.WorldAlpha) {
this.worldAlpha = (parent?.worldAlpha ?? 1) * this.props.alpha;
updateType |=
UpdateType.PremultipliedColors |
UpdateType.Children |
UpdateType.IsRenderable;
updateParent = hasParent;
childUpdateType |= UpdateType.WorldAlpha;
}
if (updateType & UpdateType.IsRenderable) {
this.updateIsRenderable();
}
// Handle autosize updates when children transforms change
if (updateType & UpdateType.Global &&
this.isRenderable === true &&
this.parentAutosizer !== null) {
this.parentAutosizer.patch(this.id);
}
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 === true) {
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 (this.renderState === CoreNodeRenderState.OutOfBounds) {
// Delay updating children until the node is in bounds
this.updateType = updateType;
this.childUpdateType = childUpdateType;
return;
}
if (updateParent === true) {
parent.setUpdateType(UpdateType.Children);
}
if (updateType & UpdateType.RecalcUniforms &&
this.hasShaderUpdater === true) {
this.updateShaderUniforms = true;
}
if (this.isRenderable === true && this.updateShaderUniforms === true) {
this.updateShaderUniforms = false;
//this exists because the boolean hasShaderUpdater === true
this.shader.update();
}
if (updateType & UpdateType.Children && this.children.length > 0) {
let childClippingRect = this.clippingRect;
if (this.rtt === true) {
childClippingRect = NO_CLIPPING_RECT;
}
for (let i = 0, length = this.children.length; i < length; i++) {
const child = this.children[i];
if (childUpdateType !== 0) {
child.setUpdateType(childUpdateType);
}
if (child.updateType === 0) {
continue;
}
child.update(delta, childClippingRect);
}
}
// If the node has an RTT parent and a visually relevant change occurred (or a
// nested RTT child already flagged this node via hasRTTupdates), notify the
// nearest RTT ancestor so it re-renders its surface.
// Guarded by RTT_NOTIFY_MASK to avoid redundant notifications on frames where
// only child-traversal bookkeeping flags (Children, RenderBounds, etc.) are set.
if (parentHasRenderTexture === true &&
(this.hasRTTupdates === true || (updateType & RTT_NOTIFY_MASK) !== 0)) {
this.notifyParentRTTOfUpdate();
}
//Resort children if needed
if (updateType & UpdateType.SortZIndexChildren) {
// reorder z-index
this.sortChildren();
}
// 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 (newRenderState === CoreNodeRenderState.OutOfBounds) {
this.updateRenderState(newRenderState);
this.updateIsRenderable();
if (this.rtt === true &&
newRenderState === 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(newRenderState);
}
}
}
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.w === 0 || this.props.h === 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, w, h } = 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 + w, _y + h, 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,
});
}
/**
* Checks if the node is renderable based on world alpha, dimensions and out of bounds status.
*/
checkBasicRenderability() {
if (this.worldAlpha === 0 || this.isOutOfBounds() === true) {
return false;
}
else {
return true;
}
}
/**
* 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.checkBasicRenderability() === false) {
this.updateTextureOwnership(false);
this.setRenderable(false);
return;
}
if (this.texture !== null) {
// preemptive check for failed textures this will mark the current node as non-renderable
// and will prevent further checks until the texture is reloaded or retry is reset on the texture
if (this.texture.retryCount > this.texture.maxRetryCount) {
// texture has failed to load, we cannot render
this.updateTextureOwnership(false);
this.setRenderable(false);
return;
}
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.hasDimensions() === 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) {
const previousIsRenderable = this.isRenderable;
this.isRenderable = isRenderable;
// Emit event if renderable status has changed
if (previousIsRenderable !== isRenderable) {
this.emit('renderable', {
type: 'renderable',
isRenderable,
});
}
}
/**
* Changes the renderable state of the node.
*/
updateTextureOwnership(isRenderable) {
this.texture?.setRenderableOwner(this._id, isRenderable);
}
/**
* Checks if the node is out of the viewport bounds.
*/
isOutOfBounds() {
return this.renderState <= CoreNodeRenderState.OutOfBounds;
}
/**
* Checks if the node has dimensions (width/height)
*/
hasDimensions() {
return this.props.w !== 0 && this.props.h !== 0;
}
calculateRenderCoords() {
const { w, h } = this.props;
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 + w * ta;
const minY = ty;
const maxY = ty + h * 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 + w * ta, ty + w * tc,
//bottom-right
tx + w * ta + h * tb, ty + w * tc + h * td,
//bottom-left
tx + h * tb, ty + h * 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 + w * sta;
const minY = sty;
const maxY = sty + h * 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 + w * sta, sty + w * stc,
//bottom-right
stx + w * sta + h * stb, sty + w * stc + h * std,
//bottom-left
stx + h * stb, sty + h * 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.w = this.props.w * gt.ta;
clippingRect.h = this.props.h * 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;
}
}
/**
* Destroy the node and cleanup all resources
*/
destroy() {
if (this.destroyed === true) {
return;
}
this.removeAllListeners();
this.destroyed = true;
this.unloadTexture();
this.isRenderable = false;
if (this.hasShaderTimeFn === true) {
this.stage.untrackTimedNode(this);
}
// Kill children
while (this.children.length > 0) {
this.children[0].destroy();
}
const parent = this.parent;
if (parent !== null) {
parent.removeChild(this);
}
this.props.parent = null;
this.props.texture = null;
if (this.rtt === true) {
this.stage.renderer.removeRTTNode(this);
}
this.stage.requestRender();
}
renderQuads(renderer) {
if (this.parentHasRenderTexture === true) {
const rtt = renderer.renderToTextureActive;
if (rtt === false || this.parentRenderTexture !== renderer.activeRttNode)
return;
}
const texture = this.props.texture || this.stage.defaultTexture;
// There is a race condition where the texture can be null
// with RTT nodes. Adding this defensively to avoid errors.
// Also check if we have a valid texture or default texture to render
if (!texture || texture.state !== 'loaded') {
return;
}
renderer.addQuad(this);
}
get quadBufferCollection() {
return this.stage.renderer.quadBufferCollection;
}
get time() {
if (this.hasShaderTimeFn === true) {
return this.getTimerValue();
}
return 0;
}
getTimerValue() {
if (typeof this.shader.time === 'function') {
return this.shader.time(this.stage);
}
return this.stage.elapsedTime;
}
sortChildren() {
const children = this.children;
const n = children.length;
if (n === 0) {
this.zIndexMin = 0;
this.zIndexMax = 0;
return;
}
let firstZIndex = children[0].props.zIndex;
let min = firstZIndex;
let max = firstZIndex;
let prevZIndex = firstZIndex;
let isSorted = true;
for (let i = 1; i < n; i++) {
const zIndex = children[i].props.zIndex;
if (zIndex < min) {
min = zIndex;
}
else if (zIndex > max) {
max = zIndex;
}
if (prevZIndex > zIndex) {
isSorted = false;
}
prevZIndex = zIndex;
}
// update min and max zIndex
this.zIndexMin = min;
this.zIndexMax = max;
// if min and max are the same, no need to sort
if (min === max || isSorted === true) {
return;
}
bucketSortByZIndex(children, min);
}
removeChild(node, targetParent = null) {
if (targetParent === null) {
if (this.props.rtt === true && this.parentHasRenderTexture === true) {
node.clearRTTInheritance();
}
const autosizeTarget = this.autosizer || this.parentAutosizer;
if (autosizeTarget !== null) {
autosizeTarget.detach(node);
}
}
const children = this.children;
removeChild(node, children);
if (children.length === 0) {
this.zIndexMin = 0;
this.zIndexMax = 0;
return;
}
const removedZIndex = node.zIndex;
if (removedZIndex === this.zIndexMin || removedZIndex === this.zIndexMax) {
this.setUpdateType(UpdateType.SortZIndexChildren);
}
}
addChild(node, previousParent = null) {
const inRttCluster = this.props.rtt === true || this.parentHasRenderTexture === true;
const children = this.children;
const zIndex = node.zIndex;
const autosizeTarget = this.autosizer || this.parentAutosizer;
let attachToAutosizer = autosizeTarget !== null;
node.parentHasRenderTexture = inRttCluster;
if (previousParent !== null) {
const previousParentInRttCluster = previousParent.props.rtt === true ||
previousParent.parentHasRenderTexture === true;
if (inRttCluster === false && previousParentInRttCluster === true) {
// update child RTT status
node.clearRTTInheritance();
}
const previousAutosizer = node.autosizer || node.parentAutosizer;
if (previousAutosizer !== null) {
if (autosizeTarget === null ||
previousAutosizer.id !== autosizeTarget.id) {
previousAutosizer.detach(node);
}
attachToAutosizer = false;
}
}
if (attachToAutosizer === true) {
//if this is true, then the autosizer really exists
autosizeTarget.attach(node);
}
if (inRttCluster === true) {
node.markChildrenWithRTT(this);
}
children.push(node);
if (children.length === 1) {
this.zIndexMin = zIndex;
this.zIndexMax = zIndex;
}
else {
if (zIndex < this.zIndexMin) {
this.zIndexMin = zIndex;
}
if (zIndex > this.zIndexMax) {
this.zIndexMax = zIndex;
}
}
if (this.zIndexMax !== this.zIndexMin) {
this.setUpdateType(UpdateType.SortZIndexChildren);
}
this.setUpdateType(UpdateType.Children);
}
//#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.w * this.props.mountX +
(this.props.parent?.absX || this.props.parent?.globalTransform?.tx || 0));
}
get absY() {
return (this.props.y +
-this.props.h * 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 w() {
return this.props.w;
}
set w(value) {
const props = this.props;
if (props.w !== value) {
props.w = value;
let updateType = UpdateType.Local;
if (props.texture !== null &&
this.stage.calculateTextureCoord === true &&
props.textureOptions !== null) {
this.textureCoords = this.stage.renderer.getTextureCoords(this);
}
if (props.rtt === true) {
this.framebufferDimensions.w = value;
this.texture = this.stage.txManager.createTexture('RenderTexture', this.framebufferDimensions);
updateType |= UpdateType.RenderTexture;
}
this.setUpdateType(updateType);
}
}
get h() {
return this.props.h;
}
set h(value) {
const props = this.props;
if (props.h !== value) {
props.h = value;
let updateType = UpdateType.Local;
if (props.texture !== null &&
this.stage.calculateTextureCoord === true &&
props.textureOptions !== null) {
this.textureCoords = this.stage.renderer.getTextureCoords(this);
}
if (props.rtt === true) {
this.framebufferDimensions.h = value;
this.texture = this.stage.txManager.createTexture('RenderTexture', this.framebufferDimensions);
updateType |= UpdateType.RenderTexture;
}
this.setUpdateType(updateType);
}
}
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) {
if (this.props.autosize === value) {
return;
}
this.props.autosize = value;
if (value === true && this.autosizer === null) {
this.autosizer = new Autosizer(this);
}
else {
this.autosizer = null;
}
}
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