From fc9becaab6dc7ad20796f4015dedd587284f7362 Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Mon, 27 Apr 2026 10:47:23 +0200 Subject: [PATCH 01/13] fix: update node styles to support white-space wrapping --- src/dom-renderer/domRenderer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dom-renderer/domRenderer.ts b/src/dom-renderer/domRenderer.ts index 334632e..7c03071 100644 --- a/src/dom-renderer/domRenderer.ts +++ b/src/dom-renderer/domRenderer.ts @@ -327,6 +327,8 @@ function updateNodeStyles(node: DOMNode | DOMText) { break; } + style += `white-space: pre-wrap;`; + if (maxLines !== Infinity) { // https://stackoverflow.com/a/13924997 style += `display: -webkit-box; @@ -628,6 +630,7 @@ function updateNodeStyles(node: DOMNode | DOMText) { if (!node.imgEl) { node.imgEl = document.createElement('img'); node.imgEl.alt = ''; + node.imgEl.crossOrigin = 'anonymous'; node.imgEl.setAttribute('aria-hidden', 'true'); node.imgEl.setAttribute('loading', 'lazy'); node.imgEl.removeAttribute('src'); From 9333be0cb04685c85499fcc2652a5b17dd698837 Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Mon, 27 Apr 2026 12:51:46 +0200 Subject: [PATCH 02/13] fix: improve image loading handling with opacity transition Co-authored-by: Copilot --- src/dom-renderer/domRenderer.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/dom-renderer/domRenderer.ts b/src/dom-renderer/domRenderer.ts index 7c03071..ec35454 100644 --- a/src/dom-renderer/domRenderer.ts +++ b/src/dom-renderer/domRenderer.ts @@ -421,6 +421,8 @@ function updateNodeStyles(node: DOMNode | DOMText) { 'bottom: 0', 'display: block', 'pointer-events: none', + `opacity: ${node.imageLoading ? 0 : 1}`, + 'transition: opacity 100ms linear', ]; if (props.textureOptions.resizeMode?.type) { @@ -663,10 +665,17 @@ function updateNodeStyles(node: DOMNode | DOMText) { supportsObjectFit, supportsObjectPosition, ); + + // Reveal only after final fit/positioning is applied + if (node.imgEl) { + node.imageLoading = false; + node.imgEl.style.opacity = '1'; + } node.emit('loaded', payload); }); node.imgEl.addEventListener('error', () => { + node.imageLoading = false; if (node.imgEl) { node.imgEl.removeAttribute('src'); node.imgEl.style.display = 'none'; @@ -1055,6 +1064,7 @@ export class DOMNode extends EventEmitter implements IRendererNode { divBg: HTMLElement | undefined; divBorder: HTMLElement | undefined; imgEl: HTMLImageElement | undefined; + imageLoading = false; lazyImagePendingSrc: string | null = null; lazyImageSubTextureProps: | InstanceType['props'] @@ -1163,6 +1173,9 @@ export class DOMNode extends EventEmitter implements IRendererNode { const pendingSrc = this.lazyImagePendingSrc; if (!pendingSrc) return; if (this.imgEl.dataset.rawSrc === pendingSrc) return; + // Hide transient frame while source is loading and being fitted. + this.imageLoading = true; + this.imgEl.style.opacity = '0'; this.imgEl.style.display = ''; this.imgEl.dataset.pendingSrc = pendingSrc; this.imgEl.src = pendingSrc; From d55dc797a98fe90513057359bea7414d87a50618 Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Mon, 27 Apr 2026 15:46:10 +0200 Subject: [PATCH 03/13] fix: enhance image loading handling with background layer visibility Co-authored-by: Copilot --- src/dom-renderer/domRenderer.ts | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/dom-renderer/domRenderer.ts b/src/dom-renderer/domRenderer.ts index ec35454..dd21065 100644 --- a/src/dom-renderer/domRenderer.ts +++ b/src/dom-renderer/domRenderer.ts @@ -617,6 +617,16 @@ function updateNodeStyles(node: DOMNode | DOMText) { node.div.insertBefore(node.divBg, node.div.firstChild); } + const isSyncSubtextureUpdate = + rawImgSrc != null && + srcPos != null && + !!node.imgEl && + node.imgEl.complete && + node.imgEl.dataset.rawSrc === rawImgSrc; + if (isSyncSubtextureUpdate) { + node.imageLoading = true; + } + let bgLayerStyle = 'position: absolute; top:0; left:0; right:0; bottom:0; z-index: -1; pointer-events: none; overflow: hidden;'; if (bgStyle) { @@ -625,6 +635,9 @@ function updateNodeStyles(node: DOMNode | DOMText) { if (maskStyle) { bgLayerStyle += maskStyle; } + if (hasDivBgTint && srcPos != null && node.imageLoading) { + bgLayerStyle += 'opacity: 0;'; + } node.divBg.setAttribute('style', bgLayerStyle + radiusStyle); @@ -671,11 +684,13 @@ function updateNodeStyles(node: DOMNode | DOMText) { node.imageLoading = false; node.imgEl.style.opacity = '1'; } + node.showBackgroundLayer(); node.emit('loaded', payload); }); node.imgEl.addEventListener('error', () => { node.imageLoading = false; + node.showBackgroundLayer(); if (node.imgEl) { node.imgEl.removeAttribute('src'); node.imgEl.style.display = 'none'; @@ -719,6 +734,11 @@ function updateNodeStyles(node: DOMNode | DOMText) { node.imgEl.dataset.rawSrc === rawImgSrc ) { applySubTextureScaling(node, node.imgEl, srcPos); + if (node.imageLoading) { + node.imageLoading = false; + node.imgEl.style.opacity = '1'; + node.showBackgroundLayer(); + } } if ( !srcPos && @@ -1168,6 +1188,21 @@ export class DOMNode extends EventEmitter implements IRendererNode { } } + showBackgroundLayer() { + if (this.divBg) { + this.divBg.style.opacity = '1'; + } + } + + hideMaskedBackgroundLayer() { + if ( + this.divBg && + (this.divBg.style.maskImage || this.divBg.style.webkitMaskImage) + ) { + this.divBg.style.opacity = '0'; + } + } + applyPendingImageSrc() { if (!this.imgEl) return; const pendingSrc = this.lazyImagePendingSrc; @@ -1176,6 +1211,7 @@ export class DOMNode extends EventEmitter implements IRendererNode { // Hide transient frame while source is loading and being fitted. this.imageLoading = true; this.imgEl.style.opacity = '0'; + this.hideMaskedBackgroundLayer(); this.imgEl.style.display = ''; this.imgEl.dataset.pendingSrc = pendingSrc; this.imgEl.src = pendingSrc; From d6f86890b6e421a64aba67dcc149adbed6b16a58 Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Tue, 5 May 2026 16:40:07 +0200 Subject: [PATCH 04/13] fix(dom): enhance node styles with perspective and backface visibility --- src/dom-renderer/domRenderer.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dom-renderer/domRenderer.ts b/src/dom-renderer/domRenderer.ts index dd21065..478d2fa 100644 --- a/src/dom-renderer/domRenderer.ts +++ b/src/dom-renderer/domRenderer.ts @@ -272,7 +272,11 @@ function updateNodeStyles(node: DOMNode | DOMText) { } if (transform.length > 0) { - style += `transform: ${transform};`; + style += `transform: ${transform} translateZ(0);`; + style += `backface-visibility: hidden;`; + style += `-webkit-backface-visibility: hidden;`; + style += `perspective: 1000px;`; + style += `-webkit-perspective: 1000px;`; } } From 86b59c353ae137053935dbac4e4153b497b57d06 Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Wed, 6 May 2026 15:56:56 +0200 Subject: [PATCH 05/13] fix(dom): optimize transform updates and reduce redundant style writes --- src/dom-renderer/domRenderer.ts | 130 +++++++++++++++++++++----------- 1 file changed, 86 insertions(+), 44 deletions(-) diff --git a/src/dom-renderer/domRenderer.ts b/src/dom-renderer/domRenderer.ts index 478d2fa..125ac03 100644 --- a/src/dom-renderer/domRenderer.ts +++ b/src/dom-renderer/domRenderer.ts @@ -237,6 +237,73 @@ function updateNodeParent(node: DOMNode | DOMText) { // getNodeLineHeight moved to domRendererUtils.ts +function buildTransformCSS(props: IRendererNodeProps): string { + const transforms: string[] = []; + + const { x, y } = props; + const hasMountX = props.mountX != null && props.mountX !== 0; + const hasMountY = props.mountY != null && props.mountY !== 0; + + if (x !== 0) transforms.push(`translateX(${x}px)`); + if (hasMountX) transforms.push(`translateX(${-props.mountX! * 100}%)`); + + if (y !== 0) transforms.push(`translateY(${y}px)`); + if (hasMountY) transforms.push(`translateY(${-props.mountY! * 100}%)`); + + if (props.rotation !== 0) transforms.push(`rotate(${props.rotation}rad)`); + + if (props.scale !== 1 && props.scale != null) { + transforms.push(`scale(${props.scale})`); + } else { + if (props.scaleX !== 1) transforms.push(`scaleX(${props.scaleX})`); + if (props.scaleY !== 1) transforms.push(`scaleY(${props.scaleY})`); + } + + return transforms.join(' '); +} + +/** + * Updates only the CSS transform of a node without rebuilding the full style + * string. Use this fast path when only positional/transform props change + * (x, y, rotation, scale, mount), since those are the most frequent updates + * during scrolling and animation. + */ +function updateTransformOnly(node: DOMNode | DOMText): void { + const transform = buildTransformCSS(node.props); + const s = node.div.style; + + if (transform.length > 0) { + s.transform = `${transform} translateZ(0)`; + s.backfaceVisibility = 'hidden'; + s.webkitBackfaceVisibility = 'hidden'; + } else { + s.transform = ''; + s.backfaceVisibility = ''; + s.webkitBackfaceVisibility = ''; + } + + updateRenderStateIfNeeded(node); +} + +/** + * Re-evaluates the render state (in-bounds / viewport) for a node. + * Extracted from updateNodeStyles so both full and fast-path updates + * can trigger it without duplicating logic. + */ +function updateRenderStateIfNeeded(node: DOMNode | DOMText): void { + if (!(node instanceof DOMNode) || node === node.stage.root) return; + const hasTextureSrc = nodeHasTextureSource(node); + if (hasTextureSrc && node.boundsDirty) { + const next = computeRenderStateForNode(node); + if (next != null) { + node.updateRenderState(next); + } + node.boundsDirty = false; + } else if (!hasTextureSrc) { + node.boundsDirty = false; + } +} + function updateNodeStyles(node: DOMNode | DOMText) { let { props } = node; @@ -250,27 +317,7 @@ function updateNodeStyles(node: DOMNode | DOMText) { // Transform { - let transform = ''; - - let { x, y } = props; - const hasMountX = props.mountX != null && props.mountX !== 0; - const hasMountY = props.mountY != null && props.mountY !== 0; - - if (x !== 0) transform += `translateX(${x}px)`; - if (hasMountX) transform += `translateX(${-props.mountX! * 100}%)`; - - if (y !== 0) transform += `translateY(${y}px)`; - if (hasMountY) transform += `translateY(${-props.mountY! * 100}%)`; - - if (props.rotation !== 0) transform += `rotate(${props.rotation}rad)`; - - if (props.scale !== 1 && props.scale != null) { - transform += `scale(${props.scale})`; - } else { - if (props.scaleX !== 1) transform += `scaleX(${props.scaleX})`; - if (props.scaleY !== 1) transform += `scaleY(${props.scaleY})`; - } - + const transform = buildTransformCSS(props); if (transform.length > 0) { style += `transform: ${transform} translateZ(0);`; style += `backface-visibility: hidden;`; @@ -459,7 +506,7 @@ function updateNodeStyles(node: DOMNode | DOMText) { if (supportsMixBlendMode) { imgStyleParts.push('mix-blend-mode: multiply'); } else { - imgStyleParts.push('opacity: 0.9'); + imgStyleParts.push('opacity: 1'); } } @@ -808,20 +855,13 @@ function updateNodeStyles(node: DOMNode | DOMText) { } } - node.div.setAttribute('style', compactString(style)); - - if (node instanceof DOMNode && node !== node.stage.root) { - const hasTextureSrc = nodeHasTextureSource(node); - if (hasTextureSrc && node.boundsDirty) { - const next = computeRenderStateForNode(node); - if (next != null) { - node.updateRenderState(next); - } - node.boundsDirty = false; - } else if (!hasTextureSrc) { - node.boundsDirty = false; - } + const newStyle = compactString(style); + if (node._lastStyleStr !== newStyle) { + node._lastStyleStr = newStyle; + node.div.setAttribute('style', newStyle); } + + updateRenderStateIfNeeded(node); } const textNodesToMeasure = new Set(); @@ -1095,6 +1135,8 @@ export class DOMNode extends EventEmitter implements IRendererNode { | null = null; boundsDirty = true; children = new Set(); + /** Cached result of the last updateNodeStyles call — avoids redundant setAttribute writes. */ + _lastStyleStr: string = ''; id = ++lastNodeId; @@ -1231,7 +1273,7 @@ export class DOMNode extends EventEmitter implements IRendererNode { this.props.x = v; this.boundsDirty = true; this.markChildrenBoundsDirty(); - updateNodeStyles(this); + updateTransformOnly(this); } get y() { return this.props.y; @@ -1241,7 +1283,7 @@ export class DOMNode extends EventEmitter implements IRendererNode { this.props.y = v; this.boundsDirty = true; this.markChildrenBoundsDirty(); - updateNodeStyles(this); + updateTransformOnly(this); } get width() { return this.props.width; @@ -1395,7 +1437,7 @@ export class DOMNode extends EventEmitter implements IRendererNode { this.props.scale = v; this.boundsDirty = true; this.markChildrenBoundsDirty(); - updateNodeStyles(this); + updateTransformOnly(this); } get scaleX() { return this.props.scaleX; @@ -1405,7 +1447,7 @@ export class DOMNode extends EventEmitter implements IRendererNode { this.props.scaleX = v; this.boundsDirty = true; this.markChildrenBoundsDirty(); - updateNodeStyles(this); + updateTransformOnly(this); } get scaleY() { return this.props.scaleY; @@ -1415,7 +1457,7 @@ export class DOMNode extends EventEmitter implements IRendererNode { this.props.scaleY = v; this.boundsDirty = true; this.markChildrenBoundsDirty(); - updateNodeStyles(this); + updateTransformOnly(this); } get mount() { return this.props.mount; @@ -1425,7 +1467,7 @@ export class DOMNode extends EventEmitter implements IRendererNode { this.props.mount = v; this.boundsDirty = true; this.markChildrenBoundsDirty(); - updateNodeStyles(this); + updateTransformOnly(this); } get mountX() { return this.props.mountX; @@ -1435,7 +1477,7 @@ export class DOMNode extends EventEmitter implements IRendererNode { this.props.mountX = v; this.boundsDirty = true; this.markChildrenBoundsDirty(); - updateNodeStyles(this); + updateTransformOnly(this); } get mountY() { return this.props.mountY; @@ -1445,7 +1487,7 @@ export class DOMNode extends EventEmitter implements IRendererNode { this.props.mountY = v; this.boundsDirty = true; this.markChildrenBoundsDirty(); - updateNodeStyles(this); + updateTransformOnly(this); } get pivot() { return this.props.pivot; @@ -1485,7 +1527,7 @@ export class DOMNode extends EventEmitter implements IRendererNode { this.props.rotation = v; this.boundsDirty = true; this.markChildrenBoundsDirty(); - updateNodeStyles(this); + updateTransformOnly(this); } get rtt() { return this.props.rtt; From 1b7ad88fc6017ce201ee88f5da3ea2a71687cb3e Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Wed, 6 May 2026 16:14:09 +0200 Subject: [PATCH 06/13] fix: removed comment --- src/dom-renderer/domRenderer.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/dom-renderer/domRenderer.ts b/src/dom-renderer/domRenderer.ts index 125ac03..b5148f9 100644 --- a/src/dom-renderer/domRenderer.ts +++ b/src/dom-renderer/domRenderer.ts @@ -235,8 +235,6 @@ function updateNodeParent(node: DOMNode | DOMText) { } } -// getNodeLineHeight moved to domRendererUtils.ts - function buildTransformCSS(props: IRendererNodeProps): string { const transforms: string[] = []; From d66d8454cd2121391f8565706f6b91aba6522aed Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Wed, 6 May 2026 16:18:16 +0200 Subject: [PATCH 07/13] doc: added comments --- src/dom-renderer/domRenderer.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/dom-renderer/domRenderer.ts b/src/dom-renderer/domRenderer.ts index b5148f9..ec2e5f9 100644 --- a/src/dom-renderer/domRenderer.ts +++ b/src/dom-renderer/domRenderer.ts @@ -235,6 +235,10 @@ function updateNodeParent(node: DOMNode | DOMText) { } } +/** + * Builds the CSS transform string from node props (x, y, rotation, scale, mount). + * Returns an empty string if no transform is needed. + */ function buildTransformCSS(props: IRendererNodeProps): string { const transforms: string[] = []; @@ -261,10 +265,9 @@ function buildTransformCSS(props: IRendererNodeProps): string { } /** - * Updates only the CSS transform of a node without rebuilding the full style - * string. Use this fast path when only positional/transform props change - * (x, y, rotation, scale, mount), since those are the most frequent updates - * during scrolling and animation. + * Fast path for transform-only updates (x, y, rotation, scale, mount). + * Skips full style rebuild but still re-evaluates viewport bounds for nodes + * with a texture source to drive lazy image load/unload during scroll. */ function updateTransformOnly(node: DOMNode | DOMText): void { const transform = buildTransformCSS(node.props); @@ -284,9 +287,8 @@ function updateTransformOnly(node: DOMNode | DOMText): void { } /** - * Re-evaluates the render state (in-bounds / viewport) for a node. - * Extracted from updateNodeStyles so both full and fast-path updates - * can trigger it without duplicating logic. + * Recomputes the viewport bounds state for a node with a texture source and, + * if changed, triggers lazy image load or unload via updateRenderState. */ function updateRenderStateIfNeeded(node: DOMNode | DOMText): void { if (!(node instanceof DOMNode) || node === node.stage.root) return; From 2a56de10c385d85972345aeefe57f3676304eb34 Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Wed, 6 May 2026 17:10:58 +0200 Subject: [PATCH 08/13] fix(dom): removed imgBg if not needed --- src/dom-renderer/domRenderer.ts | 138 +++++++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 3 deletions(-) diff --git a/src/dom-renderer/domRenderer.ts b/src/dom-renderer/domRenderer.ts index ec2e5f9..bfd8ab2 100644 --- a/src/dom-renderer/domRenderer.ts +++ b/src/dom-renderer/domRenderer.ts @@ -443,10 +443,9 @@ function updateNodeStyles(node: DOMNode | DOMText) { let imgStyle = ''; let hasDivBgTint = false; + let hasTint = false; if (rawImgSrc) { - needsBackgroundLayer = true; - - const hasTint = props.color !== 0xffffffff && props.color !== 0x00000000; + hasTint = props.color !== 0xffffffff && props.color !== 0x00000000; if (hasTint) { bgStyle += `background-color: ${colorToRgba(props.color)};`; @@ -658,6 +657,11 @@ function updateNodeStyles(node: DOMNode | DOMText) { } } + // If there's still no reason to use divBg, check out tint/gradient/subtexture + if (!needsBackgroundLayer && rawImgSrc) { + needsBackgroundLayer = hasTint || !!gradient || srcPos !== null; + } + style += radiusStyle; if (needsBackgroundLayer) { @@ -819,6 +823,134 @@ function updateNodeStyles(node: DOMNode | DOMText) { node.imgEl = undefined; } } + } else if (rawImgSrc) { + // Image directly in node.div (without divBg) when there is no tint/gradient + + // Cleanup divBg + if (node.divBg) { + node.divBg.remove(); + node.divBg = undefined; + } + + const isSyncSubtextureUpdate = + srcPos != null && + !!node.imgEl && + node.imgEl.complete && + node.imgEl.dataset.rawSrc === rawImgSrc; + if (isSyncSubtextureUpdate) { + node.imageLoading = true; + } + + if (!node.imgEl) { + node.imgEl = document.createElement('img'); + node.imgEl.alt = ''; + node.imgEl.crossOrigin = 'anonymous'; + node.imgEl.setAttribute('aria-hidden', 'true'); + node.imgEl.setAttribute('loading', 'lazy'); + node.imgEl.removeAttribute('src'); + + node.imgEl.addEventListener('load', () => { + const payload: lng.NodeTextureLoadedPayload = { + type: 'texture', + dimensions: { + width: node.imgEl!.naturalWidth, + height: node.imgEl!.naturalHeight, + }, + }; + node.imgEl!.style.display = ''; + applySubTextureScaling( + node, + node.imgEl!, + node.lazyImageSubTextureProps, + ); + + const resizeMode = (node.props.textureOptions as any)?.resizeMode; + const clipX = resizeMode?.clipX ?? 0.5; + const clipY = resizeMode?.clipY ?? 0.5; + computeLegacyObjectFit( + node, + node.imgEl!, + resizeMode, + clipX, + clipY, + node.lazyImageSubTextureProps, + supportsObjectFit, + supportsObjectPosition, + ); + + if (node.imgEl) { + node.imageLoading = false; + node.imgEl.style.opacity = '1'; + } + node.emit('loaded', payload); + }); + + node.imgEl.addEventListener('error', () => { + node.imageLoading = false; + if (node.imgEl) { + node.imgEl.removeAttribute('src'); + node.imgEl.style.display = 'none'; + node.imgEl.removeAttribute('data-rawSrc'); + } + + const failedSrc = + node.imgEl?.dataset.pendingSrc || node.lazyImagePendingSrc || ''; + + const payload: lng.NodeTextureFailedPayload = { + type: 'texture', + error: new Error(`Failed to load image: ${failedSrc}`), + }; + node.emit('failed', payload); + }); + } + + node.lazyImagePendingSrc = rawImgSrc; + node.lazyImageSubTextureProps = srcPos; + node.imgEl.dataset.pendingSrc = rawImgSrc; + + if (node.imgEl.parentElement !== node.div) { + node.div.appendChild(node.imgEl); + } + + node.imgEl.setAttribute('style', imgStyle); + + if (isRenderStateInBounds(node.renderState)) { + node.applyPendingImageSrc(); + } else if (!node.imgEl.dataset.rawSrc) { + node.imgEl.removeAttribute('src'); + } + + if ( + srcPos && + node.imgEl.complete && + node.imgEl.dataset.rawSrc === rawImgSrc + ) { + applySubTextureScaling(node, node.imgEl, srcPos); + if (node.imageLoading) { + node.imageLoading = false; + node.imgEl.style.opacity = '1'; + } + } + if ( + !srcPos && + node.imgEl.complete && + (!supportsObjectFit || !supportsObjectPosition) && + node.imgEl.dataset.rawSrc === rawImgSrc + ) { + const resizeMode = (node.props.textureOptions as any)?.resizeMode; + const clipX = resizeMode?.clipX ?? 0.5; + const clipY = resizeMode?.clipY ?? 0.5; + computeLegacyObjectFit( + node, + node.imgEl, + resizeMode, + clipX, + clipY, + srcPos, + supportsObjectFit, + supportsObjectPosition, + ); + } } else { node.lazyImagePendingSrc = null; node.lazyImageSubTextureProps = null; From cbdcace52d1520303ce3ad4023d43464dc6e3762 Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Wed, 6 May 2026 17:15:58 +0200 Subject: [PATCH 09/13] fix(dom): include radiusStyle in background layer condition --- src/dom-renderer/domRenderer.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/dom-renderer/domRenderer.ts b/src/dom-renderer/domRenderer.ts index bfd8ab2..472cf09 100644 --- a/src/dom-renderer/domRenderer.ts +++ b/src/dom-renderer/domRenderer.ts @@ -657,9 +657,10 @@ function updateNodeStyles(node: DOMNode | DOMText) { } } - // If there's still no reason to use divBg, check out tint/gradient/subtexture + // If there's still no reason to use divBg, check out tint/gradient/subtexture/radius if (!needsBackgroundLayer && rawImgSrc) { - needsBackgroundLayer = hasTint || !!gradient || srcPos !== null; + needsBackgroundLayer = + hasTint || !!gradient || srcPos !== null || radiusStyle !== ''; } style += radiusStyle; From a38e78d2a8f723b7ca4287e43941199ab1c1cea3 Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Wed, 6 May 2026 17:18:36 +0200 Subject: [PATCH 10/13] fix(dom): include bgStyle in background layer condition check --- src/dom-renderer/domRenderer.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/dom-renderer/domRenderer.ts b/src/dom-renderer/domRenderer.ts index 472cf09..ddf2cb3 100644 --- a/src/dom-renderer/domRenderer.ts +++ b/src/dom-renderer/domRenderer.ts @@ -657,10 +657,14 @@ function updateNodeStyles(node: DOMNode | DOMText) { } } - // If there's still no reason to use divBg, check out tint/gradient/subtexture/radius + // If there's still no reason to use divBg, check out tint/gradient/subtexture/radius/bgStyle if (!needsBackgroundLayer && rawImgSrc) { needsBackgroundLayer = - hasTint || !!gradient || srcPos !== null || radiusStyle !== ''; + hasTint || + !!gradient || + srcPos !== null || + radiusStyle !== '' || + bgStyle !== ''; } style += radiusStyle; From 8ab51f0d7bf78f069e3284d4d5a12c7ca3ce5643 Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Wed, 6 May 2026 18:22:22 +0200 Subject: [PATCH 11/13] fix(dom): simplify transform and backface visibility handling --- src/dom-renderer/domRenderer.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/dom-renderer/domRenderer.ts b/src/dom-renderer/domRenderer.ts index ddf2cb3..ac6e14c 100644 --- a/src/dom-renderer/domRenderer.ts +++ b/src/dom-renderer/domRenderer.ts @@ -274,13 +274,11 @@ function updateTransformOnly(node: DOMNode | DOMText): void { const s = node.div.style; if (transform.length > 0) { - s.transform = `${transform} translateZ(0)`; + s.transform = `${transform}`; s.backfaceVisibility = 'hidden'; - s.webkitBackfaceVisibility = 'hidden'; } else { s.transform = ''; s.backfaceVisibility = ''; - s.webkitBackfaceVisibility = ''; } updateRenderStateIfNeeded(node); @@ -319,11 +317,9 @@ function updateNodeStyles(node: DOMNode | DOMText) { { const transform = buildTransformCSS(props); if (transform.length > 0) { - style += `transform: ${transform} translateZ(0);`; + style += `transform: ${transform};`; style += `backface-visibility: hidden;`; style += `-webkit-backface-visibility: hidden;`; - style += `perspective: 1000px;`; - style += `-webkit-perspective: 1000px;`; } } From 325a16e5414d91f30b9d9c11646b1c3597e27fa9 Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Thu, 7 May 2026 13:49:12 +0200 Subject: [PATCH 12/13] fix(dom): remove backface visibility handling and update background layer style --- src/dom-renderer/domRenderer.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/dom-renderer/domRenderer.ts b/src/dom-renderer/domRenderer.ts index ac6e14c..9b64760 100644 --- a/src/dom-renderer/domRenderer.ts +++ b/src/dom-renderer/domRenderer.ts @@ -275,10 +275,8 @@ function updateTransformOnly(node: DOMNode | DOMText): void { if (transform.length > 0) { s.transform = `${transform}`; - s.backfaceVisibility = 'hidden'; } else { s.transform = ''; - s.backfaceVisibility = ''; } updateRenderStateIfNeeded(node); @@ -318,8 +316,6 @@ function updateNodeStyles(node: DOMNode | DOMText) { const transform = buildTransformCSS(props); if (transform.length > 0) { style += `transform: ${transform};`; - style += `backface-visibility: hidden;`; - style += `-webkit-backface-visibility: hidden;`; } } @@ -684,7 +680,7 @@ function updateNodeStyles(node: DOMNode | DOMText) { } let bgLayerStyle = - 'position: absolute; top:0; left:0; right:0; bottom:0; z-index: -1; pointer-events: none; overflow: hidden;'; + 'position: absolute; top:0; left:0; right:0; bottom:0; z-index: -1; pointer-events: none; -webkit-clip-path: inset(0); clip-path: inset(0);'; if (bgStyle) { bgLayerStyle += bgStyle; } From 7f0c7008e5f4e6e3f5d767920838c03f5d42cccc Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Thu, 7 May 2026 13:51:38 +0200 Subject: [PATCH 13/13] fix(dom): remove backface visibility from node styles update --- src/dom-renderer/domRenderer.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/dom-renderer/domRenderer.ts b/src/dom-renderer/domRenderer.ts index 34af044..9b64760 100644 --- a/src/dom-renderer/domRenderer.ts +++ b/src/dom-renderer/domRenderer.ts @@ -316,8 +316,6 @@ function updateNodeStyles(node: DOMNode | DOMText) { const transform = buildTransformCSS(props); if (transform.length > 0) { style += `transform: ${transform};`; - style += `backface-visibility: hidden;`; - style += `-webkit-backface-visibility: hidden;`; } }