@lightningjs/renderer 0.7.4 → 0.7.5

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 (33) hide show
  1. package/dist/src/core/CoreNode.d.ts +4 -0
  2. package/dist/src/core/CoreNode.js +22 -0
  3. package/dist/src/core/CoreNode.js.map +1 -1
  4. package/dist/src/core/text-rendering/TrFontManager.js +71 -27
  5. package/dist/src/core/text-rendering/TrFontManager.js.map +1 -1
  6. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.d.ts +1 -0
  7. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js +5 -1
  8. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js.map +1 -1
  9. package/dist/src/main-api/INode.d.ts +27 -0
  10. package/dist/src/main-api/Inspector.js +20 -15
  11. package/dist/src/main-api/Inspector.js.map +1 -1
  12. package/dist/src/main-api/RendererMain.js +3 -0
  13. package/dist/src/main-api/RendererMain.js.map +1 -1
  14. package/dist/src/render-drivers/main/MainOnlyNode.d.ts +4 -1
  15. package/dist/src/render-drivers/main/MainOnlyNode.js +10 -0
  16. package/dist/src/render-drivers/main/MainOnlyNode.js.map +1 -1
  17. package/dist/src/render-drivers/threadx/ThreadXMainNode.d.ts +4 -1
  18. package/dist/src/render-drivers/threadx/ThreadXMainNode.js +8 -0
  19. package/dist/src/render-drivers/threadx/ThreadXMainNode.js.map +1 -1
  20. package/dist/src/render-drivers/utils.d.ts +2 -0
  21. package/dist/src/render-drivers/utils.js +28 -0
  22. package/dist/src/render-drivers/utils.js.map +1 -1
  23. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  24. package/package.json +1 -1
  25. package/src/core/CoreNode.ts +29 -0
  26. package/src/core/text-rendering/TrFontManager.ts +104 -34
  27. package/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.ts +7 -4
  28. package/src/main-api/INode.ts +29 -0
  29. package/src/main-api/Inspector.ts +23 -16
  30. package/src/main-api/RendererMain.ts +3 -0
  31. package/src/render-drivers/main/MainOnlyNode.ts +13 -0
  32. package/src/render-drivers/threadx/ThreadXMainNode.ts +15 -1
  33. package/src/render-drivers/utils.ts +40 -0
@@ -17,10 +17,105 @@
17
17
  * limitations under the License.
18
18
  */
19
19
 
20
- import { SdfTrFontFace } from './font-face-types/SdfTrFontFace/SdfTrFontFace.js';
21
20
  import type { TrFontFace } from './font-face-types/TrFontFace.js';
22
- import { WebTrFontFace } from './font-face-types/WebTrFontFace.js';
23
21
  import type { TextRendererMap, TrFontProps } from './renderers/TextRenderer.js';
