@lightningtv/solid 3.1.6 → 3.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/src/core/animation.d.ts +1 -1
  2. package/dist/src/core/animation.js.map +1 -1
  3. package/dist/src/core/config.d.ts +2 -2
  4. package/dist/src/core/config.js.map +1 -1
  5. package/dist/src/core/{domRenderer.d.ts → dom-renderer/domRenderer.d.ts} +30 -7
  6. package/dist/src/core/{domRenderer.js → dom-renderer/domRenderer.js} +633 -122
  7. package/dist/src/core/dom-renderer/domRenderer.js.map +1 -0
  8. package/dist/src/core/dom-renderer/domRendererTypes.d.ts +111 -0
  9. package/dist/src/core/dom-renderer/domRendererTypes.js +2 -0
  10. package/dist/src/core/dom-renderer/domRendererTypes.js.map +1 -0
  11. package/dist/src/core/dom-renderer/domRendererUtils.d.ts +23 -0
  12. package/dist/src/core/dom-renderer/domRendererUtils.js +231 -0
  13. package/dist/src/core/dom-renderer/domRendererUtils.js.map +1 -0
  14. package/dist/src/core/elementNode.d.ts +8 -8
  15. package/dist/src/core/elementNode.js +54 -15
  16. package/dist/src/core/elementNode.js.map +1 -1
  17. package/dist/src/core/index.d.ts +4 -2
  18. package/dist/src/core/index.js +1 -2
  19. package/dist/src/core/index.js.map +1 -1
  20. package/dist/src/core/intrinsicTypes.d.ts +16 -6
  21. package/dist/src/core/lightningInit.d.ts +7 -89
  22. package/dist/src/core/lightningInit.js +13 -5
  23. package/dist/src/core/lightningInit.js.map +1 -1
  24. package/dist/src/core/shaders.d.ts +12 -11
  25. package/dist/src/core/shaders.js +0 -90
  26. package/dist/src/core/shaders.js.map +1 -1
  27. package/dist/src/primitives/Grid.jsx +14 -2
  28. package/dist/src/primitives/Grid.jsx.map +1 -1
  29. package/dist/src/primitives/Image.jsx +18 -0
  30. package/dist/src/primitives/Image.jsx.map +1 -1
  31. package/dist/src/render.d.ts +3 -3
  32. package/dist/src/render.js.map +1 -1
  33. package/dist/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +2 -2
  35. package/src/core/animation.ts +6 -3
  36. package/src/core/config.ts +5 -2
  37. package/src/core/{domRenderer.ts → dom-renderer/domRenderer.ts} +738 -164
  38. package/src/core/dom-renderer/domRendererTypes.ts +150 -0
  39. package/src/core/dom-renderer/domRendererUtils.ts +291 -0
  40. package/src/core/elementNode.ts +98 -35
  41. package/src/core/index.ts +4 -2
  42. package/src/core/intrinsicTypes.ts +22 -6
  43. package/src/core/lightningInit.ts +20 -124
  44. package/src/core/shaders.ts +17 -110
  45. package/src/primitives/Grid.tsx +23 -7
  46. package/src/primitives/Image.tsx +20 -0
  47. package/src/render.ts +2 -1
  48. package/dist/src/core/domRenderer.js.map +0 -1
@@ -5,72 +5,45 @@ Experimental DOM renderer
5
5
  */
6
6
 
7
7
  import * as lng from '@lightningjs/renderer';
8
- import { EventEmitter } from '@lightningjs/renderer/utils';
9
8
 
10
- import { Config } from './config.js';
11
- import {
12
- IRendererShader,
13
- IRendererStage,
14
- IRendererShaderProps,
15
- IRendererTextureProps,
16
- IRendererTexture,
9
+ import { EventEmitter } from '@lightningjs/renderer/utils';
10
+ import { Config } from '../config.js';
11
+ import type {
12
+ ExtractProps,
17
13
  IRendererMain,
18
14
  IRendererNode,
19
15
  IRendererNodeProps,
16
+ IRendererShader,
17
+ IRendererStage,
20
18
  IRendererTextNode,
21
19
  IRendererTextNodeProps,
22
- } from './lightningInit.js';
23
- import { isFunc } from './utils.js';
24
-
25
- const colorToRgba = (c: number) =>
26
- `rgba(${(c >> 24) & 0xff},${(c >> 16) & 0xff},${(c >> 8) & 0xff},${(c & 0xff) / 255})`;
27
-
28
- function applyEasing(
29
- easing: string | lng.TimingFunction,
30
- progress: number,
31
- ): number {
32
- if (isFunc(easing)) {
33
- return easing(progress);
34
- }
35
-
36
- switch (easing) {
37
- case 'linear':
38
- default:
39
- return progress;
40
- case 'ease-in':
41
- return progress * progress;
42
- case 'ease-out':
43
- return progress * (2 - progress);
44
- case 'ease-in-out':
45
- return progress < 0.5
46
- ? 2 * progress * progress
47
- : -1 + (4 - 2 * progress) * progress;
48
- }
49
- }
50
-
51
- function interpolate(start: number, end: number, t: number): number {
52
- return start + (end - start) * t;
53
- }
54
-
55
- function interpolateColor(start: number, end: number, t: number): number {
56
- return (
57
- (interpolate((start >> 24) & 0xff, (end >> 24) & 0xff, t) << 24) |
58
- (interpolate((start >> 16) & 0xff, (end >> 16) & 0xff, t) << 16) |
59
- (interpolate((start >> 8) & 0xff, (end >> 8) & 0xff, t) << 8) |
60
- interpolate(start & 0xff, end & 0xff, t)
61
- );
62
- }
63
-
64
- function interpolateProp(
65
- name: string,
66
- start: number,
67
- end: number,
68
- t: number,
69
- ): number {
70
- return name.startsWith('color')
71
- ? interpolateColor(start, end, t)
72
- : interpolate(start, end, t);
73
- }
20
+ DomRendererMainSettings,
21
+ } from './domRendererTypes.js';
22
+ import {
23
+ colorToRgba,
24
+ buildGradientStops,
25
+ computeLegacyObjectFit,
26
+ applySubTextureScaling,
27
+ getNodeLineHeight,
28
+ applyEasing,
29
+ interpolateProp,
30
+ isRenderStateInBounds,
31
+ nodeHasTextureSource,
32
+ computeRenderStateForNode,
33
+ compactString,
34
+ } from './domRendererUtils.js';
35
+ import { FontLoadOptions } from '../intrinsicTypes.js';
36
+
37
+ // Feature detection for legacy brousers
38
+ const _styleRef: any =
39
+ typeof document !== 'undefined' ? document.documentElement?.style || {} : {};
40
+
41
+ const supportsObjectFit: boolean = 'objectFit' in _styleRef;
42
+ const supportsObjectPosition: boolean = 'objectPosition' in _styleRef;
43
+ const supportsMixBlendMode: boolean = 'mixBlendMode' in _styleRef;
44
+ const supportsStandardMask: boolean = 'maskImage' in _styleRef;
45
+ const supportsWebkitMask: boolean = 'webkitMaskImage' in _styleRef;
46
+ const supportsCssMask: boolean = supportsStandardMask || supportsWebkitMask;
74
47
 
