Skip to content
Merged
Show file tree
Hide file tree
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
126 changes: 110 additions & 16 deletions src/dom-renderer/domRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -29,6 +30,7 @@ import {
isRenderStateInBounds,
nodeHasTextureSource,
computeRenderStateForNode,
compactString,
} from './domRendererUtils.js';

// Feature detection for legacy brousers
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -791,6 +793,8 @@ function updateNodeStyles(node: DOMNode | DOMText) {
}

const textNodesToMeasure = new Set<DOMText>();
const containTextNodes = new Set<DOMText>();
let fontLoadingListenerSetup = false;

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

Expand Down Expand Up @@ -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':
Expand All @@ -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);
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -1637,7 +1731,7 @@ export class DOMRendererMain implements IRendererMain {
new Map();

constructor(
public settings: Partial<lng.RendererMainSettings>,
public settings: DomRendererMainSettings,
rawTarget: string | HTMLElement,
) {
let target: HTMLElement;
Expand Down
23 changes: 23 additions & 0 deletions src/dom-renderer/domRendererTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
4 changes: 4 additions & 0 deletions src/dom-renderer/domRendererUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 6 additions & 8 deletions src/lightningInit.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
}

Expand All @@ -30,6 +27,7 @@ export function loadFonts(
| (Partial<lng.SdfTrFontFaceOptions> & { type: SdfFontType })
)[],
) {
const enableDomRenderer = DOM_RENDERING && Config.domRendererEnabled;
for (const font of fonts) {
// WebGL — SDF
if (
Expand All @@ -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));
Expand Down
Loading