@lightningjs/renderer
Advanced tools
Comparing version 2.6.2 to 2.7.0
@@ -681,2 +681,3 @@ import type { TextureOptions } from './CoreTextureManager.js'; | ||
update(delta: number, parentClippingRect: RectWithValid): void; | ||
private notifyParentRTTOfUpdate; | ||
hasRenderableProperties(): boolean; | ||
@@ -777,2 +778,7 @@ checkRenderBounds(): CoreNodeRenderState; | ||
set rtt(value: boolean); | ||
private initRenderTexture; | ||
private cleanupRenderTexture; | ||
private markChildrenWithRTT; | ||
private applyRTTInheritance; | ||
private clearRTTInheritance; | ||
get shader(): BaseShaderController; | ||
@@ -809,3 +815,2 @@ set shader(value: BaseShaderController); | ||
set strictBounds(v: boolean); | ||
setRTTUpdates(type: number): void; | ||
animate(props: Partial<CoreNodeAnimateProps>, settings: Partial<AnimationSettings>): IAnimationController; | ||
@@ -812,0 +817,0 @@ flush(): void; |
@@ -223,2 +223,14 @@ /* | ||
} | ||
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') { | ||
@@ -235,5 +247,2 @@ assertTruthy(texture.dimensions); | ||
} | ||
texture.on('loaded', this.onTextureLoaded); | ||
texture.on('failed', this.onTextureFailed); | ||
texture.on('freed', this.onTextureFreed); | ||
}); | ||
@@ -261,5 +270,4 @@ } | ||
// If parent has a render texture, flag that we need to update | ||
// @todo: Reserve type for RTT updates | ||
if (this.parentHasRenderTexture) { | ||
this.setRTTUpdates(1); | ||
this.notifyParentRTTOfUpdate(); | ||
} | ||
@@ -276,2 +284,6 @@ this.emit('loaded', { | ||
onTextureFailed = (_, error) => { | ||
// If parent has a render texture, flag that we need to update | ||
if (this.parentHasRenderTexture) { | ||
this.notifyParentRTTOfUpdate(); | ||
} | ||
this.emit('failed', { | ||
@@ -283,2 +295,6 @@ type: 'texture', | ||
onTextureFreed = () => { | ||
// If parent has a render texture, flag that we need to update | ||
if (this.parentHasRenderTexture) { | ||
this.notifyParentRTTOfUpdate(); | ||
} | ||
this.emit('freed', { | ||
@@ -302,18 +318,6 @@ type: 'texture', | ||
return; | ||
// Inform the parent if it doesn’t already have a child update | ||
if ((parent.updateType & UpdateType.Children) === 0) { | ||
// Inform the parent if it doesn’t already have a child update | ||
parent.setUpdateType(UpdateType.Children); | ||
} | ||
if (this.parentHasRenderTexture === false) | ||
return; | ||
if (this.rtt === false) { | ||
if ((parent.updateType & UpdateType.RenderTexture) === 0) { | ||
this.setRTTUpdates(type); | ||
parent.setUpdateType(UpdateType.RenderTexture); | ||
} | ||
} | ||
// If this node has outstanding RTT updates, propagate them | ||
if (this.hasRTTupdates) { | ||
this.setRTTUpdates(type); | ||
} | ||
} | ||
@@ -398,18 +402,8 @@ sortChildren() { | ||
let renderState = null; | ||
if (this.updateType & UpdateType.ParentRenderTexture) { | ||
let p = this.parent; | ||
while (p) { | ||
if (p.rtt) { | ||
this.parentHasRenderTexture = true; | ||
} | ||
p = p.parent; | ||
} | ||
// Handle specific RTT updates at this node level | ||
if (this.updateType & UpdateType.RenderTexture && this.rtt) { | ||
// Only the RTT node itself triggers `renderToTexture` | ||
this.hasRTTupdates = true; | ||
this.stage.renderer?.renderToTexture(this); | ||
} | ||
// If we have render texture updates and not already running a full update | ||
if (this.updateType ^ UpdateType.All && | ||
this.updateType & UpdateType.RenderTexture) { | ||
for (let i = 0, length = this.children.length; i < length; i++) { | ||
this.children[i]?.setUpdateType(UpdateType.All); | ||
} | ||
} | ||
if (this.updateType & UpdateType.Global) { | ||
@@ -496,5 +490,3 @@ assertTruthy(this.localTransform); | ||
} | ||
if (this.updateType & UpdateType.Children && | ||
this.children.length > 0 && | ||
this.rtt === false) { | ||
if (this.updateType & UpdateType.Children && this.children.length > 0) { | ||
for (let i = 0, length = this.children.length; i < length; i++) { | ||
@@ -509,2 +501,8 @@ const child = this.children[i]; | ||
} | ||
// If the node has an RTT parent and requires a texture re-render, inform the RTT parent | ||
// if (this.parentHasRenderTexture && this.updateType & UpdateType.RenderTexture) { | ||
// @TODO have a more scoped down updateType for RTT updates | ||
if (this.parentHasRenderTexture && this.updateType > 0) { | ||
this.notifyParentRTTOfUpdate(); | ||
} | ||
// Sorting children MUST happen after children have been updated so | ||
@@ -527,2 +525,22 @@ // that they have the oppotunity to update their calculated zIndex. | ||
} | ||
notifyParentRTTOfUpdate() { | ||
if (this.parent === null) { | ||
return; | ||
} | ||
let rttNode = this.parent; | ||
// Traverse up to find the RTT root node | ||
while (rttNode && !rttNode.rtt) { | ||
rttNode = rttNode.parent; | ||
} | ||
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(); | ||
} | ||
} | ||
//check if CoreNode is renderable based on props | ||
@@ -1136,4 +1154,5 @@ hasRenderableProperties() { | ||
newParent.setUpdateType(UpdateType.Children | UpdateType.ZIndexSortedChildren); | ||
// If the new parent has an RTT enabled, apply RTT inheritance | ||
if (newParent.rtt || newParent.parentHasRenderTexture) { | ||
this.setRTTUpdates(UpdateType.All); | ||
this.applyRTTInheritance(newParent); | ||
} | ||
@@ -1155,20 +1174,19 @@ } | ||
set rtt(value) { | ||
if (this.props.rtt === true) { | ||
this.props.rtt = value; | ||
// unload texture if we used to have a render texture | ||
if (value === false && this.texture !== null) { | ||
this.unloadTexture(); | ||
this.setUpdateType(UpdateType.All); | ||
for (let i = 0, length = this.children.length; i < length; i++) { | ||
this.children[i].parentHasRenderTexture = false; | ||
} | ||
this.stage.renderer?.removeRTTNode(this); | ||
return; | ||
} | ||
} | ||
// if the new value is false and we didnt have rtt previously, we don't need to do anything | ||
if (value === false) { | ||
if (this.props.rtt === value) { | ||
return; | ||
} | ||
// load texture | ||
this.props.rtt = value; | ||
if (value) { | ||
this.initRenderTexture(); | ||
this.markChildrenWithRTT(); | ||
} | ||
else { | ||
this.cleanupRenderTexture(); | ||
} | ||
this.setUpdateType(UpdateType.RenderTexture); | ||
if (this.parentHasRenderTexture) { | ||
this.notifyParentRTTOfUpdate(); | ||
} | ||
} | ||
initRenderTexture() { | ||
this.texture = this.stage.txManager.loadTexture('RenderTexture', { | ||
@@ -1179,11 +1197,42 @@ width: this.width, | ||
this.textureOptions.preload = true; | ||
this.props.rtt = true; | ||
this.hasRTTupdates = true; | ||
this.setUpdateType(UpdateType.All); | ||
for (let i = 0, length = this.children.length; i < length; i++) { | ||
this.children[i].setUpdateType(UpdateType.All); | ||
this.stage.renderer?.renderToTexture(this); // Only this RTT node | ||
} | ||
cleanupRenderTexture() { | ||
this.unloadTexture(); | ||
this.clearRTTInheritance(); | ||
this.stage.renderer?.removeRTTNode(this); | ||
this.hasRTTupdates = false; | ||
this.texture = null; | ||
} | ||
markChildrenWithRTT(node = null) { | ||
const parent = node || this; | ||
for (const child of parent.children) { | ||
child.setUpdateType(UpdateType.All); | ||
child.parentHasRenderTexture = true; | ||
child.markChildrenWithRTT(); | ||
} | ||
// Store RTT nodes in a separate list | ||
this.stage.renderer?.renderToTexture(this); | ||
} | ||
// Apply RTT inheritance when a node has an RTT-enabled parent | ||
applyRTTInheritance(parent) { | ||
if (parent.rtt) { | ||
// Only the RTT node should be added to `renderToTexture` | ||
parent.setUpdateType(UpdateType.RenderTexture); | ||
} | ||
// Propagate `parentHasRenderTexture` downwards | ||
this.markChildrenWithRTT(parent); | ||
} | ||
// Clear RTT inheritance when detaching from an RTT chain | ||
clearRTTInheritance() { | ||
// if this node is RTT itself stop the propagation important for nested RTT nodes | ||
// for the initial RTT node this is already handled in `set rtt` | ||
if (this.rtt) { | ||
return; | ||
} | ||
for (const child of this.children) { | ||
// force child to update everything as the RTT inheritance has changed | ||
child.parentHasRenderTexture = false; | ||
child.setUpdateType(UpdateType.All); | ||
child.clearRTTInheritance(); | ||
} | ||
} | ||
get shader() { | ||
@@ -1316,6 +1365,2 @@ return this.props.shader; | ||
} | ||
setRTTUpdates(type) { | ||
this.hasRTTupdates = true; | ||
this.parent?.setRTTUpdates(type); | ||
} | ||
animate(props, settings) { | ||
@@ -1322,0 +1367,0 @@ const animation = new CoreAnimation(this, props, settings); |
@@ -23,4 +23,4 @@ /* | ||
import { CanvasCoreTexture } from './CanvasCoreTexture.js'; | ||
import { getRadius } from './internal/C2DShaderUtils.js'; | ||
import { formatRgba, parseColor, } from './internal/ColorUtils.js'; | ||
import { getBorder, getRadius, strokeLine } from './internal/C2DShaderUtils.js'; | ||
import { formatRgba, parseColorRgba, parseColor, } from './internal/ColorUtils.js'; | ||
import { UnsupportedShader } from './shaders/UnsupportedShader.js'; | ||
@@ -93,3 +93,5 @@ export class CanvasCoreRenderer extends CoreRenderer { | ||
const hasGradient = colorTl !== colorTr || colorTl !== colorBr; | ||
const radius = quad.shader ? getRadius(quad) : 0; | ||
const hasQuadShader = Boolean(quad.shader); | ||
const radius = hasQuadShader ? getRadius(quad) : 0; | ||
const border = hasQuadShader ? getBorder(quad) : undefined; | ||
if (hasTransform || hasClipping || radius) { | ||
@@ -160,2 +162,37 @@ ctx.save(); | ||
} | ||
if (border && border.width) { | ||
const borderWidth = border.width; | ||
const borderInnerWidth = border.width / 2; | ||
const borderColor = formatRgba(parseColorRgba(border.color ?? 0)); | ||
ctx.beginPath(); | ||
ctx.lineWidth = borderWidth; | ||
ctx.strokeStyle = borderColor; | ||
ctx.globalAlpha = alpha; | ||
if (radius) { | ||
ctx.roundRect(tx + borderInnerWidth, ty + borderInnerWidth, width - borderWidth, height - borderWidth, radius); | ||
ctx.stroke(); | ||
} | ||
else { | ||
ctx.strokeRect(tx + borderInnerWidth, ty + borderInnerWidth, width - borderWidth, height - borderWidth); | ||
} | ||
ctx.globalAlpha = 1; | ||
} | ||
else if (hasQuadShader) { | ||
const borderTop = getBorder(quad, 'Top'); | ||
const borderRight = getBorder(quad, 'Right'); | ||
const borderBottom = getBorder(quad, 'Bottom'); | ||
const borderLeft = getBorder(quad, 'Left'); | ||
if (borderTop) { | ||
strokeLine(ctx, tx, ty, width, height, borderTop.width, borderTop.color, 'Top'); | ||
} | ||
if (borderRight) { | ||
strokeLine(ctx, tx, ty, width, height, borderRight.width, borderRight.color, 'Right'); | ||
} | ||
if (borderBottom) { | ||
strokeLine(ctx, tx, ty, width, height, borderBottom.width, borderBottom.color, 'Bottom'); | ||
} | ||
if (borderLeft) { | ||
strokeLine(ctx, tx, ty, width, height, borderLeft.width, borderLeft.color, 'Left'); | ||
} | ||
} | ||
if (hasTransform || hasClipping || radius) { | ||
@@ -162,0 +199,0 @@ ctx.restore(); |
import type { QuadOptions } from '../../CoreRenderer.js'; | ||
import type { BorderEffectProps } from '../../webgl/shaders/effects/BorderEffect.js'; | ||
import type { RadiusEffectProps } from '../../webgl/shaders/effects/RadiusEffect.js'; | ||
type Direction = 'Top' | 'Right' | 'Bottom' | 'Left'; | ||
/** | ||
* Extract `RoundedRectangle` shader radius to apply as a clipping | ||
*/ | ||
export declare function getRadius(quad: QuadOptions): number; | ||
export declare function getRadius(quad: QuadOptions): RadiusEffectProps['radius']; | ||
/** | ||
* Extract `RoundedRectangle` shader radius to apply as a clipping */ | ||
export declare function getBorder(quad: QuadOptions, direction?: '' | Direction): BorderEffectProps | undefined; | ||
export declare function strokeLine(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, lineWidth: number | undefined, color: number | undefined, direction: Direction): void; | ||
export {}; |
@@ -20,2 +20,3 @@ /* | ||
import { ROUNDED_RECTANGLE_SHADER_TYPE, UnsupportedShader, } from '../shaders/UnsupportedShader.js'; | ||
import { formatRgba, parseColorRgba } from './ColorUtils.js'; | ||
/** | ||
@@ -30,5 +31,72 @@ * Extract `RoundedRectangle` shader radius to apply as a clipping | ||
} | ||
else if (shType === 'DynamicShader') { | ||
const effects = quad.shaderProps?.effects; | ||
if (effects) { | ||
const effect = effects.find((effect) => { | ||
return effect.type === 'radius' && effect?.props?.radius; | ||
}); | ||
return (effect && effect.type === 'radius' && effect.props.radius) || 0; | ||
} | ||
} | ||
} | ||
return 0; | ||
} | ||
/** | ||
* Extract `RoundedRectangle` shader radius to apply as a clipping */ | ||
export function getBorder(quad, direction = '') { | ||
if (quad.shader instanceof UnsupportedShader) { | ||
const shType = quad.shader.shType; | ||
if (shType === 'DynamicShader') { | ||
const effects = quad.shaderProps?.effects; | ||
if (effects && effects.length) { | ||
const effect = effects.find((effect) => { | ||
return (effect.type === `border${direction}` && | ||
effect.props && | ||
effect.props.width); | ||
}); | ||
return effect && effect.props; | ||
} | ||
} | ||
} | ||
return undefined; | ||
} | ||
export function strokeLine(ctx, x, y, width, height, lineWidth = 0, color, direction) { | ||
if (!lineWidth) { | ||
return; | ||
} | ||
let sx, sy = 0; | ||
let ex, ey = 0; | ||
switch (direction) { | ||
case 'Top': | ||
sx = x; | ||
sy = y; | ||
ex = width + x; | ||
ey = y; | ||
break; | ||
case 'Right': | ||
sx = x + width; | ||
sy = y; | ||
ex = x + width; | ||
ey = y + height; | ||
break; | ||
case 'Bottom': | ||
sx = x; | ||
sy = y + height; | ||
ex = x + width; | ||
ey = y + height; | ||
break; | ||
case 'Left': | ||
sx = x; | ||
sy = y; | ||
ex = x; | ||
ey = y + height; | ||
break; | ||
} | ||
ctx.beginPath(); | ||
ctx.lineWidth = lineWidth; | ||
ctx.strokeStyle = formatRgba(parseColorRgba(color ?? 0)); | ||
ctx.moveTo(sx, sy); | ||
ctx.lineTo(ex, ey); | ||
ctx.stroke(); | ||
} | ||
//# sourceMappingURL=C2DShaderUtils.js.map |
@@ -13,4 +13,8 @@ export interface IParsedColor { | ||
/** | ||
* Extract color components | ||
*/ | ||
export declare function parseColorRgba(rgba: number): IParsedColor; | ||
/** | ||
* Format a parsed color into a rgba CSS color | ||
*/ | ||
export declare function formatRgba({ a, r, g, b }: IParsedColor): string; |
@@ -40,2 +40,15 @@ /* | ||
/** | ||
* Extract color components | ||
*/ | ||
export function parseColorRgba(rgba) { | ||
if (rgba === 0xffffffff) { | ||
return WHITE; | ||
} | ||
const r = (rgba >>> 24) & 0xff; | ||
const g = (rgba >>> 16) & 0xff & 0xff; | ||
const b = (rgba >>> 8) & 0xff & 0xff; | ||
const a = (rgba & 0xff & 0xff) / 255; | ||
return { isWhite: false, r, g, b, a }; | ||
} | ||
/** | ||
* Format a parsed color into a rgba CSS color | ||
@@ -42,0 +55,0 @@ */ |
@@ -26,5 +26,5 @@ /* | ||
this.shType = shType; | ||
if (shType !== ROUNDED_RECTANGLE_SHADER_TYPE) { | ||
console.warn('Unsupported shader:', shType); | ||
} | ||
// if (shType !== ROUNDED_RECTANGLE_SHADER_TYPE) { | ||
// console.warn('Unsupported shader:', shType); | ||
// } | ||
} | ||
@@ -31,0 +31,0 @@ bindRenderOp() { |
@@ -0,1 +1,19 @@ | ||
/* | ||
* 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 { updateWebSafeRadius, validateArrayLength4 } from './EffectUtils.js'; | ||
@@ -2,0 +20,0 @@ import { ShaderEffect, } from './ShaderEffect.js'; |
@@ -97,2 +97,25 @@ import { CoreRenderer, type BufferInfo, type CoreRendererOptions, type QuadOptions } from '../CoreRenderer.js'; | ||
renderToTexture(node: CoreNode): void; | ||
/** | ||
* Inserts an RTT node into `this.rttNodes` while maintaining the correct rendering order based on hierarchy. | ||
* | ||
* Rendering order for RTT nodes is critical when nested RTT nodes exist in a parent-child relationship. | ||
* Specifically: | ||
* - Child RTT nodes must be rendered before their RTT-enabled parents to ensure proper texture composition. | ||
* - If an RTT node is added and it has existing RTT children, it should be rendered after those children. | ||
* | ||
* This function addresses both cases by: | ||
* 1. **Checking Upwards**: It traverses the node's hierarchy upwards to identify any RTT parent | ||
* already in `rttNodes`. If an RTT parent is found, the new node is placed before this parent. | ||
* 2. **Checking Downwards**: It traverses the node’s children recursively to find any RTT-enabled | ||
* children that are already in `rttNodes`. If such children are found, the new node is inserted | ||
* after the last (highest index) RTT child node. | ||
* | ||
* The final calculated insertion index ensures the new node is positioned in `rttNodes` to respect | ||
* both parent-before-child and child-before-parent rendering rules, preserving the correct order | ||
* for the WebGL renderer. | ||
* | ||
* @param node - The RTT-enabled CoreNode to be added to `rttNodes` in the appropriate hierarchical position. | ||
*/ | ||
private insertRTTNodeInOrder; | ||
private findMaxChildRTTIndex; | ||
renderRTTNodes(): void; | ||
@@ -99,0 +122,0 @@ removeRTTNode(node: CoreNode): void; |
@@ -465,5 +465,68 @@ /* | ||
} | ||
// @todo: Better bottom up rendering order | ||
this.rttNodes.unshift(node); | ||
this.insertRTTNodeInOrder(node); | ||
} | ||
/** | ||
* Inserts an RTT node into `this.rttNodes` while maintaining the correct rendering order based on hierarchy. | ||
* | ||
* Rendering order for RTT nodes is critical when nested RTT nodes exist in a parent-child relationship. | ||
* Specifically: | ||
* - Child RTT nodes must be rendered before their RTT-enabled parents to ensure proper texture composition. | ||
* - If an RTT node is added and it has existing RTT children, it should be rendered after those children. | ||
* | ||
* This function addresses both cases by: | ||
* 1. **Checking Upwards**: It traverses the node's hierarchy upwards to identify any RTT parent | ||
* already in `rttNodes`. If an RTT parent is found, the new node is placed before this parent. | ||
* 2. **Checking Downwards**: It traverses the node’s children recursively to find any RTT-enabled | ||
* children that are already in `rttNodes`. If such children are found, the new node is inserted | ||
* after the last (highest index) RTT child node. | ||
* | ||
* The final calculated insertion index ensures the new node is positioned in `rttNodes` to respect | ||
* both parent-before-child and child-before-parent rendering rules, preserving the correct order | ||
* for the WebGL renderer. | ||
* | ||
* @param node - The RTT-enabled CoreNode to be added to `rttNodes` in the appropriate hierarchical position. | ||
*/ | ||
insertRTTNodeInOrder(node) { | ||
let insertIndex = this.rttNodes.length; // Default to the end of the array | ||
// 1. Traverse upwards to ensure the node is placed before its RTT parent (if any). | ||
let currentNode = node; | ||
while (currentNode) { | ||
if (!currentNode.parent) { | ||
break; | ||
} | ||
const parentIndex = this.rttNodes.indexOf(currentNode.parent); | ||
if (parentIndex !== -1) { | ||
// Found an RTT parent in the list; set insertIndex to place node before the parent | ||
insertIndex = parentIndex; | ||
break; | ||
} | ||
currentNode = currentNode.parent; | ||
} | ||
// 2. Traverse downwards to ensure the node is placed after any RTT children. | ||
// Look through each child recursively to see if any are already in rttNodes. | ||
const maxChildIndex = this.findMaxChildRTTIndex(node); | ||
if (maxChildIndex !== -1) { | ||
// Adjust insertIndex to be after the last child RTT node | ||
insertIndex = Math.max(insertIndex, maxChildIndex + 1); | ||
} | ||
// 3. Insert the node at the calculated position | ||
this.rttNodes.splice(insertIndex, 0, node); | ||
} | ||
// Helper function to find the highest index of any RTT children of a node within rttNodes | ||
findMaxChildRTTIndex(node) { | ||
let maxIndex = -1; | ||
const traverseChildren = (currentNode) => { | ||
const currentIndex = this.rttNodes.indexOf(currentNode); | ||
if (currentIndex !== -1) { | ||
maxIndex = Math.max(maxIndex, currentIndex); | ||
} | ||
// Recursively check all children of the current node | ||
for (const child of currentNode.children) { | ||
traverseChildren(child); | ||
} | ||
}; | ||
// Start traversal directly with the provided node | ||
traverseChildren(node); | ||
return maxIndex; | ||
} | ||
renderRTTNodes() { | ||
@@ -498,9 +561,2 @@ const { glw } = this; | ||
} | ||
child.update(this.stage.deltaTime, { | ||
x: 0, | ||
y: 0, | ||
width: 0, | ||
height: 0, | ||
valid: false, | ||
}); | ||
this.stage.addQuads(child); | ||
@@ -507,0 +563,0 @@ child.hasRTTupdates = false; |
{ | ||
"name": "@lightningjs/renderer", | ||
"version": "2.6.2", | ||
"version": "2.7.0", | ||
"description": "Lightning 3 Renderer", | ||
@@ -64,3 +64,2 @@ "type": "module", | ||
], | ||
"packageManager": "pnpm@8.9.2", | ||
"engines": { | ||
@@ -67,0 +66,0 @@ "npm": ">= 10.0.0", |
@@ -786,2 +786,17 @@ /* | ||
} | ||
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') { | ||
@@ -796,5 +811,2 @@ assertTruthy(texture.dimensions); | ||
} | ||
texture.on('loaded', this.onTextureLoaded); | ||
texture.on('failed', this.onTextureFailed); | ||
texture.on('freed', this.onTextureFreed); | ||
}); | ||
@@ -827,5 +839,4 @@ } | ||
// If parent has a render texture, flag that we need to update | ||
// @todo: Reserve type for RTT updates | ||
if (this.parentHasRenderTexture) { | ||
this.setRTTUpdates(1); | ||
this.notifyParentRTTOfUpdate(); | ||
} | ||
@@ -845,2 +856,7 @@ | ||
private onTextureFailed: TextureFailedEventHandler = (_, error) => { | ||
// If parent has a render texture, flag that we need to update | ||
if (this.parentHasRenderTexture) { | ||
this.notifyParentRTTOfUpdate(); | ||
} | ||
this.emit('failed', { | ||
@@ -853,2 +869,7 @@ type: 'texture', | ||
private onTextureFreed: TextureFreedEventHandler = () => { | ||
// If parent has a render texture, flag that we need to update | ||
if (this.parentHasRenderTexture) { | ||
this.notifyParentRTTOfUpdate(); | ||
} | ||
this.emit('freed', { | ||
@@ -874,20 +895,6 @@ type: 'texture', | ||
// Inform the parent if it doesn’t already have a child update | ||
if ((parent.updateType & UpdateType.Children) === 0) { | ||
// Inform the parent if it doesn’t already have a child update | ||
parent.setUpdateType(UpdateType.Children); | ||
} | ||
if (this.parentHasRenderTexture === false) return; | ||
if (this.rtt === false) { | ||
if ((parent.updateType & UpdateType.RenderTexture) === 0) { | ||
this.setRTTUpdates(type); | ||
parent.setUpdateType(UpdateType.RenderTexture); | ||
} | ||
} | ||
// If this node has outstanding RTT updates, propagate them | ||
if (this.hasRTTupdates) { | ||
this.setRTTUpdates(type); | ||
} | ||
} | ||
@@ -997,22 +1004,9 @@ | ||
if (this.updateType & UpdateType.ParentRenderTexture) { | ||
let p = this.parent; | ||
while (p) { | ||
if (p.rtt) { | ||
this.parentHasRenderTexture = true; | ||
} | ||
p = p.parent; | ||
} | ||
// Handle specific RTT updates at this node level | ||
if (this.updateType & UpdateType.RenderTexture && this.rtt) { | ||
// Only the RTT node itself triggers `renderToTexture` | ||
this.hasRTTupdates = true; | ||
this.stage.renderer?.renderToTexture(this); | ||
} | ||
// If we have render texture updates and not already running a full update | ||
if ( | ||
this.updateType ^ UpdateType.All && | ||
this.updateType & UpdateType.RenderTexture | ||
) { | ||
for (let i = 0, length = this.children.length; i < length; i++) { | ||
this.children[i]?.setUpdateType(UpdateType.All); | ||
} | ||
} | ||
if (this.updateType & UpdateType.Global) { | ||
@@ -1140,7 +1134,3 @@ assertTruthy(this.localTransform); | ||
if ( | ||
this.updateType & UpdateType.Children && | ||
this.children.length > 0 && | ||
this.rtt === false | ||
) { | ||
if (this.updateType & UpdateType.Children && this.children.length > 0) { | ||
for (let i = 0, length = this.children.length; i < length; i++) { | ||
@@ -1159,2 +1149,9 @@ const child = this.children[i] as CoreNode; | ||
// If the node has an RTT parent and requires a texture re-render, inform the RTT parent | ||
// if (this.parentHasRenderTexture && this.updateType & UpdateType.RenderTexture) { | ||
// @TODO have a more scoped down updateType for RTT updates | ||
if (this.parentHasRenderTexture && this.updateType > 0) { | ||
this.notifyParentRTTOfUpdate(); | ||
} | ||
// Sorting children MUST happen after children have been updated so | ||
@@ -1180,2 +1177,27 @@ // that they have the oppotunity to update their calculated zIndex. | ||
private notifyParentRTTOfUpdate() { | ||
if (this.parent === null) { | ||
return; | ||
} | ||
let rttNode: CoreNode | null = this.parent; | ||
// Traverse up to find the RTT root node | ||
while (rttNode && !rttNode.rtt) { | ||
rttNode = rttNode.parent; | ||
} | ||
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(); | ||
} | ||
} | ||
//check if CoreNode is renderable based on props | ||
@@ -1953,4 +1975,5 @@ hasRenderableProperties(): boolean { | ||
// If the new parent has an RTT enabled, apply RTT inheritance | ||
if (newParent.rtt || newParent.parentHasRenderTexture) { | ||
this.setRTTUpdates(UpdateType.All); | ||
this.applyRTTInheritance(newParent); | ||
} | ||
@@ -1977,23 +2000,21 @@ } | ||
set rtt(value: boolean) { | ||
if (this.props.rtt === true) { | ||
this.props.rtt = value; | ||
if (this.props.rtt === value) { | ||
return; | ||
} | ||
this.props.rtt = value; | ||
// unload texture if we used to have a render texture | ||
if (value === false && this.texture !== null) { | ||
this.unloadTexture(); | ||
this.setUpdateType(UpdateType.All); | ||
for (let i = 0, length = this.children.length; i < length; i++) { | ||
this.children[i]!.parentHasRenderTexture = false; | ||
} | ||
this.stage.renderer?.removeRTTNode(this); | ||
return; | ||
} | ||
if (value) { | ||
this.initRenderTexture(); | ||
this.markChildrenWithRTT(); | ||
} else { | ||
this.cleanupRenderTexture(); | ||
} | ||
// if the new value is false and we didnt have rtt previously, we don't need to do anything | ||
if (value === false) { | ||
return; | ||
this.setUpdateType(UpdateType.RenderTexture); | ||
if (this.parentHasRenderTexture) { | ||
this.notifyParentRTTOfUpdate(); | ||
} | ||
// load texture | ||
} | ||
private initRenderTexture() { | ||
this.texture = this.stage.txManager.loadTexture('RenderTexture', { | ||
@@ -2004,15 +2025,51 @@ width: this.width, | ||
this.textureOptions.preload = true; | ||
this.stage.renderer?.renderToTexture(this); // Only this RTT node | ||
} | ||
this.props.rtt = true; | ||
this.hasRTTupdates = true; | ||
this.setUpdateType(UpdateType.All); | ||
private cleanupRenderTexture() { | ||
this.unloadTexture(); | ||
this.clearRTTInheritance(); | ||
for (let i = 0, length = this.children.length; i < length; i++) { | ||
this.children[i]!.setUpdateType(UpdateType.All); | ||
this.stage.renderer?.removeRTTNode(this); | ||
this.hasRTTupdates = false; | ||
this.texture = null; | ||
} | ||
private markChildrenWithRTT(node: CoreNode | null = null) { | ||
const parent = node || this; | ||
for (const child of parent.children) { | ||
child.setUpdateType(UpdateType.All); | ||
child.parentHasRenderTexture = true; | ||
child.markChildrenWithRTT(); | ||
} | ||
} | ||
// Store RTT nodes in a separate list | ||
this.stage.renderer?.renderToTexture(this); | ||
// Apply RTT inheritance when a node has an RTT-enabled parent | ||
private applyRTTInheritance(parent: CoreNode) { | ||
if (parent.rtt) { | ||
// Only the RTT node should be added to `renderToTexture` | ||
parent.setUpdateType(UpdateType.RenderTexture); | ||
} | ||
// Propagate `parentHasRenderTexture` downwards | ||
this.markChildrenWithRTT(parent); | ||
} | ||
// Clear RTT inheritance when detaching from an RTT chain | ||
private clearRTTInheritance() { | ||
// if this node is RTT itself stop the propagation important for nested RTT nodes | ||
// for the initial RTT node this is already handled in `set rtt` | ||
if (this.rtt) { | ||
return; | ||
} | ||
for (const child of this.children) { | ||
// force child to update everything as the RTT inheritance has changed | ||
child.parentHasRenderTexture = false; | ||
child.setUpdateType(UpdateType.All); | ||
child.clearRTTInheritance(); | ||
} | ||
} | ||
get shader(): BaseShaderController { | ||
@@ -2174,7 +2231,2 @@ return this.props.shader; | ||
setRTTUpdates(type: number) { | ||
this.hasRTTupdates = true; | ||
this.parent?.setRTTUpdates(type); | ||
} | ||
animate( | ||
@@ -2181,0 +2233,0 @@ props: Partial<CoreNodeAnimateProps>, |
@@ -33,5 +33,6 @@ /* | ||
import { CanvasCoreTexture } from './CanvasCoreTexture.js'; | ||
import { getRadius } from './internal/C2DShaderUtils.js'; | ||
import { getBorder, getRadius, strokeLine } from './internal/C2DShaderUtils.js'; | ||
import { | ||
formatRgba, | ||
parseColorRgba, | ||
parseColor, | ||
@@ -137,3 +138,5 @@ type IParsedColor, | ||
const hasGradient = colorTl !== colorTr || colorTl !== colorBr; | ||
const radius = quad.shader ? getRadius(quad) : 0; | ||
const hasQuadShader = Boolean(quad.shader); | ||
const radius = hasQuadShader ? getRadius(quad) : 0; | ||
const border = hasQuadShader ? getBorder(quad) : undefined; | ||
@@ -216,2 +219,88 @@ if (hasTransform || hasClipping || radius) { | ||
if (border && border.width) { | ||
const borderWidth = border.width; | ||
const borderInnerWidth = border.width / 2; | ||
const borderColor = formatRgba(parseColorRgba(border.color ?? 0)); | ||
ctx.beginPath(); | ||
ctx.lineWidth = borderWidth; | ||
ctx.strokeStyle = borderColor; | ||
ctx.globalAlpha = alpha; | ||
if (radius) { | ||
ctx.roundRect( | ||
tx + borderInnerWidth, | ||
ty + borderInnerWidth, | ||
width - borderWidth, | ||
height - borderWidth, | ||
radius, | ||
); | ||
ctx.stroke(); | ||
} else { | ||
ctx.strokeRect( | ||
tx + borderInnerWidth, | ||
ty + borderInnerWidth, | ||
width - borderWidth, | ||
height - borderWidth, | ||
); | ||
} | ||
ctx.globalAlpha = 1; | ||
} else if (hasQuadShader) { | ||
const borderTop = getBorder(quad, 'Top'); | ||
const borderRight = getBorder(quad, 'Right'); | ||
const borderBottom = getBorder(quad, 'Bottom'); | ||
const borderLeft = getBorder(quad, 'Left'); | ||
if (borderTop) { | ||
strokeLine( | ||
ctx, | ||
tx, | ||
ty, | ||
width, | ||
height, | ||
borderTop.width, | ||
borderTop.color, | ||
'Top', | ||
); | ||
} | ||
if (borderRight) { | ||
strokeLine( | ||
ctx, | ||
tx, | ||
ty, | ||
width, | ||
height, | ||
borderRight.width, | ||
borderRight.color, | ||
'Right', | ||
); | ||
} | ||
if (borderBottom) { | ||
strokeLine( | ||
ctx, | ||
tx, | ||
ty, | ||
width, | ||
height, | ||
borderBottom.width, | ||
borderBottom.color, | ||
'Bottom', | ||
); | ||
} | ||
if (borderLeft) { | ||
strokeLine( | ||
ctx, | ||
tx, | ||
ty, | ||
width, | ||
height, | ||
borderLeft.width, | ||
borderLeft.color, | ||
'Left', | ||
); | ||
} | ||
} | ||
if (hasTransform || hasClipping || radius) { | ||
@@ -218,0 +307,0 @@ ctx.restore(); |
@@ -21,2 +21,5 @@ /* | ||
import type { QuadOptions } from '../../CoreRenderer.js'; | ||
import type { BorderEffectProps } from '../../webgl/shaders/effects/BorderEffect.js'; | ||
import type { RadiusEffectProps } from '../../webgl/shaders/effects/RadiusEffect.js'; | ||
import type { EffectDescUnion } from '../../webgl/shaders/effects/ShaderEffect.js'; | ||
import { | ||
@@ -26,7 +29,10 @@ ROUNDED_RECTANGLE_SHADER_TYPE, | ||
} from '../shaders/UnsupportedShader.js'; | ||
import { formatRgba, parseColorRgba } from './ColorUtils.js'; | ||
type Direction = 'Top' | 'Right' | 'Bottom' | 'Left'; | ||
/** | ||
* Extract `RoundedRectangle` shader radius to apply as a clipping | ||
*/ | ||
export function getRadius(quad: QuadOptions): number { | ||
export function getRadius(quad: QuadOptions): RadiusEffectProps['radius'] { | ||
if (quad.shader instanceof UnsupportedShader) { | ||
@@ -36,2 +42,14 @@ const shType = quad.shader.shType; | ||
return (quad.shaderProps?.radius as number) ?? 0; | ||
} else if (shType === 'DynamicShader') { | ||
const effects = quad.shaderProps?.effects as | ||
| EffectDescUnion[] | ||
| undefined; | ||
if (effects) { | ||
const effect = effects.find((effect: EffectDescUnion) => { | ||
return effect.type === 'radius' && effect?.props?.radius; | ||
}); | ||
return (effect && effect.type === 'radius' && effect.props.radius) || 0; | ||
} | ||
} | ||
@@ -41,1 +59,84 @@ } | ||
} | ||
/** | ||
* Extract `RoundedRectangle` shader radius to apply as a clipping */ | ||
export function getBorder( | ||
quad: QuadOptions, | ||
direction: '' | Direction = '', | ||
): BorderEffectProps | undefined { | ||
if (quad.shader instanceof UnsupportedShader) { | ||
const shType = quad.shader.shType; | ||
if (shType === 'DynamicShader') { | ||
const effects = quad.shaderProps?.effects as | ||
| EffectDescUnion[] | ||
| undefined; | ||
if (effects && effects.length) { | ||
const effect = effects.find((effect: EffectDescUnion) => { | ||
return ( | ||
effect.type === `border${direction}` && | ||
effect.props && | ||
effect.props.width | ||
); | ||
}); | ||
return effect && effect.props; | ||
} | ||
} | ||
} | ||
return undefined; | ||
} | ||
export function strokeLine( | ||
ctx: CanvasRenderingContext2D, | ||
x: number, | ||
y: number, | ||
width: number, | ||
height: number, | ||
lineWidth = 0, | ||
color: number | undefined, | ||
direction: Direction, | ||
) { | ||
if (!lineWidth) { | ||
return; | ||
} | ||
let sx, | ||
sy = 0; | ||
let ex, | ||
ey = 0; | ||
switch (direction) { | ||
case 'Top': | ||
sx = x; | ||
sy = y; | ||
ex = width + x; | ||
ey = y; | ||
break; | ||
case 'Right': | ||
sx = x + width; | ||
sy = y; | ||
ex = x + width; | ||
ey = y + height; | ||
break; | ||
case 'Bottom': | ||
sx = x; | ||
sy = y + height; | ||
ex = x + width; | ||
ey = y + height; | ||
break; | ||
case 'Left': | ||
sx = x; | ||
sy = y; | ||
ex = x; | ||
ey = y + height; | ||
break; | ||
} | ||
ctx.beginPath(); | ||
ctx.lineWidth = lineWidth; | ||
ctx.strokeStyle = formatRgba(parseColorRgba(color ?? 0)); | ||
ctx.moveTo(sx, sy); | ||
ctx.lineTo(ex, ey); | ||
ctx.stroke(); | ||
} |
@@ -51,2 +51,16 @@ /* | ||
/** | ||
* Extract color components | ||
*/ | ||
export function parseColorRgba(rgba: number): IParsedColor { | ||
if (rgba === 0xffffffff) { | ||
return WHITE; | ||
} | ||
const r = (rgba >>> 24) & 0xff; | ||
const g = (rgba >>> 16) & 0xff & 0xff; | ||
const b = (rgba >>> 8) & 0xff & 0xff; | ||
const a = (rgba & 0xff & 0xff) / 255; | ||
return { isWhite: false, r, g, b, a }; | ||
} | ||
/** | ||
* Format a parsed color into a rgba CSS color | ||
@@ -53,0 +67,0 @@ */ |
@@ -30,5 +30,5 @@ /* | ||
this.shType = shType; | ||
if (shType !== ROUNDED_RECTANGLE_SHADER_TYPE) { | ||
console.warn('Unsupported shader:', shType); | ||
} | ||
// if (shType !== ROUNDED_RECTANGLE_SHADER_TYPE) { | ||
// console.warn('Unsupported shader:', shType); | ||
// } | ||
} | ||
@@ -35,0 +35,0 @@ |
@@ -19,3 +19,2 @@ /* | ||
*/ | ||
import type { DynamicShaderProps } from '../DynamicShader.js'; | ||
import { updateWebSafeRadius, validateArrayLength4 } from './EffectUtils.js'; | ||
@@ -26,3 +25,2 @@ import { | ||
type ShaderEffectUniforms, | ||
type ShaderEffectValueMap, | ||
} from './ShaderEffect.js'; | ||
@@ -29,0 +27,0 @@ |
import type { EffectMap } from '../../../../CoreShaderManager.js'; | ||
import type { ExtractProps } from '../../../../CoreTextureManager.js'; | ||
import type { WebGlContextWrapper } from '../../../../lib/WebGlContextWrapper.js'; | ||
import type { | ||
@@ -5,0 +4,0 @@ AlphaShaderProp, |
@@ -91,3 +91,6 @@ /* | ||
this._state = 'failed'; | ||
this.textureSource.setState('failed', new Error('Could not create WebGL Texture')); | ||
this.textureSource.setState( | ||
'failed', | ||
new Error('Could not create WebGL Texture'), | ||
); | ||
console.error('Could not create WebGL Texture'); | ||
@@ -94,0 +97,0 @@ return; |
@@ -629,9 +629,84 @@ /* | ||
// @todo: Better bottom up rendering order | ||
this.rttNodes.unshift(node); | ||
this.insertRTTNodeInOrder(node); | ||
} | ||
/** | ||
* Inserts an RTT node into `this.rttNodes` while maintaining the correct rendering order based on hierarchy. | ||
* | ||
* Rendering order for RTT nodes is critical when nested RTT nodes exist in a parent-child relationship. | ||
* Specifically: | ||
* - Child RTT nodes must be rendered before their RTT-enabled parents to ensure proper texture composition. | ||
* - If an RTT node is added and it has existing RTT children, it should be rendered after those children. | ||
* | ||
* This function addresses both cases by: | ||
* 1. **Checking Upwards**: It traverses the node's hierarchy upwards to identify any RTT parent | ||
* already in `rttNodes`. If an RTT parent is found, the new node is placed before this parent. | ||
* 2. **Checking Downwards**: It traverses the node’s children recursively to find any RTT-enabled | ||
* children that are already in `rttNodes`. If such children are found, the new node is inserted | ||
* after the last (highest index) RTT child node. | ||
* | ||
* The final calculated insertion index ensures the new node is positioned in `rttNodes` to respect | ||
* both parent-before-child and child-before-parent rendering rules, preserving the correct order | ||
* for the WebGL renderer. | ||
* | ||
* @param node - The RTT-enabled CoreNode to be added to `rttNodes` in the appropriate hierarchical position. | ||
*/ | ||
private insertRTTNodeInOrder(node: CoreNode) { | ||
let insertIndex = this.rttNodes.length; // Default to the end of the array | ||
// 1. Traverse upwards to ensure the node is placed before its RTT parent (if any). | ||
let currentNode: CoreNode = node; | ||
while (currentNode) { | ||
if (!currentNode.parent) { | ||
break; | ||
} | ||
const parentIndex = this.rttNodes.indexOf(currentNode.parent); | ||
if (parentIndex !== -1) { | ||
// Found an RTT parent in the list; set insertIndex to place node before the parent | ||
insertIndex = parentIndex; | ||
break; | ||
} | ||
currentNode = currentNode.parent; | ||
} | ||
// 2. Traverse downwards to ensure the node is placed after any RTT children. | ||
// Look through each child recursively to see if any are already in rttNodes. | ||
const maxChildIndex = this.findMaxChildRTTIndex(node); | ||
if (maxChildIndex !== -1) { | ||
// Adjust insertIndex to be after the last child RTT node | ||
insertIndex = Math.max(insertIndex, maxChildIndex + 1); | ||
} | ||
// 3. Insert the node at the calculated position | ||
this.rttNodes.splice(insertIndex, 0, node); | ||
} | ||
// Helper function to find the highest index of any RTT children of a node within rttNodes | ||
private findMaxChildRTTIndex(node: CoreNode): number { | ||
let maxIndex = -1; | ||
const traverseChildren = (currentNode: CoreNode) => { | ||
const currentIndex = this.rttNodes.indexOf(currentNode); | ||
if (currentIndex !== -1) { | ||
maxIndex = Math.max(maxIndex, currentIndex); | ||
} | ||
// Recursively check all children of the current node | ||
for (const child of currentNode.children) { | ||
traverseChildren(child); | ||
} | ||
}; | ||
// Start traversal directly with the provided node | ||
traverseChildren(node); | ||
return maxIndex; | ||
} | ||
renderRTTNodes() { | ||
const { glw } = this; | ||
const { txManager } = this.stage; | ||
// Render all associated RTT nodes to their textures | ||
@@ -666,12 +741,6 @@ for (let i = 0; i < this.rttNodes.length; i++) { | ||
const child = node.children[i]; | ||
if (!child) { | ||
continue; | ||
} | ||
child.update(this.stage.deltaTime, { | ||
x: 0, | ||
y: 0, | ||
width: 0, | ||
height: 0, | ||
valid: false, | ||
}); | ||
@@ -678,0 +747,0 @@ this.stage.addQuads(child); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
3
1838593
417
41206