Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 227 additions & 48 deletions src/dom-renderer/domRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,72 @@ function updateNodeParent(node: DOMNode | DOMText) {
}
}

// getNodeLineHeight moved to domRendererUtils.ts
/**
* 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[] = [];

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(' ');
}

/**
* 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);
const s = node.div.style;

if (transform.length > 0) {
s.transform = `${transform}`;
s.backfaceVisibility = 'hidden';
} else {
s.transform = '';
s.backfaceVisibility = '';
}

updateRenderStateIfNeeded(node);
}

/**
* 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;
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;
Expand All @@ -250,29 +315,11 @@ 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};`;
style += `backface-visibility: hidden;`;
style += `-webkit-backface-visibility: hidden;`;
}
}

Expand Down Expand Up @@ -392,10 +439,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)};`;
Expand Down Expand Up @@ -455,7 +501,7 @@ function updateNodeStyles(node: DOMNode | DOMText) {
if (supportsMixBlendMode) {
imgStyleParts.push('mix-blend-mode: multiply');
} else {
imgStyleParts.push('opacity: 0.9');
imgStyleParts.push('opacity: 1');
}
}

Expand Down Expand Up @@ -607,6 +653,16 @@ function updateNodeStyles(node: DOMNode | DOMText) {
}
}

// 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 !== '' ||
bgStyle !== '';
}

style += radiusStyle;

if (needsBackgroundLayer) {
Expand Down Expand Up @@ -768,6 +824,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;
Expand Down Expand Up @@ -804,20 +988,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<DOMText>();
Expand Down Expand Up @@ -1091,6 +1268,8 @@ export class DOMNode extends EventEmitter implements IRendererNode {
| null = null;
boundsDirty = true;
children = new Set<DOMNode>();
/** Cached result of the last updateNodeStyles call — avoids redundant setAttribute writes. */
_lastStyleStr: string = '';

id = ++lastNodeId;

Expand Down Expand Up @@ -1227,7 +1406,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;
Expand All @@ -1237,7 +1416,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;
Expand Down Expand Up @@ -1391,7 +1570,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;
Expand All @@ -1401,7 +1580,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;
Expand All @@ -1411,7 +1590,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;
Expand All @@ -1421,7 +1600,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;
Expand All @@ -1431,7 +1610,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;
Expand All @@ -1441,7 +1620,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;
Expand Down Expand Up @@ -1481,7 +1660,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;
Expand Down
Loading