Skip to content

Commit 66800bc

Browse files
authored
feat: enhance DOM renderer with font loading and measurement improvements (#80)
* feat: enhance DOM renderer with font loading and measurement improvements * refactor: remove unused import
1 parent 89752a6 commit 66800bc

4 files changed

Lines changed: 143 additions & 24 deletions

File tree

src/dom-renderer/domRenderer.ts

Lines changed: 110 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as lng from '@lightningjs/renderer';
99
import { EventEmitter } from '@lightningjs/renderer/utils';
1010
import { Config } from '../config.js';
1111
import type {
12+
DomRendererMainSettings,
1213
ExtractProps,
1314
IRendererMain,
1415
IRendererNode,
@@ -29,6 +30,7 @@ import {
2930
isRenderStateInBounds,
3031
nodeHasTextureSource,
3132
computeRenderStateForNode,
33+
compactString,
3234
} from './domRendererUtils.js';
3335

3436
// Feature detection for legacy brousers
@@ -297,7 +299,7 @@ function updateNodeStyles(node: DOMNode | DOMText) {
297299
if (textProps.fontWeight !== 'normal') {
298300
style += `font-weight: ${textProps.fontWeight};`;
299301
}
300-
if (textProps.fontStretch !== 'normal') {
302+
if (textProps.fontStretch && textProps.fontStretch !== 'normal') {
301303
style += `font-stretch: ${textProps.fontStretch};`;
302304
}
303305
if (textProps.lineHeight != null) {
@@ -774,7 +776,7 @@ function updateNodeStyles(node: DOMNode | DOMText) {
774776
}
775777
}
776778

777-
node.div.setAttribute('style', style);
779+
node.div.setAttribute('style', compactString(style));
778780

779781
if (node instanceof DOMNode && node !== node.stage.root) {
780782
const hasTextureSrc = nodeHasTextureSource(node);
@@ -791,6 +793,8 @@ function updateNodeStyles(node: DOMNode | DOMText) {
791793
}
792794

793795
const textNodesToMeasure = new Set<DOMText>();
796+
const containTextNodes = new Set<DOMText>();
797+
let fontLoadingListenerSetup = false;
794798

795799
type Size = { width: number; height: number };
796800

@@ -827,12 +831,17 @@ function getElSize(node: DOMNode): Size {
827831
*/
828832
function updateDOMTextSize(node: DOMText): void {
829833
let size: Size;
834+
let dimensionsChanged = false;
830835
switch (node.contain) {
831836
case 'width':
832837
size = getElSize(node);
838+
if (node.props.width !== size.width) {
839+
node.width = size.width;
840+
dimensionsChanged = true;
841+
}
833842
if (node.props.height !== size.height) {
834-
node.props.height = size.height;
835-
updateNodeStyles(node);
843+
node.height = size.height;
844+
dimensionsChanged = true;
836845
}
837846
break;
838847
case 'none':
@@ -841,19 +850,19 @@ function updateDOMTextSize(node: DOMText): void {
841850
node.props.height !== size.height ||
842851
node.props.width !== size.width
843852
) {
844-
node.props.width = size.width;
845-
node.props.height = size.height;
846-
updateNodeStyles(node);
853+
node.width = size.width;
854+
node.height = size.height;
855+
dimensionsChanged = true;
847856
}
848857
break;
849858
}
850859

851-
if (!node.loaded) {
860+
if (!node.loaded || dimensionsChanged) {
852861
const payload: lng.NodeTextLoadedPayload = {
853862
type: 'text',
854863
dimensions: {
855-
width: node.props.width,
856-
height: node.props.height,
864+
width: node.width,
865+
height: node.height,
857866
},
858867
};
859868
node.emit('loaded', payload);
@@ -866,21 +875,69 @@ function updateDOMTextMeasurements() {
866875
textNodesToMeasure.clear();
867876
}
868877

878+
function shouldTrackContainTextNode(node: DOMText): boolean {
879+
return node.contain === 'width' || node.contain === 'none';
880+
}
881+
882+
function syncContainTextNodeTracking(node: DOMText): void {
883+
if (shouldTrackContainTextNode(node)) {
884+
containTextNodes.add(node);
885+
} else {
886+
containTextNodes.delete(node);
887+
}
888+
}
889+
890+
function scheduleContainTextNodesMeasurement(): void {
891+
if (containTextNodes.size === 0) return;
892+
893+
containTextNodes.forEach((node) => {
894+
if (node.div.isConnected) {
895+
textNodesToMeasure.add(node);
896+
}
897+
});
898+
899+
if (textNodesToMeasure.size > 0) {
900+
setTimeout(updateDOMTextMeasurements);
901+
}
902+
}
903+
904+
function setupFontLoadingListeners(): void {
905+
if (fontLoadingListenerSetup) return;
906+
if (
907+
typeof document === 'undefined' ||
908+
!(document.fonts as FontFaceSet | undefined)
909+
) {
910+
return;
911+
}
912+
913+
const fonts = document.fonts;
914+
if (typeof fonts.addEventListener === 'function') {
915+
fonts.addEventListener('loadingdone', scheduleContainTextNodesMeasurement);
916+
}
917+
918+
fontLoadingListenerSetup = true;
919+
}
920+
869921
function scheduleUpdateDOMTextMeasurement(node: DOMText) {
870922
/*
871923
Make sure the font is loaded before measuring
872924
*/
873925

926+
setupFontLoadingListeners();
927+
874928
if (textNodesToMeasure.size === 0) {
875-
const fonts = document.fonts;
876-
if (document.fonts.status === 'loaded') {
877-
setTimeout(updateDOMTextMeasurements);
878-
} else {
879-
if (fonts && fonts.ready && typeof fonts.ready.then === 'function') {
929+
if (typeof document !== 'undefined' && 'fonts' in document) {
930+
const fonts = document.fonts;
931+
if (fonts.status === 'loaded') {
932+
setTimeout(updateDOMTextMeasurements);
933+
} else if (fonts.ready && typeof fonts.ready.then === 'function') {
880934
fonts.ready.then(updateDOMTextMeasurements);
881935
} else {
882936
setTimeout(updateDOMTextMeasurements, 500);
883937
}
938+
} else {
939+
// Fallback for devices without FontFaceSet.ready()
940+
setTimeout(updateDOMTextMeasurements, 500);
884941
}
885942
}
886943

@@ -1284,69 +1341,98 @@ export class DOMNode extends EventEmitter implements IRendererNode {
12841341
set scale(v) {
12851342
if (this.props.scale === v) return;
12861343
this.props.scale = v;
1344+
this.boundsDirty = true;
1345+
this.markChildrenBoundsDirty();
12871346
updateNodeStyles(this);
12881347
}
12891348
get scaleX() {
12901349
return this.props.scaleX;
12911350
}
12921351
set scaleX(v) {
1352+
if (this.props.scaleX === v) return;
12931353
this.props.scaleX = v;
1354+
this.boundsDirty = true;
1355+
this.markChildrenBoundsDirty();
12941356
updateNodeStyles(this);
12951357
}
12961358
get scaleY() {
12971359
return this.props.scaleY;
12981360
}
12991361
set scaleY(v) {
1362+
if (this.props.scaleY === v) return;
13001363
this.props.scaleY = v;
1364+
this.boundsDirty = true;
1365+
this.markChildrenBoundsDirty();
13011366
updateNodeStyles(this);
13021367
}
13031368
get mount() {
13041369
return this.props.mount;
13051370
}
13061371
set mount(v) {
1372+
if (this.props.mount === v) return;
13071373
this.props.mount = v;
1374+
this.boundsDirty = true;
1375+
this.markChildrenBoundsDirty();
13081376
updateNodeStyles(this);
13091377
}
13101378
get mountX() {
13111379
return this.props.mountX;
13121380
}
13131381
set mountX(v) {
1382+
if (this.props.mountX === v) return;
13141383
this.props.mountX = v;
1384+
this.boundsDirty = true;
1385+
this.markChildrenBoundsDirty();
13151386
updateNodeStyles(this);
13161387
}
13171388
get mountY() {
13181389
return this.props.mountY;
13191390
}
13201391
set mountY(v) {
1392+
if (this.props.mountY === v) return;
13211393
this.props.mountY = v;
1394+
this.boundsDirty = true;
1395+
this.markChildrenBoundsDirty();
13221396
updateNodeStyles(this);
13231397
}
13241398
get pivot() {
13251399
return this.props.pivot;
13261400
}
13271401
set pivot(v) {
1402+
if (this.props.pivot === v) return;
13281403
this.props.pivot = v;
1404+
this.boundsDirty = true;
1405+
this.markChildrenBoundsDirty();
13291406
updateNodeStyles(this);
13301407
}
13311408
get pivotX() {
13321409
return this.props.pivotX;
13331410
}
13341411
set pivotX(v) {
1412+
if (this.props.pivotX === v) return;
13351413
this.props.pivotX = v;
1414+
this.boundsDirty = true;
1415+
this.markChildrenBoundsDirty();
13361416
updateNodeStyles(this);
13371417
}
13381418
get pivotY() {
13391419
return this.props.pivotY;
13401420
}
13411421
set pivotY(v) {
1422+
if (this.props.pivotY === v) return;
13421423
this.props.pivotY = v;
1424+
this.boundsDirty = true;
1425+
this.markChildrenBoundsDirty();
13431426
updateNodeStyles(this);
13441427
}
13451428
get rotation() {
13461429
return this.props.rotation;
13471430
}
13481431
set rotation(v) {
1432+
if (this.props.rotation === v) return;
13491433
this.props.rotation = v;
1434+
this.boundsDirty = true;
1435+
this.markChildrenBoundsDirty();
13501436
updateNodeStyles(this);
13511437
}
13521438
get rtt() {
@@ -1446,9 +1532,16 @@ class DOMText extends DOMNode {
14461532
) {
14471533
super(stage, props);
14481534
this.div.innerText = props.text;
1535+
syncContainTextNodeTracking(this);
14491536
scheduleUpdateDOMTextMeasurement(this);
14501537
}
14511538

1539+
override destroy(): void {
1540+
textNodesToMeasure.delete(this);
1541+
containTextNodes.delete(this);
1542+
super.destroy();
1543+
}
1544+
14521545
get text() {
14531546
return this.props.text;
14541547
}
@@ -1552,6 +1645,7 @@ class DOMText extends DOMNode {
15521645
set contain(v) {
15531646
if (this.props.contain === v) return;
15541647
this.props.contain = v;
1648+
syncContainTextNodeTracking(this);
15551649
updateNodeStyles(this);
15561650
scheduleUpdateDOMTextMeasurement(this);
15571651
}
@@ -1637,7 +1731,7 @@ export class DOMRendererMain implements IRendererMain {
16371731
new Map();
16381732

16391733
constructor(
1640-
public settings: Partial<lng.RendererMainSettings>,
1734+
public settings: DomRendererMainSettings,
16411735
rawTarget: string | HTMLElement,
16421736
) {
16431737
let target: HTMLElement;

src/dom-renderer/domRendererTypes.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,26 @@ export interface IRendererMain extends IEventEmitter {
105105
createTexture: typeof lng.RendererMain.prototype.createTexture;
106106
createEffect: typeof lng.RendererMain.prototype.createEffect;
107107
}
108+
109+
export interface DomRendererMainSettings {
110+
/**
111+
* The logical width of the application (default: 1920)
112+
*/
113+
appWidth?: number;
114+
115+
/**
116+
* The logical height of the application (default: 1080)
117+
*/
118+
appHeight?: number;
119+
120+
/**
121+
* Device logical pixel ratio (default: 1)
122+
*/
123+
deviceLogicalPixelRatio?: number;
124+
125+
/**
126+
* Bounds margin for the renderer
127+
* Can be a single number (applied to all sides) or an array [top, right, bottom, left]
128+
*/
129+
boundsMargin?: number | [number, number, number, number];
130+
}

src/dom-renderer/domRendererUtils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ export function interpolateProp(
194194
: interpolate(start, end, t);
195195
}
196196

197+
export function compactString(input: string): string {
198+
return input.replace(/\s*\n\s*/g, ' ');
199+
}
200+
197201
// #region Renderer State Utils
198202

199203
export function isRenderStateInBounds(state: lng.CoreNodeRenderState): boolean {

src/lightningInit.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import * as lng from '@lightningjs/renderer';
2-
import {
3-
DOMRendererMain,
4-
isDomRenderer,
5-
loadFontToDom,
6-
} from './dom-renderer/domRenderer.js';
2+
import { DOMRendererMain, loadFontToDom } from './dom-renderer/domRenderer.js';
73
import { Config, DOM_RENDERING } from './config.js';
4+
import { DomRendererMainSettings } from './dom-renderer/domRendererTypes.js';
85

96
export type SdfFontType = 'ssdf' | 'msdf';
107
// Global renderer instance: can be either the Lightning or DOM implementation
@@ -13,14 +10,14 @@ export let renderer: lng.RendererMain | DOMRendererMain;
1310
export const getRenderer = () => renderer;
1411

1512
export function startLightningRenderer(
16-
options: lng.RendererMainSettings,
13+
options: lng.RendererMainSettings | DomRendererMainSettings,
1714
rootId: string | HTMLElement = 'app',
1815
) {
1916
const enableDomRenderer = DOM_RENDERING && Config.domRendererEnabled;
2017

2118
renderer = enableDomRenderer
2219
? new DOMRendererMain(options, rootId)
23-
: new lng.RendererMain(options, rootId);
20+
: new lng.RendererMain(options as lng.RendererMainSettings, rootId);
2421
return renderer;
2522
}
2623

@@ -30,6 +27,7 @@ export function loadFonts(
3027
| (Partial<lng.SdfTrFontFaceOptions> & { type: SdfFontType })
3128
)[],
3229
) {
30+
const enableDomRenderer = DOM_RENDERING && Config.domRendererEnabled;
3331
for (const font of fonts) {
3432
// WebGL — SDF
3533
if (
@@ -46,7 +44,7 @@ export function loadFonts(
4644
}
4745
// Canvas — Web
4846
else if ('fontUrl' in font) {
49-
if (DOM_RENDERING && isDomRenderer(renderer)) {
47+
if (enableDomRenderer) {
5048
loadFontToDom(font);
5149
} else {
5250
renderer.stage.fontManager.addFontFace(new lng.WebTrFontFace(font));

0 commit comments

Comments
 (0)