75
48
  /*
76
49
  Animations
@@ -117,6 +90,8 @@ function updateAnimations(time: number) {
117
90
  // Animation complete
118
91
  else {
119
92
  Object.assign(task.node.props, task.propsEnd);
93
+ task.node.boundsDirty = true;
94
+ task.node.markChildrenBoundsDirty();
120
95
  updateNodeStyles(task.node);
121
96
 
122
97
  task.stop();
@@ -251,17 +226,12 @@ function animate(
251
226
  let elMap = new WeakMap<DOMNode, HTMLElement>();
252
227
 
253
228
  function updateNodeParent(node: DOMNode | DOMText) {
254
- if (node.parent != null) {
255
- elMap.get(node.parent as DOMNode)!.appendChild(node.div);
229
+ const parent = node.props.parent;
230
+ if (parent instanceof DOMNode) {
231
+ elMap.get(parent)!.appendChild(node.div);
256
232
  }
257
233
  }
258
234
 
259
- function getNodeLineHeight(props: IRendererTextNodeProps): number {
260
- return (
261
- props.lineHeight ?? Config.fontSettings.lineHeight ?? 1.2 * props.fontSize
262
- );
263
- }
264
-
265
235
  function updateNodeStyles(node: DOMNode | DOMText) {
266
236
  let { props } = node;
267
237
 
@@ -324,6 +294,9 @@ function updateNodeStyles(node: DOMNode | DOMText) {
324
294
  if (textProps.fontWeight !== 'normal') {
325
295
  style += `font-weight: ${textProps.fontWeight};`;
326
296
  }
297
+ if (textProps.fontStretch && textProps.fontStretch !== 'normal') {
298
+ style += `font-stretch: ${textProps.fontStretch};`;
299
+ }
327
300
  if (textProps.lineHeight != null) {
328
301
  style += `line-height: ${textProps.lineHeight}px;`;
329
302
  }
@@ -337,21 +310,45 @@ function updateNodeStyles(node: DOMNode | DOMText) {
337
310
  let maxLines = textProps.maxLines || Infinity;
338
311
  switch (textProps.contain) {
339
312
  case 'width':
340
- style += `width: ${props.w}px; overflow: hidden;`;
313
+ if (textProps.maxWidth && textProps.maxWidth > 0) {
314
+ style += `width: ${textProps.maxWidth}px;`;
315
+ } else {
316
+ style += `width: 100%;`;
317
+ }
318
+ style += `overflow: hidden;`;
341
319
  break;
342
320
  case 'both': {
343
321
  let lineHeight = getNodeLineHeight(textProps);
344
- maxLines = Math.min(maxLines, Math.floor(props.h / lineHeight));
345
- maxLines = Math.max(1, maxLines);
346
- let height = maxLines * lineHeight;
347
- style += `width: ${props.w}px; height: ${height}px; overflow: hidden;`;
322
+ const widthConstraint =
323
+ textProps.maxWidth && textProps.maxWidth > 0
324
+ ? `${textProps.maxWidth}px`
325
+ : `100%`;
326
+ const heightConstraint =
327
+ textProps.maxHeight && textProps.maxHeight > 0
328
+ ? textProps.maxHeight
329
+ : props.h;
330
+
331
+ let height = heightConstraint || 0;
332
+ if (height > 0) {
333
+ const maxLinesByHeight = Math.max(1, Math.floor(height / lineHeight));
334
+ maxLines = Math.min(maxLines, maxLinesByHeight);
335
+ height = Math.max(lineHeight, maxLines * lineHeight);
336
+ } else {
337
+ maxLines = Number.isFinite(maxLines) ? Math.max(1, maxLines) : 1;
338
+ height = maxLines * lineHeight;
339
+ }
340
+
341
+ style += `width: ${widthConstraint}; height: ${height}px; overflow: hidden;`;
348
342
  break;
349
343
  }
350
344
  case 'none':
345
+ style += `width: -webkit-max-content;`;
351
346
  style += `width: max-content;`;
352
347
  break;
353
348
  }
354
349
 
350
+ style += `white-space: pre-wrap;`;
351
+
355
352
  if (maxLines !== Infinity) {
356
353
  // https://stackoverflow.com/a/13924997
357
354
  style += `display: -webkit-box;
@@ -363,12 +360,10 @@ function updateNodeStyles(node: DOMNode | DOMText) {
363
360
 
364
361
  // if (node.overflowSuffix) style += `overflow-suffix: ${node.overflowSuffix};`
365
362
  // if (node.verticalAlign) style += `vertical-align: ${node.verticalAlign};`
366
-
367
- scheduleUpdateDOMTextMeasurement(node);
368
363
  }
369
364
  // <Node>
370
365
  else {
371
- if (props.w !== 0) style += `width: ${props.w}px;`;
366
+ if (props.w !== 0) style += `width: ${props.w < 0 ? 0 : props.w}px;`;
372
367
  if (props.h !== 0) style += `height: ${props.h}px;`;
373
368
 
374
369
  let vGradient =
@@ -387,57 +382,101 @@ function updateNodeStyles(node: DOMNode | DOMText) {
387
382
  : vGradient || hGradient;
388
383
 
389
384
  let srcImg: string | null = null;
390
- let srcPos: null | { x: number; y: number } = null;
385
+ let srcPos: null | InstanceType<lng.TextureMap['SubTexture']>['props'] =
386
+ null;
387
+ let rawImgSrc: string | null = null;
391
388
 
392
389
  if (
393
390
  props.texture != null &&
394
391
  props.texture.type === lng.TextureType.subTexture
395
392
  ) {
396
- srcPos = (props.texture as any).props;
397
- srcImg = `url(${(props.texture as any).props.texture.props.src})`;
393
+ const texture = props.texture as InstanceType<
394
+ lng.TextureMap['SubTexture']
395
+ >;
396
+ srcPos = texture.props;
397
+ rawImgSrc = (texture.props.texture as any).props.src;
398
398
  } else if (props.src) {
399
- srcImg = `url(${props.src})`;
399
+ rawImgSrc = props.src;
400
+ }
401
+
402
+ if (rawImgSrc) {
403
+ srcImg = `url(${rawImgSrc})`;
400
404
  }
401
405
 
402
406
  let bgStyle = '';
403
407
  let borderStyle = '';
404
408
  let radiusStyle = '';
405
409
  let maskStyle = '';
406
-
407
- if (srcImg) {
408
- if (props.color !== 0xffffffff && props.color !== 0x00000000) {
409
- // use image as a mask
410
- bgStyle += `background-color: ${colorToRgba(props.color)}; background-blend-mode: multiply;`;
411
- maskStyle += `mask-image: ${srcImg};`;
412
- if (srcPos !== null) {
413
- maskStyle += `mask-position: -${srcPos.x}px -${srcPos.y}px;`;
414
- } else {
415
- maskStyle += `mask-size: 100% 100%;`;
410
+ let needsBackgroundLayer = false;
411
+ let imgStyle = '';
412
+ let hasDivBgTint = false;
413
+
414
+ if (rawImgSrc) {
415
+ needsBackgroundLayer = true;
416
+
417
+ const hasTint = props.color !== 0xffffffff && props.color !== 0x00000000;
418
+
419
+ if (hasTint) {
420
+ bgStyle += `background-color: ${colorToRgba(props.color)};`;
421
+ if (srcImg) {
422
+ maskStyle += `mask-image: ${srcImg};`;
423
+ if (srcPos !== null) {
424
+ maskStyle += `mask-position: -${srcPos.x}px -${srcPos.y}px;`;
425
+ } else {
426
+ maskStyle += `mask-size: 100% 100%;`;
427
+ }
428
+ hasDivBgTint = true;
416
429
  }
417
430
  } else if (gradient) {
418
- // use gradient as a mask
431
+ // use gradient as a mask when no tint is applied
419
432
  maskStyle += `mask-image: ${gradient};`;
420
433
  }
421
434
 
422
- bgStyle += `background-image: ${srcImg};`;
423
- bgStyle += `background-repeat: no-repeat;`;
435
+ const imgStyleParts = [
436
+ 'position: absolute',
437
+ 'top: 0',
438
+ 'left: 0',
439
+ 'right: 0',
440
+ 'bottom: 0',
441
+ 'display: block',
442
+ 'pointer-events: none',
443
+ ];
424
444
 
425
445
  if (props.textureOptions.resizeMode?.type) {
426
- bgStyle += `background-size: ${props.textureOptions.resizeMode.type}; background-position: center;`;
446
+ const resizeMode = props.textureOptions.resizeMode;
447
+ imgStyleParts.push('width: 100%');
448
+ imgStyleParts.push('height: 100%');
449
+ imgStyleParts.push(`object-fit: ${resizeMode.type}`);
450
+
451
+ // Handle clipX and clipY for object-position
452
+ const clipX = (resizeMode as any).clipX ?? 0.5;
453
+ const clipY = (resizeMode as any).clipY ?? 0.5;
454
+ imgStyleParts.push(`object-position: ${clipX * 100}% ${clipY * 100}%`);
427
455
  } else if (srcPos !== null) {
428
- bgStyle += `background-position: -${srcPos.x}px -${srcPos.y}px;`;
456
+ imgStyleParts.push('width: auto');
457
+ imgStyleParts.push('height: auto');
458
+ imgStyleParts.push('object-fit: none');
459
+ imgStyleParts.push(`object-position: -${srcPos.x}px -${srcPos.y}px`);
460
+ } else if (props.w && !props.h) {
461
+ imgStyleParts.push('width: 100%');
462
+ imgStyleParts.push('height: auto');
463
+ } else if (props.h && !props.w) {
464
+ imgStyleParts.push('width: auto');
465
+ imgStyleParts.push('height: 100%');
429
466
  } else {
430
- bgStyle += 'background-size: 100% 100%;';
467
+ imgStyleParts.push('width: 100%');
468
+ imgStyleParts.push('height: 100%');
469
+ imgStyleParts.push('object-fit: fill');
431
470
  }
432
-
433
- if (maskStyle !== '') {
434
- bgStyle += maskStyle;
435
- }
436
- // separate layers are needed for the mask
437
- if (maskStyle !== '' && node.divBg == null) {
438
- node.div.appendChild((node.divBg = document.createElement('div')));
439
- node.div.appendChild((node.divBorder = document.createElement('div')));
471
+ if (hasTint) {
472
+ if (supportsMixBlendMode) {
473
+ imgStyleParts.push('mix-blend-mode: multiply');
474
+ } else {
475
+ imgStyleParts.push('opacity: 0.9');
476
+ }
440
477
  }
478
+
479
+ imgStyle = imgStyleParts.join('; ') + ';';
441
480
  } else if (gradient) {
442
481
  bgStyle += `background-image: ${gradient};`;
443
482
  bgStyle += `background-repeat: no-repeat;`;
@@ -447,13 +486,13 @@ function updateNodeStyles(node: DOMNode | DOMText) {
447
486
  }
448
487
 
449
488
  if (props.shader?.props != null) {
450
- let shader = props.shader.props;
489
+ let shaderProps = props.shader.props;
451
490
 
452
- let borderWidth = shader['border-w'];
453
- let borderColor = shader['border-color'];
454
- let borderGap = shader['border-gap'] ?? 0;
455
- let borderInset = shader['border-inset'] ?? true;
456
- let radius = shader['radius'];
491
+ let borderWidth = shaderProps['border-w'];
492
+ let borderColor = shaderProps['border-color'];
493
+ let borderGap = shaderProps['border-gap'] ?? 0;
494
+ let borderInset = shaderProps['border-inset'] ?? true;
495
+ let radius = shaderProps['radius'];
457
496
 
458
497
  // Border
459
498
  if (
@@ -462,10 +501,10 @@ function updateNodeStyles(node: DOMNode | DOMText) {
462
501
  typeof borderColor === 'number' &&
463
502
  borderColor !== 0
464
503
  ) {
504
+ const rgbaColor = colorToRgba(borderColor);
465
505
  // Handle inset borders by making gap negative
466
506
  let gap = borderInset ? -(borderWidth + borderGap) : borderGap;
467
-
468
- borderStyle += `outline: ${borderWidth}px solid ${colorToRgba(borderColor)};`;
507
+ borderStyle += `outline: ${borderWidth}px solid ${rgbaColor};`;
469
508
  borderStyle += `outline-offset: ${gap}px;`;
470
509
  }
471
510
  // Rounded
@@ -474,49 +513,302 @@ function updateNodeStyles(node: DOMNode | DOMText) {
474
513
  } else if (Array.isArray(radius) && radius.length === 4) {
475
514
  radiusStyle += `border-radius: ${radius[0]}px ${radius[1]}px ${radius[2]}px ${radius[3]}px;`;
476
515
  }
516
+
517
+ if ('radial' in shaderProps) {
518
+ const rg = shaderProps.radial as
519
+ | Partial<lng.RadialGradientProps>
520
+ | undefined;
521
+ const colors = Array.isArray(rg?.colors) ? rg!.colors! : [];
522
+ const stops = Array.isArray(rg?.stops) ? rg!.stops! : undefined;
523
+ const pivot = Array.isArray(rg?.pivot) ? rg!.pivot! : [0.5, 0.5];
524
+ const width = typeof rg?.w === 'number' ? rg!.w! : props.w || 0;
525
+ const height = typeof rg?.h === 'number' ? rg!.h! : width;
526
+
527
+ if (colors.length > 0) {
528
+ const gradientStops = buildGradientStops(colors, stops);
529
+ if (gradientStops) {
530
+ if (colors.length === 1) {
531
+ // Single color -> solid fill
532
+ if (srcImg || gradient) {
533
+ maskStyle += `mask-image: linear-gradient(${gradientStops});`;
534
+ } else {
535
+ bgStyle += `background-color: ${colorToRgba(colors[0]!)};`;
536
+ }
537
+ } else {
538
+ const isEllipse = width > 0 && height > 0 && width !== height;
539
+ const pivotX = (pivot[0] ?? 0.5) * 100;
540
+ const pivotY = (pivot[1] ?? 0.5) * 100;
541
+ let sizePart = '';
542
+ if (width > 0 && height > 0) {
543
+ if (!isEllipse && width === height) {
544
+ sizePart = `${Math.round(width)}px`;
545
+ } else {
546
+ sizePart = `${Math.round(width)}px ${Math.round(height)}px`;
547
+ }
548
+ } else {
549
+ sizePart = 'closest-side';
550
+ }
551
+ const radialGradient = `radial-gradient(${isEllipse ? 'ellipse' : 'circle'} ${sizePart} at ${pivotX.toFixed(2)}% ${pivotY.toFixed(2)}%, ${gradientStops})`;
552
+ if (srcImg || gradient) {
553
+ maskStyle += `mask-image: ${radialGradient};`;
554
+ } else {
555
+ bgStyle += `background-image: ${radialGradient};`;
556
+ bgStyle += `background-repeat: no-repeat;`;
557
+ bgStyle += `background-size: 100% 100%;`;
558
+ }
559
+ }
560
+ }
561
+ }
562
+ }
563
+
564
+ if ('linear' in shaderProps) {
565
+ const lg = shaderProps.linear as
566
+ | Partial<lng.LinearGradientProps>
567
+ | undefined;
568
+ const colors = Array.isArray(lg?.colors) ? lg!.colors! : [];
569
+ const stops = Array.isArray(lg?.stops) ? lg!.stops! : undefined;
570
+ const angleRad = typeof lg?.angle === 'number' ? lg!.angle! : 0; // radians
571
+
572
+ if (colors.length > 0) {
573
+ const gradientStops = buildGradientStops(colors, stops);
574
+ if (gradientStops) {
575
+ if (colors.length === 1) {
576
+ if (srcImg || gradient) {
577
+ maskStyle += `mask-image: linear-gradient(${gradientStops});`;
578
+ } else {
579
+ bgStyle += `background-color: ${colorToRgba(colors[0]!)};`;
580
+ }
581
+ } else {
582
+ const angleDeg = 180 * (angleRad / Math.PI - 1);
583
+ const linearGradient = `linear-gradient(${angleDeg.toFixed(2)}deg, ${gradientStops})`;
584
+ if (srcImg || gradient) {
585
+ maskStyle += `mask-image: ${linearGradient};`;
586
+ } else {
587
+ bgStyle += `background-image: ${linearGradient};`;
588
+ bgStyle += `background-repeat: no-repeat;`;
589
+ bgStyle += `background-size: 100% 100%;`;
590
+ }
591
+ }
592
+ }
593
+ }
594
+ }
595
+ }
596
+
597
+ if (maskStyle !== '') {
598
+ if (!supportsStandardMask && supportsWebkitMask) {
599
+ maskStyle = maskStyle.replace(/mask-/g, '-webkit-mask-');
600
+ } else if (!supportsCssMask) {
601
+ maskStyle = '';
602
+ }
603
+ if (maskStyle !== '') {
604
+ needsBackgroundLayer = true;
605
+ }
477
606
  }
478
607
 
479
608
  style += radiusStyle;
480
- bgStyle += radiusStyle;
481
- borderStyle += radiusStyle;
482
609
 
483
- if (node.divBg == null) {
484
- style += bgStyle;
610
+ if (needsBackgroundLayer) {
611
+ if (node.divBg == null) {
612
+ node.divBg = document.createElement('div');
613
+ node.div.insertBefore(node.divBg, node.div.firstChild);
614
+ } else if (node.divBg.parentElement !== node.div) {
615
+ node.div.insertBefore(node.divBg, node.div.firstChild);
616
+ }
617
+
618
+ let bgLayerStyle =
619
+ 'position: absolute; top:0; left:0; right:0; bottom:0; z-index: -1; pointer-events: none; overflow: hidden;';
620
+ if (bgStyle) {
621
+ bgLayerStyle += bgStyle;
622
+ }
623
+ if (maskStyle) {
624
+ bgLayerStyle += maskStyle;
625
+ }
626
+
627
+ node.divBg.setAttribute('style', bgLayerStyle + radiusStyle);
628
+
629
+ if (rawImgSrc) {
630
+ if (!node.imgEl) {
631
+ node.imgEl = document.createElement('img');
632
+ node.imgEl.alt = '';
633
+ node.imgEl.crossOrigin = 'anonymous';
634
+ node.imgEl.setAttribute('aria-hidden', 'true');
635
+ node.imgEl.setAttribute('loading', 'lazy');
636
+ node.imgEl.removeAttribute('src');
637
+
638
+ node.imgEl.addEventListener('load', () => {
639
+ const payload: lng.NodeTextureLoadedPayload = {
640
+ type: 'texture',
641
+ dimensions: {
642
+ w: node.imgEl!.naturalWidth,
643
+ h: node.imgEl!.naturalHeight,
644
+ },
645
+ };
646
+ node.imgEl!.style.display = '';
647
+ applySubTextureScaling(
648
+ node,
649
+ node.imgEl!,
650
+ node.lazyImageSubTextureProps,
651
+ );
652
+
653
+ const resizeMode = (node.props.textureOptions as any)?.resizeMode;
654
+ const clipX = resizeMode?.clipX ?? 0.5;
655
+ const clipY = resizeMode?.clipY ?? 0.5;
656
+ computeLegacyObjectFit(
657
+ node,
658
+ node.imgEl!,
659
+ resizeMode,
660
+ clipX,
661
+ clipY,
662
+ node.lazyImageSubTextureProps,
663
+ supportsObjectFit,
664
+ supportsObjectPosition,
665
+ );
666
+ node.emit('loaded', payload);
667
+ });
668
+
669
+ node.imgEl.addEventListener('error', () => {
670
+ if (node.imgEl) {
671
+ node.imgEl.removeAttribute('src');
672
+ node.imgEl.style.display = 'none';
673
+ node.imgEl.removeAttribute('data-rawSrc');
674
+ }
675
+
676
+ const failedSrc =
677
+ node.imgEl?.dataset.pendingSrc || node.lazyImagePendingSrc || '';
678
+
679
+ const payload: lng.NodeTextureFailedPayload = {
680
+ type: 'texture',
681
+ error: new Error(`Failed to load image: ${failedSrc}`),
682
+ };
683
+ node.emit('failed', payload);
684
+ });
685
+ }
686
+
687
+ node.lazyImagePendingSrc = rawImgSrc;
688
+ node.lazyImageSubTextureProps = srcPos;
689
+ node.imgEl.dataset.pendingSrc = rawImgSrc;
690
+
691
+ if (node.imgEl.parentElement !== node.divBg) {
692
+ node.divBg.appendChild(node.imgEl);
693
+ }
694
+
695
+ node.imgEl.setAttribute('style', imgStyle);
696
+
697
+ if (hasDivBgTint) {
698
+ node.imgEl.style.visibility = 'hidden';
699
+ }
700
+
701
+ if (isRenderStateInBounds(node.renderState)) {
702
+ node.applyPendingImageSrc();
703
+ } else if (!node.imgEl.dataset.rawSrc) {
704
+ node.imgEl.removeAttribute('src');
705
+ }
706
+
707
+ if (
708
+ srcPos &&
709
+ node.imgEl.complete &&
710
+ node.imgEl.dataset.rawSrc === rawImgSrc
711
+ ) {
712
+ applySubTextureScaling(node, node.imgEl, srcPos);
713
+ }
714
+ if (
715
+ !srcPos &&
716
+ node.imgEl.complete &&
717
+ (!supportsObjectFit || !supportsObjectPosition) &&
718
+ node.imgEl.dataset.rawSrc === rawImgSrc
719
+ ) {
720
+ const resizeMode = (node.props.textureOptions as any)?.resizeMode;
721
+ const clipX = resizeMode?.clipX ?? 0.5;
722
+ const clipY = resizeMode?.clipY ?? 0.5;
723
+ computeLegacyObjectFit(
724
+ node,
725
+ node.imgEl,
726
+ resizeMode,
727
+ clipX,
728
+ clipY,
729
+ srcPos,
730
+ supportsObjectFit,
731
+ supportsObjectPosition,
732
+ );
733
+ }
734
+ } else {
735
+ node.lazyImagePendingSrc = null;
736
+ node.lazyImageSubTextureProps = null;
737
+ if (node.imgEl) {
738
+ node.imgEl.remove();
739
+ node.imgEl = undefined;
740
+ }
741
+ }
485
742
  } else {
486
- bgStyle += 'position: absolute; inset: 0; z-index: -1;';
487
- node.divBg.setAttribute('style', bgStyle);
743
+ node.lazyImagePendingSrc = null;
744
+ node.lazyImageSubTextureProps = null;
745
+ if (node.imgEl) {
746
+ node.imgEl.remove();
747
+ node.imgEl = undefined;
748
+ }
749
+ if (node.divBg) {
750
+ node.divBg.remove();
751
+ node.divBg = undefined;
752
+ }
753
+ style += bgStyle;
754
+ }
755
+
756
+ const needsSeparateBorderLayer = needsBackgroundLayer && maskStyle !== '';
757
+
758
+ if (needsSeparateBorderLayer) {
759
+ if (node.divBorder == null) {
760
+ node.divBorder = document.createElement('div');
761
+ node.div.appendChild(node.divBorder);
762
+ }
763
+ } else if (node.divBorder) {
764
+ node.divBorder.remove();
765
+ node.divBorder = undefined;
488
766
  }
767
+
489
768
  if (node.divBorder == null) {
490
769
  style += borderStyle;
491
770
  } else {
492
- borderStyle += 'position: absolute; inset: 0; z-index: -1;';
493
- node.divBorder.setAttribute('style', borderStyle);
771
+ let borderLayerStyle =
772
+ 'position: absolute; top:0; left:0; right:0; bottom:0; z-index: -1; pointer-events: none;';
773
+ borderLayerStyle += borderStyle;
774
+ node.divBorder.setAttribute('style', borderLayerStyle + radiusStyle);
494
775
  }
495
776
  }
496
777
 
497
- node.div.setAttribute('style', style);
498
- }
778
+ node.div.setAttribute('style', compactString(style));
499
779
 
500
- const fontFamiliesToLoad = new Set<string>();
780
+ if (node instanceof DOMNode && node !== node.stage.root) {
781
+ const hasTextureSrc = nodeHasTextureSource(node);
782
+ if (hasTextureSrc && node.boundsDirty) {
783
+ const next = computeRenderStateForNode(node);
784
+ if (next != null) {
785
+ node.updateRenderState(next);
786
+ }
787
+ node.boundsDirty = false;
788
+ } else if (!hasTextureSrc) {
789
+ node.boundsDirty = false;
790
+ }
791
+ }
792
+ }
501
793
 
502
794
  const textNodesToMeasure = new Set<DOMText>();
503
795
 
504
796
  type Size = { width: number; height: number };
505
797
 
506
798
  function getElSize(node: DOMNode): Size {
507
- let rect = node.div.getBoundingClientRect();
799
+ const rawRect = node.div.getBoundingClientRect();
508
800
 
509
- let dpr = Config.rendererOptions?.deviceLogicalPixelRatio ?? 1;
510
- rect.height /= dpr;
511
- rect.width /= dpr;
801
+ const dpr = Config.rendererOptions?.deviceLogicalPixelRatio ?? 1;
802
+ let width = rawRect.width / dpr;
803
+ let height = rawRect.height / dpr;
512
804
 
513
805
  for (;;) {
514
806
  if (node.props.scale != null && node.props.scale !== 1) {
515
- rect.height /= node.props.scale;
516
- rect.width /= node.props.scale;
807
+ width /= node.props.scale;
808
+ height /= node.props.scale;
517
809
  } else {
518
- rect.height /= node.props.scaleY;
519
- rect.width /= node.props.scaleX;
810
+ width /= node.props.scaleX;
811
+ height /= node.props.scaleY;
520
812
  }
521
813
 
522
814
  if (node.parent instanceof DOMNode) {
@@ -526,7 +818,7 @@ function getElSize(node: DOMNode): Size {
526
818
  }
527
819
  }
528
820
 
529
- return rect;
821
+ return { width, height };
530
822
  }
531
823
 
532
824
  /*
@@ -542,7 +834,6 @@ function updateDOMTextSize(node: DOMText): void {
542
834
  if (node.props.h !== size.height) {
543
835
  node.props.h = size.height;
544
836
  updateNodeStyles(node);
545
- node.emit('loaded');
546
837
  }
547
838
  break;
548
839
  case 'none':
@@ -551,10 +842,21 @@ function updateDOMTextSize(node: DOMText): void {
551
842
  node.props.w = size.width;
552
843
  node.props.h = size.height;
553
844
  updateNodeStyles(node);
554
- node.emit('loaded');
555
845
  }
556
846
  break;
557
847
  }
848
+
849
+ if (!node.loaded) {
850
+ const payload: lng.NodeTextLoadedPayload = {
851
+ type: 'text',
852
+ dimensions: {
853
+ w: node.props.w,
854
+ h: node.props.h,
855
+ },
856
+ };
857
+ node.emit('loaded', payload);
858
+ node.loaded = true;
859
+ }
558
860
  }
559
861
 
560
862
  function updateDOMTextMeasurements() {
@@ -566,16 +868,17 @@ function scheduleUpdateDOMTextMeasurement(node: DOMText) {
566
868
  /*
567
869
  Make sure the font is loaded before measuring
568
870
  */