22
+ import memize from 'memize';
23
+
24
+ const weightConversions: { [key: string]: number } = {
25
+ normal: 400,
26
+ bold: 700,
27
+ bolder: 900,
28
+ lighter: 100,
29
+ };
30
+
31
+ const fontWeightToNumber = (weight: string | number): number => {
32
+ if (typeof weight === 'number') {
33
+ return weight;
34
+ }
35
+
36
+ return weightConversions[weight] || 400;
37
+ };
38
+
39
+ function rawResolveFontToUse(
40
+ familyMapsByPriority: FontFamilyMap[],
41
+ family: string,
42
+ weightIn: string | number,
43
+ style: string,
44
+ stretch: string,
45
+ ): TrFontFace | undefined {
46
+ const weight = fontWeightToNumber(weightIn);
47
+ let result = undefined;
48
+
49
+ for (const fontFamiles of familyMapsByPriority) {
50
+ if (result) {
51
+ break;
52
+ }
53
+
54
+ const fontFaces = fontFamiles[family];
55
+ if (!fontFaces) {
56
+ continue;
57
+ }
58
+
59
+ if (fontFaces.size === 1) {
60
+ // No Exact match found, find nearest weight match
61
+ console.warn(
62
+ `TrFontManager: Only one font face found for family: '${family}' - will be used for all weights and styles`,
63
+ );
64
+
65
+ result = fontFaces.values().next().value as TrFontFace;
66
+ continue;
67
+ }
68
+
69
+ const weightMap = new Map<number, TrFontFace>();
70
+ for (const fontFace of fontFaces) {
71
+ const fontFamilyWeight = fontWeightToNumber(fontFace.descriptors.weight);
72
+ if (
73
+ fontFamilyWeight === weight &&
74
+ fontFace.descriptors.style === style &&
75
+ fontFace.descriptors.stretch === stretch
76
+ ) {
77
+ result = fontFace;
78
+ break;
79
+ }
80
+
81
+ weightMap.set(fontFamilyWeight, fontFace);
82
+ }
83
+
84
+ // No Exact match found, find nearest weight match
85
+ const msg = `TrFontManager: No exact match: '${family} Weight: ${weight} Style: ${style} Stretch: ${stretch}'`;
86
+ console.error(msg);
87
+
88
+ if (!result && weight === 400 && weightMap.has(500)) {
89
+ result = weightMap.get(500);
90
+ }
91
+
92
+ if (!result && weight === 500 && weightMap.has(400)) {
93
+ result = weightMap.get(400);
94
+ }
95
+
96
+ if (!result && weight < 400) {
97
+ const lighter =
98
+ weightMap.get(300) || weightMap.get(200) || weightMap.get(100);
99
+ if (lighter) {
100
+ result = lighter;
101
+ }
102
+ }
103
+
104
+ if (!result && weight > 500) {
105
+ const bolder =
106
+ weightMap.get(600) ||
107
+ weightMap.get(700) ||
108
+ weightMap.get(800) ||
109
+ weightMap.get(900);
110
+ if (bolder) {
111
+ result = bolder;
112
+ }
113
+ }
114
+ }
115
+
116
+ return result;
117
+ }
118
+ const resolveFontToUse = memize(rawResolveFontToUse);
24
119
 
25
120
  /**
26
121
  * Structure mapping font family names to a set of font faces.
@@ -59,38 +154,13 @@ export class TrFontManager {
59
154
  familyMapsByPriority: FontFamilyMap[],
60
155
  props: TrFontProps,
61
156
  ): TrFontFace | undefined {
62
- const closeFaces: WebTrFontFace[] = [];
63
- const exactMatch = familyMapsByPriority.reduce<TrFontFace | undefined>(
64
- (prev, fontFamiles) => {
65
- if (prev) {
66
- return prev;
67
- }
68
- const fontFaces = fontFamiles[props.fontFamily];
69
- if (!fontFaces) {
70
- return undefined;
71
- }
72
- const fontFacesCopy = new Set(fontFaces);
73
-
74
- // Remove any font faces that don't match the style exactly
75
- // TODO: In the future we may enhance this to find the best match
76
- // based on font weight, style, etc.
77
- // See https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#font-matching-algorithm
78
- for (const fontFace of fontFacesCopy) {
79
- if (
80
- fontFace.descriptors.stretch !== props.fontStretch ||
81
- fontFace.descriptors.style !== props.fontStyle ||
82
- fontFace.descriptors.weight !== props.fontWeight
83
- ) {
84
- fontFacesCopy.delete(fontFace);
85
- }
86
- }
87
-
88
- // Return the first font face that matches the style exactly
89
- return fontFacesCopy.values().next().value;
90
- },
91
- undefined,
157
+ const { fontFamily, fontWeight, fontStyle, fontStretch } = props;
158
+ return resolveFontToUse(
159
+ familyMapsByPriority,
160
+ fontFamily,
161
+ fontWeight,
162
+ fontStyle,
163
+ fontStretch,
92
164
  );
93
-
94
- return exactMatch || closeFaces[0];
95
165
  }
96
166
  }
@@ -127,6 +127,10 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
127
127
  */
128
128
  private ssdfFontFamilies: FontFamilyMap = {};
129
129
  private msdfFontFamilies: FontFamilyMap = {};
