diff --git a/src/dom-renderer/domRenderer.ts b/src/dom-renderer/domRenderer.ts index 9573321..88128b0 100644 --- a/src/dom-renderer/domRenderer.ts +++ b/src/dom-renderer/domRenderer.ts @@ -9,6 +9,7 @@ import * as lng from '@lightningjs/renderer'; import { EventEmitter } from '@lightningjs/renderer/utils'; import { Config } from '../config.js'; import type { + DomRendererMainSettings, ExtractProps, IRendererMain, IRendererNode, @@ -29,6 +30,7 @@ import { isRenderStateInBounds, nodeHasTextureSource, computeRenderStateForNode, + compactString, } from './domRendererUtils.js'; // Feature detection for legacy brousers @@ -297,7 +299,7 @@ function updateNodeStyles(node: DOMNode | DOMText) { if (textProps.fontWeight !== 'normal') { style += `font-weight: ${textProps.fontWeight};`; } - if (textProps.fontStretch !== 'normal') { + if (textProps.fontStretch && textProps.fontStretch !== 'normal') { style += `font-stretch: ${textProps.fontStretch};`; } if (textProps.lineHeight != null) { @@ -774,7 +776,7 @@ function updateNodeStyles(node: DOMNode | DOMText) { } } - node.div.setAttribute('style', style); + node.div.setAttribute('style', compactString(style)); if (node instanceof DOMNode && node !== node.stage.root) { const hasTextureSrc = nodeHasTextureSource(node); @@ -791,6 +793,8 @@ function updateNodeStyles(node: DOMNode | DOMText) { } const textNodesToMeasure = new Set(); +const containTextNodes = new Set(); +let fontLoadingListenerSetup = false; type Size = { width: number; height: number }; @@ -827,12 +831,17 @@ function getElSize(node: DOMNode): Size { */ function updateDOMTextSize(node: DOMText): void { let size: Size; + let dimensionsChanged = false; switch (node.contain) { case 'width': size = getElSize(node); + if (node.props.width !== size.width) { + node.width = size.width; + dimensionsChanged = true; + } if (node.props.height !== size.height) { - node.props.height = size.height; - updateNodeStyles(node); + node.height = size.height; + dimensionsChanged = true; } break; case 'none': @@ -841,19 +850,19 @@ function updateDOMTextSize(node: DOMText): void { node.props.height !== size.height || node.props.width !== size.width ) { - node.props.width = size.width; - node.props.height = size.height; - updateNodeStyles(node); + node.width = size.width; + node.height = size.height; + dimensionsChanged = true; } break; } - if (!node.loaded) { + if (!node.loaded || dimensionsChanged) { const payload: lng.NodeTextLoadedPayload = { type: 'text', dimensions: { - width: node.props.width, - height: node.props.height, + width: node.width, + height: node.height, }, }; node.emit('loaded', payload); @@ -866,21 +875,69 @@ function updateDOMTextMeasurements() { textNodesToMeasure.clear(); } +function shouldTrackContainTextNode(node: DOMText): boolean { + return node.contain === 'width' || node.contain === 'none'; +} + +function syncContainTextNodeTracking(node: DOMText): void { + if (shouldTrackContainTextNode(node)) { + containTextNodes.add(node); + } else { + containTextNodes.delete(node); + } +} + +function scheduleContainTextNodesMeasurement(): void { + if (containTextNodes.size === 0) return; + + containTextNodes.forEach((node) => { + if (node.div.isConnected) { + textNodesToMeasure.add(node); + } + }); + + if (textNodesToMeasure.size > 0) { + setTimeout(updateDOMTextMeasurements); + } +} + +function setupFontLoadingListeners(): void { + if (fontLoadingListenerSetup) return; + if ( + typeof document === 'undefined' || + !(document.fonts as FontFaceSet | undefined) + ) { + return; + } + + const fonts = document.fonts; + if (typeof fonts.addEventListener === 'function') { + fonts.addEventListener('loadingdone', scheduleContainTextNodesMeasurement); + } + + fontLoadingListenerSetup = true; +} + function scheduleUpdateDOMTextMeasurement(node: DOMText) { /* Make sure the font is loaded before measuring */ + setupFontLoadingListeners(); + if (textNodesToMeasure.size === 0) { - const fonts = document.fonts; - if (document.fonts.status === 'loaded') { - setTimeout(updateDOMTextMeasurements); - } else { - if (fonts && fonts.ready && typeof fonts.ready.then === 'function') { + if (typeof document !== 'undefined' && 'fonts' in document) { + const fonts = document.fonts; + if (fonts.status === 'loaded') { + setTimeout(updateDOMTextMeasurements); + } else if (fonts.ready && typeof fonts.ready.then === 'function') { fonts.ready.then(updateDOMTextMeasurements); } else { setTimeout(updateDOMTextMeasurements, 500); } + } else { + // Fallback for devices without FontFaceSet.ready() + setTimeout(updateDOMTextMeasurements, 500); } } @@ -1284,69 +1341,98 @@ export class DOMNode extends EventEmitter implements IRendererNode { set scale(v) { if (this.props.scale === v) return; this.props.scale = v; + this.boundsDirty = true; + this.markChildrenBoundsDirty(); updateNodeStyles(this); } get scaleX() { return this.props.scaleX; } set scaleX(v) { + if (this.props.scaleX === v) return; this.props.scaleX = v; + this.boundsDirty = true; + this.markChildrenBoundsDirty(); updateNodeStyles(this); } get scaleY() { return this.props.scaleY; } set scaleY(v) { + if (this.props.scaleY === v) return; this.props.scaleY = v; + this.boundsDirty = true; + this.markChildrenBoundsDirty(); updateNodeStyles(this); } get mount() { return this.props.mount; } set mount(v) { + if (this.props.mount === v) return; this.props.mount = v; + this.boundsDirty = true; + this.markChildrenBoundsDirty(); updateNodeStyles(this); } get mountX() { return this.props.mountX; } set mountX(v) { + if (this.props.mountX === v) return; this.props.mountX = v; + this.boundsDirty = true; + this.markChildrenBoundsDirty(); updateNodeStyles(this); } get mountY() { return this.props.mountY; } set mountY(v) { + if (this.props.mountY === v) return; this.props.mountY = v; + this.boundsDirty = true; + this.markChildrenBoundsDirty(); updateNodeStyles(this); } get pivot() { return this.props.pivot; } set pivot(v) { + if (this.props.pivot === v) return; this.props.pivot = v; + this.boundsDirty = true; + this.markChildrenBoundsDirty(); updateNodeStyles(this); } get pivotX() { return this.props.pivotX; } set pivotX(v) { + if (this.props.pivotX === v) return; this.props.pivotX = v; + this.boundsDirty = true; + this.markChildrenBoundsDirty(); updateNodeStyles(this); } get pivotY() { return this.props.pivotY; } set pivotY(v) { + if (this.props.pivotY === v) return; this.props.pivotY = v; + this.boundsDirty = true; + this.markChildrenBoundsDirty(); updateNodeStyles(this); } get rotation() { return this.props.rotation; } set rotation(v) { + if (this.props.rotation === v) return; this.props.rotation = v; + this.boundsDirty = true; + this.markChildrenBoundsDirty(); updateNodeStyles(this); } get rtt() { @@ -1446,9 +1532,16 @@ class DOMText extends DOMNode { ) { super(stage, props); this.div.innerText = props.text; + syncContainTextNodeTracking(this); scheduleUpdateDOMTextMeasurement(this); } + override destroy(): void { + textNodesToMeasure.delete(this); + containTextNodes.delete(this); + super.destroy(); + } + get text() { return this.props.text; } @@ -1552,6 +1645,7 @@ class DOMText extends DOMNode { set contain(v) { if (this.props.contain === v) return; this.props.contain = v; + syncContainTextNodeTracking(this); updateNodeStyles(this); scheduleUpdateDOMTextMeasurement(this); } @@ -1637,7 +1731,7 @@ export class DOMRendererMain implements IRendererMain { new Map(); constructor( - public settings: Partial, + public settings: DomRendererMainSettings, rawTarget: string | HTMLElement, ) { let target: HTMLElement; diff --git a/src/dom-renderer/domRendererTypes.ts b/src/dom-renderer/domRendererTypes.ts index 2a2c165..9a2a6f0 100644 --- a/src/dom-renderer/domRendererTypes.ts +++ b/src/dom-renderer/domRendererTypes.ts @@ -105,3 +105,26 @@ export interface IRendererMain extends IEventEmitter { createTexture: typeof lng.RendererMain.prototype.createTexture; createEffect: typeof lng.RendererMain.prototype.createEffect; } + +export interface DomRendererMainSettings { + /** + * The logical width of the application (default: 1920) + */ + appWidth?: number; + + /** + * The logical height of the application (default: 1080) + */ + appHeight?: number; + + /** + * Device logical pixel ratio (default: 1) + */ + deviceLogicalPixelRatio?: number; + + /** + * Bounds margin for the renderer + * Can be a single number (applied to all sides) or an array [top, right, bottom, left] + */ + boundsMargin?: number | [number, number, number, number]; +} diff --git a/src/dom-renderer/domRendererUtils.ts b/src/dom-renderer/domRendererUtils.ts index 6fea3f4..ae097dd 100644 --- a/src/dom-renderer/domRendererUtils.ts +++ b/src/dom-renderer/domRendererUtils.ts @@ -194,6 +194,10 @@ export function interpolateProp( : interpolate(start, end, t); } +export function compactString(input: string): string { + return input.replace(/\s*\n\s*/g, ' '); +} + // #region Renderer State Utils export function isRenderStateInBounds(state: lng.CoreNodeRenderState): boolean { diff --git a/src/lightningInit.ts b/src/lightningInit.ts index 6eb598e..2bc7019 100644 --- a/src/lightningInit.ts +++ b/src/lightningInit.ts @@ -1,10 +1,7 @@ import * as lng from '@lightningjs/renderer'; -import { - DOMRendererMain, - isDomRenderer, - loadFontToDom, -} from './dom-renderer/domRenderer.js'; +import { DOMRendererMain, loadFontToDom } from './dom-renderer/domRenderer.js'; import { Config, DOM_RENDERING } from './config.js'; +import { DomRendererMainSettings } from './dom-renderer/domRendererTypes.js'; export type SdfFontType = 'ssdf' | 'msdf'; // Global renderer instance: can be either the Lightning or DOM implementation @@ -13,14 +10,14 @@ export let renderer: lng.RendererMain | DOMRendererMain; export const getRenderer = () => renderer; export function startLightningRenderer( - options: lng.RendererMainSettings, + options: lng.RendererMainSettings | DomRendererMainSettings, rootId: string | HTMLElement = 'app', ) { const enableDomRenderer = DOM_RENDERING && Config.domRendererEnabled; renderer = enableDomRenderer ? new DOMRendererMain(options, rootId) - : new lng.RendererMain(options, rootId); + : new lng.RendererMain(options as lng.RendererMainSettings, rootId); return renderer; } @@ -30,6 +27,7 @@ export function loadFonts( | (Partial & { type: SdfFontType }) )[], ) { + const enableDomRenderer = DOM_RENDERING && Config.domRendererEnabled; for (const font of fonts) { // WebGL — SDF if ( @@ -46,7 +44,7 @@ export function loadFonts( } // Canvas — Web else if ('fontUrl' in font) { - if (DOM_RENDERING && isDomRenderer(renderer)) { + if (enableDomRenderer) { loadFontToDom(font); } else { renderer.stage.fontManager.addFontFace(new lng.WebTrFontFace(font));