569
- if (node.fontFamily && !fontFamiliesToLoad.has(node.fontFamily)) {
570
- fontFamiliesToLoad.add(node.fontFamily);
571
- document.fonts.load(`16px ${node.fontFamily}`);
572
- }
573
871
 
574
872
  if (textNodesToMeasure.size === 0) {
873
+ const fonts = document.fonts;
575
874
  if (document.fonts.status === 'loaded') {
576
875
  setTimeout(updateDOMTextMeasurements);
577
876
  } else {
578
- document.fonts.ready.then(updateDOMTextMeasurements);
877
+ if (fonts && fonts.ready && typeof fonts.ready.then === 'function') {
878
+ fonts.ready.then(updateDOMTextMeasurements);
879
+ } else {
880
+ setTimeout(updateDOMTextMeasurements, 500);
881
+ }
579
882
  }
580
883
  }
581
884
 
@@ -583,12 +886,13 @@ function scheduleUpdateDOMTextMeasurement(node: DOMText) {
583
886
  }
584
887
 
585
888
  function updateNodeData(node: DOMNode | DOMText) {
586
- for (let key in node.data) {
587
- let keyValue: unknown = node.data[key];
889
+ const data = node.data;
890
+ for (let key in data) {
891
+ let keyValue: unknown = data[key];
588
892
  if (keyValue === undefined) {
589
893
  node.div.removeAttribute('data-' + key);
590
894
  } else {
591
- node.div.setAttribute('data-' + key, String(keyValue));
895
+ node.div.dataset[key] = String(keyValue);
592
896
  }
593
897
  }
594
898
  }
@@ -596,7 +900,7 @@ function updateNodeData(node: DOMNode | DOMText) {
596
900
  function resolveNodeDefaults(
597
901
  props: Partial<IRendererNodeProps>,
598
902
  ): IRendererNodeProps {
599
- const color = props.color ?? 0xffffffff;
903
+ const color = props.color ?? 0x00000000;
600
904
 
601
905
  return {
602
906
  x: props.x ?? 0,
@@ -677,15 +981,31 @@ const defaultShader: IRendererShader = {
677
981
 
678
982
  let lastNodeId = 0;
679
983
 
680
- class DOMNode extends EventEmitter implements IRendererNode {
984
+ const CoreNodeRenderStateMap = new Map<number, string>([
985
+ [0, 'init'],
986
+ [2, 'outOfBounds'],
987
+ [4, 'inBounds'],
988
+ [8, 'inViewport'],
989
+ ]);
990
+
991
+ export class DOMNode extends EventEmitter implements IRendererNode {
681
992
  div = document.createElement('div');
682
993
  divBg: HTMLElement | undefined;
683
994
  divBorder: HTMLElement | undefined;
995
+ imgEl: HTMLImageElement | undefined;
996
+ lazyImagePendingSrc: string | null = null;
997
+ lazyImageSubTextureProps:
998
+ | InstanceType<lng.TextureMap['SubTexture']>['props']
999
+ | null = null;
1000
+ boundsDirty = true;
1001
+ children = new Set<DOMNode>();
684
1002
 
685
1003
  id = ++lastNodeId;
686
1004
 
687
1005
  renderState: lng.CoreNodeRenderState = 0 /* Init */;
688
1006
 
1007
+ preventCleanup = true;
1008
+
689
1009
  constructor(
690
1010
  public stage: IRendererStage,
691
1011
  public props: IRendererNodeProps,
@@ -697,6 +1017,11 @@ class DOMNode extends EventEmitter implements IRendererNode {
697
1017
  this.div.setAttribute('data-id', String(this.id));
698
1018
  elMap.set(this, this.div);
699
1019
 
1020
+ const parent = this.props.parent;
1021
+ if (parent instanceof DOMNode) {
1022
+ parent.children.add(this);
1023
+ }
1024
+
700
1025
  updateNodeParent(this);
701
1026
  updateNodeStyles(this);
702
1027
  updateNodeData(this);
@@ -704,6 +1029,10 @@ class DOMNode extends EventEmitter implements IRendererNode {
704
1029
 
705
1030
  destroy(): void {
706
1031
  elMap.delete(this);
1032
+ const parent = this.props.parent;
1033
+ if (parent instanceof DOMNode) {
1034
+ parent.children.delete(this);
1035
+ }
707
1036
  this.div.parentNode!.removeChild(this.div);
708
1037
  }
709
1038
 
@@ -711,52 +1040,132 @@ class DOMNode extends EventEmitter implements IRendererNode {
711
1040
  return this.props.parent;
712
1041
  }
713
1042
  set parent(value: IRendererNode | null) {
1043
+ if (this.props.parent === value) return;
1044
+
1045
+ const prevParent = this.props.parent;
1046
+ if (prevParent instanceof DOMNode) {
1047
+ prevParent.children.delete(this);
1048
+ prevParent.markChildrenBoundsDirty();
1049
+ }
1050
+
714
1051
  this.props.parent = value;
1052
+
1053
+ if (value instanceof DOMNode) {
1054
+ value.children.add(this);
1055
+ value.markChildrenBoundsDirty();
1056
+ }
1057
+
1058
+ this.boundsDirty = true;
1059
+ this.markChildrenBoundsDirty();
715
1060
  updateNodeParent(this);
716
1061
  }
717
1062
 
1063
+ public markChildrenBoundsDirty() {
1064
+ for (const child of this.children) {
1065
+ child.boundsDirty = true;
1066
+
1067
+ if (child !== child.stage.root) {
1068
+ if (nodeHasTextureSource(child)) {
1069
+ const nextState = computeRenderStateForNode(child);
1070
+ if (nextState != null) {
1071
+ child.updateRenderState(nextState);
1072
+ }
1073
+ }
1074
+ child.boundsDirty = false;
1075
+ }
1076
+
1077
+ child.markChildrenBoundsDirty();
1078
+ }
1079
+ }
1080
+
718
1081
  animate = animate;
719
1082
 
1083
+ updateRenderState(renderState: lng.CoreNodeRenderState) {
1084
+ if (renderState === this.renderState) return;
1085
+ const previous = this.renderState;
1086
+ this.renderState = renderState;
1087
+ const event = CoreNodeRenderStateMap.get(renderState);
1088
+ if (isRenderStateInBounds(renderState)) {
1089
+ this.applyPendingImageSrc();
1090
+ }
1091
+ if (event && event !== 'init') {
1092
+ this.emit(event, { previous, current: renderState });
1093
+ }
1094
+ if (this.imgEl) {
1095
+ this.imgEl.dataset.state = event;
1096
+ }
1097
+ }
1098
+
1099
+ applyPendingImageSrc() {
1100
+ if (!this.imgEl) return;
1101
+ const pendingSrc = this.lazyImagePendingSrc;
1102
+ if (!pendingSrc) return;
1103
+ if (this.imgEl.dataset.rawSrc === pendingSrc) return;
1104
+ this.imgEl.style.display = '';
1105
+ this.imgEl.dataset.pendingSrc = pendingSrc;
1106
+ this.imgEl.src = pendingSrc;
1107
+ this.imgEl.dataset.rawSrc = pendingSrc;
1108
+ this.imgEl.dataset.pendingSrc = '';
1109
+ }
1110
+
720
1111
  get x() {
721
1112
  return this.props.x;
722
1113
  }
723
1114
  set x(v) {
1115
+ if (this.props.x === v) return;
724
1116
  this.props.x = v;
1117
+ this.boundsDirty = true;
1118
+ this.markChildrenBoundsDirty();
725
1119
  updateNodeStyles(this);
726
1120
  }
727
1121
  get y() {
728
1122
  return this.props.y;
729
1123
  }
730
1124
  set y(v) {
1125
+ if (this.props.y === v) return;
731
1126
  this.props.y = v;
1127
+ this.boundsDirty = true;
1128
+ this.markChildrenBoundsDirty();
732
1129
  updateNodeStyles(this);
733
1130
  }
734
1131
  get w() {
735
1132
  return this.props.w;
736
1133
  }
737
1134
  set w(v) {
1135
+ if (this.props.w === v) return;
738
1136
  this.props.w = v;
1137
+ this.boundsDirty = true;
1138
+ this.markChildrenBoundsDirty();
739
1139
  updateNodeStyles(this);
740
1140
  }
741
1141
  get h() {
742
1142
  return this.props.h;
743
1143
  }
744
1144
  set h(v) {
1145
+ if (this.props.h === v) return;
745
1146
  this.props.h = v;
1147
+ this.boundsDirty = true;
1148
+ this.markChildrenBoundsDirty();
746
1149
  updateNodeStyles(this);
747
1150
  }
748
1151
  get width() {
749
1152
  return this.props.w;
750
1153
  }
751
1154
  set width(v) {
1155
+ if (this.props.w === v) return;
752
1156
  this.props.w = v;
1157
+ this.boundsDirty = true;
1158
+ this.markChildrenBoundsDirty();
753
1159
  updateNodeStyles(this);
754
1160
  }
755
1161
  get height() {
756
1162
  return this.props.h;
757
1163
  }
758
1164
  set height(v) {
1165
+ if (this.props.h === v) return;
759
1166
  this.props.h = v;
1167
+ this.boundsDirty = true;
1168
+ this.markChildrenBoundsDirty();
760
1169
  updateNodeStyles(this);
761
1170
  }
762
1171
  get alpha() {
@@ -847,14 +1256,17 @@ class DOMNode extends EventEmitter implements IRendererNode {
847
1256
  return this.props.zIndex;
848
1257
  }
849
1258
  set zIndex(v) {
850
- this.props.zIndex = v;
1259
+ if (this.props.zIndex === v) return;
1260
+ this.props.zIndex = Math.ceil(v);
851
1261
  updateNodeStyles(this);
852
1262
  }
853
1263
  get texture() {
854
1264
  return this.props.texture;
855
1265
  }
856
1266
  set texture(v) {
1267
+ if (this.props.texture === v) return;
857
1268
  this.props.texture = v;
1269
+ this.boundsDirty = true;
858
1270
  updateNodeStyles(this);
859
1271
  }
860
1272
  get textureOptions(): IRendererNode['textureOptions'] {
@@ -868,13 +1280,16 @@ class DOMNode extends EventEmitter implements IRendererNode {
868
1280
  return this.props.src;
869
1281
  }
870
1282
  set src(v) {
1283
+ if (this.props.src === v) return;
871
1284
  this.props.src = v;
1285
+ this.boundsDirty = true;
872
1286
  updateNodeStyles(this);
873
1287
  }
874
1288
  get scale() {
875
1289
  return this.props.scale ?? 1;
876
1290
  }
877
1291
  set scale(v) {
1292
+ if (this.props.scale === v) return;
878
1293
  this.props.scale = v;
879
1294
  updateNodeStyles(this);
880
1295
  }
@@ -955,6 +1370,7 @@ class DOMNode extends EventEmitter implements IRendererNode {
955
1370
  this.props.shader = v;
956
1371
  updateNodeStyles(this);
957
1372
  }
1373
+
958
1374
  get data(): IRendererNode['data'] {
959
1375
  return this.props.data;
960
1376
  }
@@ -999,29 +1415,45 @@ class DOMNode extends EventEmitter implements IRendererNode {
999
1415
  }
1000
1416
  set boundsMargin(value: number | [number, number, number, number] | null) {
1001
1417
  this.props.boundsMargin = value;
1418
+ this.boundsDirty = true;
1419
+ this.markChildrenBoundsDirty();
1002
1420
  }
1003
1421
 
1004
1422
  get absX(): number {
1005
- return this.x + -this.width * this.mountX + (this.parent?.absX ?? 0);
1423
+ const parent = this.props.parent;
1424
+ return (
1425
+ this.x +
1426
+ -this.w * this.mountX +
1427
+ (parent instanceof DOMNode ? parent.absX : 0)
1428
+ );
1006
1429
  }
1007
1430
  get absY(): number {
1008
- return this.y + -this.height * this.mountY + (this.parent?.absY ?? 0);
1431
+ const parent = this.props.parent;
1432
+ return (
1433
+ this.y +
1434
+ -this.h * this.mountY +
1435
+ (parent instanceof DOMNode ? parent.absY : 0)
1436
+ );
1009
1437
  }
1010
1438
  }
1011
1439
 
1012
1440
  class DOMText extends DOMNode {
1441
+ public loaded = false;
1442
+
1013
1443
  constructor(
1014
1444
  stage: IRendererStage,
1015
1445
  public override props: IRendererTextNodeProps,
1016
1446
  ) {
1017
1447
  super(stage, props);
1018
1448
  this.div.innerText = props.text;
1449
+ scheduleUpdateDOMTextMeasurement(this);
1019
1450
  }
1020
1451
 
1021
1452
  get text() {
1022
1453
  return this.props.text;
1023
1454
  }
1024
1455
  set text(v) {
1456
+ if (this.props.text === v) return;
1025
1457
  this.props.text = v;
1026
1458
  this.div.innerText = v;
1027
1459
  scheduleUpdateDOMTextMeasurement(this);
@@ -1030,29 +1462,46 @@ class DOMText extends DOMNode {
1030
1462
  return this.props.fontFamily;
1031
1463
  }
1032
1464
  set fontFamily(v) {
1465
+ if (this.props.fontFamily === v) return;
1033
1466
  this.props.fontFamily = v;
1034
1467
  updateNodeStyles(this);
1468
+ scheduleUpdateDOMTextMeasurement(this);
1035
1469
  }
1036
1470
  get fontSize() {
1037
1471
  return this.props.fontSize;
1038
1472
  }
1039
1473
  set fontSize(v) {
1474
+ if (this.props.fontSize === v) return;
1040
1475
  this.props.fontSize = v;
1041
1476
  updateNodeStyles(this);
1477
+ scheduleUpdateDOMTextMeasurement(this);
1042
1478
  }
1043
1479
  get fontStyle() {
1044
1480
  return this.props.fontStyle;
1045
1481
  }
1046
1482
  set fontStyle(v) {
1483
+ if (this.props.fontStyle === v) return;
1047
1484
  this.props.fontStyle = v;
1048
1485
  updateNodeStyles(this);
1486
+ scheduleUpdateDOMTextMeasurement(this);
1049
1487
  }
1050
1488
  get fontWeight() {
1051
1489
  return this.props.fontWeight;
1052
1490
  }
1053
1491
  set fontWeight(v) {
1492
+ if (this.props.fontWeight === v) return;
1054
1493
  this.props.fontWeight = v;
1055
1494
  updateNodeStyles(this);
1495
+ scheduleUpdateDOMTextMeasurement(this);
1496
+ }
1497
+ get fontStretch() {
1498
+ return this.props.fontStretch;
1499
+ }
1500
+ set fontStretch(v) {
1501
+ if (this.props.fontStretch === v) return;
1502
+ this.props.fontStretch = v;
1503
+ updateNodeStyles(this);
1504
+ scheduleUpdateDOMTextMeasurement(this);
1056
1505
  }
1057
1506
  get forceLoad() {
1058
1507
  return this.props.forceLoad;
@@ -1064,34 +1513,43 @@ class DOMText extends DOMNode {
1064
1513
  return this.props.lineHeight;
1065
1514
  }
1066
1515
  set lineHeight(v) {
1516
+ if (this.props.lineHeight === v) return;
1067
1517
  this.props.lineHeight = v;
1068
1518
  updateNodeStyles(this);
1519
+ scheduleUpdateDOMTextMeasurement(this);
1069
1520
  }
1070
1521
  get maxWidth() {
1071
1522
  return this.props.maxWidth;
1072
1523
  }
1073
1524
  set maxWidth(v) {
1525
+ if (this.props.maxWidth === v) return;
1074
1526
  this.props.maxWidth = v;
1075
1527
  updateNodeStyles(this);
1528
+ scheduleUpdateDOMTextMeasurement(this);
1076
1529
  }
1077
1530
  get maxHeight() {
1078
1531
  return this.props.maxHeight;
1079
1532
  }
1080
1533
  set maxHeight(v) {
1534
+ if (this.props.maxHeight === v) return;
1081
1535
  this.props.maxHeight = v;
1082
1536
  updateNodeStyles(this);
1537
+ scheduleUpdateDOMTextMeasurement(this);
1083
1538
  }
1084
1539
  get letterSpacing() {
1085
1540
  return this.props.letterSpacing;
1086
1541
  }
1087
1542
  set letterSpacing(v) {
1543
+ if (this.props.letterSpacing === v) return;
1088
1544
  this.props.letterSpacing = v;
1089
1545
  updateNodeStyles(this);
1546
+ scheduleUpdateDOMTextMeasurement(this);
1090
1547
  }
1091
1548
  get textAlign() {
1092
1549
  return this.props.textAlign;
1093
1550
  }
1094
1551
  set textAlign(v) {
1552
+ if (this.props.textAlign === v) return;
1095
1553
  this.props.textAlign = v;
1096
1554
  updateNodeStyles(this);
1097
1555
  }
@@ -1099,6 +1557,7 @@ class DOMText extends DOMNode {
1099
1557
  return this.props.overflowSuffix;
1100
1558
  }
1101
1559
  set overflowSuffix(v) {
1560
+ if (this.props.overflowSuffix === v) return;
1102
1561
  this.props.overflowSuffix = v;
1103
1562
  updateNodeStyles(this);
1104
1563
  }
@@ -1106,15 +1565,19 @@ class DOMText extends DOMNode {
1106
1565
  return this.props.maxLines;
1107
1566
  }
1108
1567
  set maxLines(v) {
1568
+ if (this.props.maxLines === v) return;
1109
1569
  this.props.maxLines = v;
1110
1570
  updateNodeStyles(this);
1571
+ scheduleUpdateDOMTextMeasurement(this);
1111
1572
  }
1112
1573
  get contain() {
1113
1574
  return this.props.contain;
1114
1575
  }
1115
1576
  set contain(v) {
1577
+ if (this.props.contain === v) return;
1116
1578
  this.props.contain = v;
1117
1579
  updateNodeStyles(this);
1580
+ scheduleUpdateDOMTextMeasurement(this);
1118
1581
  }
1119
1582
  get verticalAlign() {
1120
1583
  return this.props.verticalAlign;
@@ -1171,11 +1634,12 @@ function updateRootPosition(this: DOMRendererMain) {
1171
1634
  export class DOMRendererMain implements IRendererMain {
1172
1635
  root: DOMNode;
1173
1636
  canvas: HTMLCanvasElement;
1174
-
1175
1637
  stage: IRendererStage;
1638
+ private eventListeners: Map<string, Set<(target: any, data: any) => void>> =
1639
+ new Map();
1176
1640
 
1177
1641
  constructor(
1178
- public settings: lng.RendererMainSettings,
1642
+ public settings: DomRendererMainSettings,
1179
1643
  rawTarget: string | HTMLElement,
1180
1644
  ) {
1181
1645
  let target: HTMLElement;
@@ -1203,15 +1667,20 @@ export class DOMRendererMain implements IRendererMain {
1203
1667
  root: null!,
1204
1668
  renderer: {
1205
1669
  mode: 'canvas',
1670
+ boundsMargin: settings.boundsMargin,
1206
1671
  },
1207
- loadFont: async () => {},
1208
1672
  shManager: {
1209
1673
  registerShaderType() {},
1210
1674
  },
1211
1675
  animationManager: {
1212
- registerAnimation() {},
1213
- unregisterAnimation() {},
1676
+ registerAnimation(anim) {
1677
+ console.log('registerAnimation', anim);
1678
+ },
1679
+ unregisterAnimation(anim) {
1680
+ console.log('unregisterAnimation', anim);
1681
+ },
1214
1682
  },
1683
+ loadFont: async () => {},
1215
1684
  cleanup() {},
1216
1685
  };
1217
1686
 
@@ -1221,7 +1690,7 @@ export class DOMRendererMain implements IRendererMain {
1221
1690
  w: settings.appWidth ?? 1920,
1222
1691
  h: settings.appHeight ?? 1080,
1223
1692
  shader: defaultShader,
1224
- zIndex: 65534,
1693
+ zIndex: 1,
1225
1694
  }),
1226
1695
  );
1227
1696
  this.stage.root = this.root;
@@ -1255,6 +1724,77 @@ export class DOMRendererMain implements IRendererMain {
1255
1724
  window.addEventListener('resize', updateRootPosition.bind(this));
1256
1725
  }
1257
1726
 
1727
+ removeAllListeners(): void {
1728
+ if (this.eventListeners.size === 0) return;
1729
+ this.eventListeners.forEach((listeners) => listeners.clear());
1730
+ this.eventListeners.clear();
1731
+ }
1732
+
1733
+ once<K extends string | number>(
1734
+ event: Extract<K, string>,
1735
+ listener: { [s: string]: (target: any, data: any) => void }[K],
1736
+ ): void {
1737
+ const wrappedListener = (target: any, data: any) => {
1738
+ this.off(event, wrappedListener);
1739
+ listener(target, data);
1740
+ };
1741
+ this.on(event, wrappedListener);
1742
+ }
1743
+
1744
+ on(name: string, callback: (target: any, data: any) => void) {
1745
+ let listeners = this.eventListeners.get(name);
1746
+ if (!listeners) {
1747
+ listeners = new Set();
1748
+ this.eventListeners.set(name, listeners);
1749
+ }
1750
+ listeners.add(callback);
1751
+ }
1752
+
1753
+ off<K extends string | number>(
1754
+ event: Extract<K, string>,
1755
+ listener: { [s: string]: (target: any, data: any) => void }[K],
1756
+ ): void {
1757
+ const listeners = this.eventListeners.get(event);
1758
+ if (listeners) {
1759
+ listeners.delete(listener);
1760
+ if (listeners.size === 0) {
1761
+ this.eventListeners.delete(event);
1762
+ }
1763
+ }
1764
+ }
1765
+
1766
+ emit<K extends string | number>(
1767
+ event: Extract<K, string>,
1768
+ data: Parameters<any>[1],
1769
+ ): void;
1770
+ emit<K extends string | number>(
1771
+ event: Extract<K, string>,
1772
+ target: any,
1773
+ data: Parameters<any>[1],
1774
+ ): void;
1775
+ emit<K extends string | number>(
1776
+ event: Extract<K, string>,
1777
+ targetOrData: any,
1778
+ maybeData?: Parameters<any>[1],
1779
+ ): void {
1780
+ const listeners = this.eventListeners.get(event);
1781
+ if (!listeners || listeners.size === 0) {
1782
+ return;
1783
+ }
1784
+
1785
+ const hasExplicitTarget = arguments.length === 3;
1786
+ const target = hasExplicitTarget ? targetOrData : this.root;
1787
+ const data = hasExplicitTarget ? maybeData : targetOrData;
1788
+
1789
+ for (const listener of Array.from(listeners)) {
1790
+ try {
1791
+ listener(target, data);
1792
+ } catch (error) {
1793
+ console.error(`Error in listener for event "${event}"`, error);
1794
+ }
1795
+ }
1796
+ }
1797
+
1258
1798
  createNode(props: Partial<IRendererNodeProps>): IRendererNode {
1259
1799
  return new DOMNode(this.stage, resolveNodeDefaults(props));
1260
1800
  }
@@ -1263,17 +1803,32 @@ export class DOMRendererMain implements IRendererMain {
1263
1803
  return new DOMText(this.stage, resolveTextNodeDefaults(props));
1264
1804
  }
1265
1805
 
1806
+ /** TODO: restore this */
1807
+ // createShader<ShType extends keyof ShaderMap>(
1808
+ // shType: ShType,
1809
+ // props?: OptionalShaderProps<ShType>,
1810
+ // ): InstanceType<lng.ShaderMap[ShType]> {
1811
+ // return { shaderType: shType, props, program: {} } as InstanceType<
1812
+ // lng.ShaderMap[ShType]
1813
+ // >;
1814
+ // }
1815
+
1266
1816
  createShader(
1267
- shaderType: string,
1268
- props?: IRendererShaderProps,
1269
- ): IRendererShader {
1270
- return { shaderType, props, program: {} };
1817
+ ...args: Parameters<typeof lng.RendererMain.prototype.createShader>
1818
+ ): ReturnType<typeof lng.RendererMain.prototype.createShader> {
1819
+ const [shaderType, props] = args;
1820
+ return {
1821
+ // @ts-ignore
1822
+ shaderType,
1823
+ props,
1824
+ program: {},
1825
+ };
1271
1826
  }
1272
1827
 
1273
- createTexture(
1274
- textureType: keyof lng.TextureMap,
1275
- props: IRendererTextureProps,
1276
- ): IRendererTexture {
1828
+ createTexture<Type extends keyof lng.TextureMap>(
1829
+ textureType: Type,
1830
+ props: ExtractProps<lng.TextureMap[Type]>,
1831
+ ): InstanceType<lng.TextureMap[Type]> {
1277
1832
  let type = lng.TextureType.generic;
1278
1833
  switch (textureType) {
1279
1834
  case 'SubTexture':
@@ -1292,10 +1847,29 @@ export class DOMRendererMain implements IRendererMain {
1292
1847
  type = lng.TextureType.renderToTexture;
1293
1848
  break;
1294
1849
  }
1295
- return { type, props };
1850
+ return { type, props } as InstanceType<lng.TextureMap[Type]>;
1296
1851
  }
1852
+ }
1297
1853
 
1298
- on(name: string, callback: (target: any, data: any) => void) {
1299
- console.log('on', name, callback);
1854
+ export function loadFontToDom(font: FontLoadOptions): void {
1855
+ // fontFamily: string;
1856
+ // metrics?: FontMetrics;
1857
+ // fontUrl?: string;
1858
+ // atlasUrl?: string;
1859
+ // atlasDataUrl?: string;
1860
+
1861
+ const fontFace = new FontFace(font.fontFamily, `url(${font.fontUrl})`);
1862
+
1863
+ if (typeof document !== 'undefined' && 'fonts' in document) {
1864
+ const fontSet = document.fonts as FontFaceSet & {
1865
+ add?: (font: FontFace) => FontFaceSet;
1866
+ };
1867
+ fontSet.add?.(fontFace);
1300
1868
  }
1301
1869
  }
1870
+
1871
+ export function isDomRenderer(
1872
+ r: lng.RendererMain | DOMRendererMain,
1873
+ ): r is DOMRendererMain {
1874
+ return r instanceof DOMRendererMain;
1875
+ }