130
+ private fontFamilyArray: FontFamilyMap[] = [
131
+ this.ssdfFontFamilies,
132
+ this.msdfFontFamilies,
133
+ ];
130
134
  private sdfShader: SdfShader;
131
135
  private rendererBounds: Bound;
132
136
 
@@ -734,10 +738,9 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
734
738
  //#endregion Overrides
735
739
 
736
740
  public resolveFontFace(props: TrFontProps): SdfTrFontFace | undefined {
737
- return TrFontManager.resolveFontFace(
738
- [this.msdfFontFamilies, this.ssdfFontFamilies],
739
- props,
740
- ) as SdfTrFontFace | undefined;
741
+ return TrFontManager.resolveFontFace(this.fontFamilyArray, props) as
742
+ | SdfTrFontFace
743
+ | undefined;
741
744
  }
742
745
 
743
746
  /**
@@ -392,8 +392,36 @@ export interface INodeWritableProps {
392
392
  * - `2 * Math.PI`: 360 rotation clockwise
393
393
  */
394
394
  rotation: number;
395
+ /**
396
+ * Node data element for custom data storage (optional)
397
+ *
398
+ * @remarks
399
+ * This property is used to store custom data on the Node as a key/value data store.
400
+ * Data values are limited to string, numbers, booleans. Strings will be truncated
401
+ * to a 2048 character limit for performance reasons.
402
+ *
403
+ * This is not a data storage mechanism for large amounts of data please use a
404
+ * dedicated data storage mechanism for that.
405
+ *
406
+ * The custom data will be reflected in the inspector as part of `data-*` attributes
407
+ *
408
+ * @default `undefined`
409
+ */
410
+ data?: CustomDataMap;
395
411
  }
396
412
 
413
+ /**
414
+ * A custom data map which can be stored on the INode
415
+ *
416
+ * @remarks
417
+ * This is a map of key-value pairs that can be stored on an INode. It is used
418
+ * to store custom data that can be used by the application.
419
+ * The data stored can only be of type string, number or boolean.
420
+ */
421
+ export type CustomDataMap = {
422
+ [key: string]: string | number | boolean;
423
+ };
424
+
397
425
  export type INodeAnimatableProps = {
398
426
  [Key in keyof INodeWritableProps as NonNullable<
399
427
  INodeWritableProps[Key]
@@ -403,6 +431,7 @@ export type INodeAnimatableProps = {
403
431
  };
404
432
 
405
433
  export interface INodeEvents {
434
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
406
435
  [s: string]: (target: INode, data: any) => void;
407
436
  }
408
437
 
@@ -111,13 +111,6 @@ const stylePropertyMap: {
111
111
 
112
112
  return { prop: 'transform', value: `scaleY(${v})` };
113
113
  },
114
- src: (v) => {
115
- if (!v) {
116
- return null;
117
- }
118
-
119
- return { prop: 'background-image', value: `url(${v})` };
120
- },
121
114
  color: (v) => {
122
115
  if (v === 0) {
123
116
  return null;
@@ -221,7 +214,7 @@ export class Inspector {
221
214
  this.root.style.transformOrigin = '0 0 0';
222
215
  this.root.style.transform = `scale(${this.scaleX}, ${this.scaleY})`;
223
216
  this.root.style.overflow = 'hidden';
224
- this.root.style.zIndex = '-65534';
217
+ this.root.style.zIndex = '65534';
225
218
  }
226
219
 
227
220
  createDiv(
@@ -254,6 +247,9 @@ export class Inspector {
254
247
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
255
248
  (div as any).node = node;
256
249
 
250
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
251
+ (node as any).div = div;
252
+
257
253
  return this.createProxy(node, div);
258
254
  }
259
255
 
@@ -335,6 +331,18 @@ export class Inspector {
335
331
  // special case for text
336
332
  if (property === 'text') {
337
333
  div.innerHTML = String(value);
334
+
335
+ // hide text because we can't render SDF fonts
336
+ // it would look weird and obstruct the WebGL rendering
337
+ div.style.visibility = 'hidden';
338
+ return;
339
+ }
340
+
341
+ // special case for images
342
+ // we're not setting any CSS properties to avoid images getting loaded twice
343
+ // as the renderer will handle the loading of the image. Setting it to `data-src`
344
+ if (property === 'src' && value) {
345
+ div.setAttribute(`data-src`, String(value));
338
346
  return;
339
347
  }
340
348
 
@@ -376,14 +384,13 @@ export class Inspector {
376
384
  }
377
385
 
378
386
  // custom data properties
379
- // Needs https://github.com/lightning-js/renderer/pull/178 to be merged
380
- // if (property === 'data') {
381
- // for (const key in value) {
382
- // // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
383
- // div.setAttribute(`data-${key}`, String(value[key]));
384
- // }
385
- // return;
386
- // }
387
+ if (property === 'data') {
388
+ for (const key in value) {
389
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
390
+ div.setAttribute(`data-${key}`, String(value[key]));
391
+ }
392
+ return;
393
+ }
387
394
  }
388
395
 
389
396
  // simple animation handler
@@ -39,6 +39,7 @@ import { FinalizationRegistryTextureUsageTracker } from './texture-usage-tracker
39
39
  import type { TextureUsageTracker } from './texture-usage-trackers/TextureUsageTracker.js';
40
40
  import { EventEmitter } from '../common/EventEmitter.js';
41
41
  import { Inspector } from './Inspector.js';
42
+ import { santizeCustomDataMap } from '../render-drivers/utils.js';
42
43
 
43
44
  /**
44
45
  * An immutable reference to a specific Texture type
@@ -501,6 +502,7 @@ export class RendererMain extends EventEmitter {
501
502
  props.colorBl ?? props.colorBottom ?? props.colorLeft ?? color;
502
503
  const colorBr =
503
504
  props.colorBr ?? props.colorBottom ?? props.colorRight ?? color;
505
+ const data = santizeCustomDataMap(props.data ?? {});
504
506
 
505
507
  return {
506
508
  x: props.x ?? 0,
@@ -536,6 +538,7 @@ export class RendererMain extends EventEmitter {
536
538
  pivotX: props.pivotX ?? props.pivot ?? 0.5,
537
539
  pivotY: props.pivotY ?? props.pivot ?? 0.5,
538
540
  rotation: props.rotation ?? 0,
541
+ data: data,
539
542
  };
540
543
  }
541
544
 
@@ -18,6 +18,7 @@
18
18
  */
19
19
 
20
20
  import type {
21
+ CustomDataMap,
21
22
  INode,
22
23
  INodeAnimatableProps,
23
24
  INodeWritableProps,
@@ -39,6 +40,7 @@ import type {
39
40
  NodeLoadedEventHandler,
40
41
  NodeFailedEventHandler,
41
42
  } from '../../common/CommonTypes.js';
43
+ import { santizeCustomDataMap } from '../utils.js';
42
44
 
43
45
  let nextId = 1;
44
46
 
@@ -56,6 +58,7 @@ export class MainOnlyNode extends EventEmitter implements INode {
56
58
  protected _parent: MainOnlyNode | null = null;
57
59
  protected _texture: TextureRef | null = null;
58
60
  protected _shader: ShaderRef | null = null;
61
+ protected _data: CustomDataMap | undefined = {};
59
62
 
60
63
  constructor(
61
64
  props: INodeWritableProps,
@@ -110,6 +113,7 @@ export class MainOnlyNode extends EventEmitter implements INode {
110
113
  this.shader = props.shader;
111
114
  this.texture = props.texture;
112
115
  this.src = props.src;
116
+ this._data = props.data;
113
117
  }
114
118
 
115
119
  get x(): number {
@@ -425,8 +429,17 @@ export class MainOnlyNode extends EventEmitter implements INode {
425
429
  }
426
430
  }
427
431
 
432
+ get data(): CustomDataMap | undefined {
433
+ return this._data;
434
+ }
435
+
436
+ set data(d: CustomDataMap) {
437
+ this._data = santizeCustomDataMap(d);
438
+ }
439
+
428
440
  destroy(): void {
429
441
  this.emit('beforeDestroy', {});
442
+ this.coreNode.destroy();
430
443
  this.parent = null;
431
444
  this.texture = null;
432
445
  this.emit('afterDestroy', {});
@@ -18,7 +18,11 @@
18
18
  */
19
19
 
20
20
  import type { IAnimationController } from '../../common/IAnimationController.js';
21
- import type { INode, INodeAnimatableProps } from '../../main-api/INode.js';
21
+ import type {
22
+ CustomDataMap,
23
+ INode,
24
+ INodeAnimatableProps,
25
+ } from '../../main-api/INode.js';
22
26
  import type {
23
27
  RendererMain,
24
28
  ShaderRef,
@@ -29,6 +33,7 @@ import type { NodeStruct } from './NodeStruct.js';
29
33
  import { SharedNode } from './SharedNode.js';
30
34
  import { ThreadXMainAnimationController } from './ThreadXMainAnimationController.js';
31
35
  import type { AnimationSettings } from '../../core/animations/CoreAnimation.js';
36
+ import { santizeCustomDataMap } from '../utils.js';
32
37
 
33
38
  export class ThreadXMainNode extends SharedNode implements INode {
34
39
  private nextAnimationId = 1;
@@ -36,6 +41,7 @@ export class ThreadXMainNode extends SharedNode implements INode {
36
41
  protected _children: ThreadXMainNode[] = [];
37
42
  protected _texture: TextureRef | null = null;
38
43
  protected _shader: ShaderRef | null = null;
44
+ protected _data: CustomDataMap | undefined = {};
39
45
  private _src = '';
40
46
 
41
47
  /**
@@ -171,6 +177,14 @@ export class ThreadXMainNode extends SharedNode implements INode {
171
177
  return this.curProps;
172
178
  }
173
179
 
180
+ get data(): CustomDataMap | undefined {
181
+ return this._data;
182
+ }
183
+
184
+ set data(d: CustomDataMap) {
185
+ this._data = santizeCustomDataMap(d);
186
+ }
187
+
174
188
  override destroy() {
175
189
  super.destroy();
176
190
  this.texture = null;
@@ -1,5 +1,6 @@
1
1
  import { CoreExtension } from '../../exports/core-api.js';
2
2
  import type { Stage } from '../core/Stage.js';
3
+ import type { CustomDataMap } from '../main-api/INode.js';
3
4
 
4
5
  /**
5
6
  * Type guard that checks if a Class extends CoreExtension.
@@ -55,3 +56,42 @@ export async function loadCoreExtension(
55
56
  );
56
57
  }
57
58
  }
59
+
60
+ export function santizeCustomDataMap(d: CustomDataMap): CustomDataMap {
61
+ const validTypes = { boolean: true, string: true, number: true };
62
+
63
+ const keys = Object.keys(d);
64
+ for (let i = 0; i < keys.length; i++) {
65
+ const key = keys[i];
66
+ if (!key) {
67
+ continue;
68
+ }
69
+
70
+ const value = d[key];
71
+ const valueType = typeof value;
72
+
73
+ // Typescript doesn't understand the above const valueType ¯\_(ツ)_/¯
74
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
75
+ // @ts-ignore-next-line
76
+ if (valueType === 'string' && value.length > 2048) {
77
+ console.warn(
78
+ `Custom Data value for ${key} is too long, it will be truncated to 2048 characters`,
79
+ );
80
+
81
+ // same here, see above comment, this can only be a string at this point
82
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
83
+ // @ts-ignore-next-line
84
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
85
+ d[key] = value.substring(0, 2048);
86
+ }
87
+
88
+ if (!validTypes[valueType as keyof typeof validTypes]) {
89
+ console.warn(
90
+ `Custom Data value for ${key} is not a boolean, string, or number, it will be ignored`,
91
+ );
92
+ delete d[key];
93
+ }
94
+ }
95
+
96
+ return d;
97
+ }