@lightningjs/renderer 0.5.0 → 0.6.0

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 (93) hide show
  1. package/README.md +40 -8
  2. package/dist/exports/main-api.d.ts +3 -3
  3. package/dist/exports/main-api.js +3 -3
  4. package/dist/exports/main-api.js.map +1 -1
  5. package/dist/src/core/CoreNode.d.ts +24 -10
  6. package/dist/src/core/CoreNode.js +92 -20
  7. package/dist/src/core/CoreNode.js.map +1 -1
  8. package/dist/src/core/CoreShaderManager.d.ts +4 -0
  9. package/dist/src/core/CoreShaderManager.js +8 -0
  10. package/dist/src/core/CoreShaderManager.js.map +1 -1
  11. package/dist/src/core/CoreTextNode.d.ts +2 -4
  12. package/dist/src/core/CoreTextNode.js +5 -53
  13. package/dist/src/core/CoreTextNode.js.map +1 -1
  14. package/dist/src/core/Stage.d.ts +14 -3
  15. package/dist/src/core/Stage.js +52 -32
  16. package/dist/src/core/Stage.js.map +1 -1
  17. package/dist/src/core/animations/CoreAnimation.d.ts +1 -0
  18. package/dist/src/core/animations/CoreAnimation.js +25 -15
  19. package/dist/src/core/animations/CoreAnimation.js.map +1 -1
  20. package/dist/src/core/lib/utils.d.ts +5 -1
  21. package/dist/src/core/lib/utils.js +17 -12
  22. package/dist/src/core/lib/utils.js.map +1 -1
  23. package/dist/src/core/platform.js +4 -4
  24. package/dist/src/core/platform.js.map +1 -1
  25. package/dist/src/core/renderers/CoreRenderer.d.ts +1 -3
  26. package/dist/src/core/renderers/CoreRenderer.js.map +1 -1
  27. package/dist/src/core/renderers/webgl/WebGlCoreRenderer.d.ts +11 -17
  28. package/dist/src/core/renderers/webgl/WebGlCoreRenderer.js +13 -30
  29. package/dist/src/core/renderers/webgl/WebGlCoreRenderer.js.map +1 -1
  30. package/dist/src/core/renderers/webgl/shaders/DefaultShader.js +3 -0
  31. package/dist/src/core/renderers/webgl/shaders/DefaultShader.js.map +1 -1
  32. package/dist/src/core/renderers/webgl/shaders/DynamicShader.js +5 -28
  33. package/dist/src/core/renderers/webgl/shaders/DynamicShader.js.map +1 -1
  34. package/dist/src/core/renderers/webgl/shaders/effects/RadialProgressEffect.d.ts +61 -0
  35. package/dist/src/core/renderers/webgl/shaders/effects/RadialProgressEffect.js +126 -0
  36. package/dist/src/core/renderers/webgl/shaders/effects/RadialProgressEffect.js.map +1 -0
  37. package/dist/src/core/text-rendering/renderers/CanvasTextRenderer.d.ts +20 -2
  38. package/dist/src/core/text-rendering/renderers/CanvasTextRenderer.js +98 -45
  39. package/dist/src/core/text-rendering/renderers/CanvasTextRenderer.js.map +1 -1
  40. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.d.ts +16 -4
  41. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js +80 -42
  42. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js.map +1 -1
  43. package/dist/src/core/text-rendering/renderers/TextRenderer.d.ts +16 -8
  44. package/dist/src/core/text-rendering/renderers/TextRenderer.js +34 -8
  45. package/dist/src/core/text-rendering/renderers/TextRenderer.js.map +1 -1
  46. package/dist/src/main-api/{IRenderDriver.d.ts → ICoreDriver.d.ts} +3 -2
  47. package/dist/src/main-api/{IRenderDriver.js → ICoreDriver.js} +1 -1
  48. package/dist/src/main-api/ICoreDriver.js.map +1 -0
  49. package/dist/src/main-api/INode.d.ts +2 -2
  50. package/dist/src/main-api/RendererMain.d.ts +16 -6
  51. package/dist/src/main-api/RendererMain.js +9 -3
  52. package/dist/src/main-api/RendererMain.js.map +1 -1
  53. package/dist/src/render-drivers/main/{MainRenderDriver.d.ts → MainCoreDriver.d.ts} +3 -2
  54. package/dist/src/render-drivers/main/{MainRenderDriver.js → MainCoreDriver.js} +14 -4
  55. package/dist/src/render-drivers/main/MainCoreDriver.js.map +1 -0
  56. package/dist/src/render-drivers/threadx/{ThreadXRenderDriver.d.ts → ThreadXCoreDriver.d.ts} +4 -2
  57. package/dist/src/render-drivers/threadx/{ThreadXRenderDriver.js → ThreadXCoreDriver.js} +18 -4
  58. package/dist/src/render-drivers/threadx/ThreadXCoreDriver.js.map +1 -0
  59. package/dist/src/render-drivers/threadx/ThreadXRendererMessage.d.ts +9 -0
  60. package/dist/src/render-drivers/threadx/ThreadXRendererMessage.js.map +1 -1
  61. package/dist/src/render-drivers/threadx/worker/renderer.js +8 -0
  62. package/dist/src/render-drivers/threadx/worker/renderer.js.map +1 -1
  63. package/dist/src/utils.js +2 -1
  64. package/dist/src/utils.js.map +1 -1
  65. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  66. package/exports/main-api.ts +3 -3
  67. package/package.json +8 -3
  68. package/src/core/CoreNode.ts +107 -21
  69. package/src/core/CoreShaderManager.ts +11 -0
  70. package/src/core/CoreTextNode.ts +5 -58
  71. package/src/core/Stage.ts +65 -34
  72. package/src/core/animations/CoreAnimation.ts +47 -27
  73. package/src/core/lib/utils.ts +39 -13
  74. package/src/core/platform.ts +5 -4
  75. package/src/core/renderers/CoreRenderer.ts +1 -2
  76. package/src/core/renderers/webgl/WebGlCoreRenderer.ts +14 -35
  77. package/src/core/renderers/webgl/shaders/DefaultShader.ts +4 -0
  78. package/src/core/renderers/webgl/shaders/DynamicShader.ts +5 -29
  79. package/src/core/renderers/webgl/shaders/effects/RadialProgressEffect.ts +186 -0
  80. package/src/core/text-rendering/renderers/CanvasTextRenderer.ts +107 -50
  81. package/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.ts +95 -44
  82. package/src/core/text-rendering/renderers/TextRenderer.ts +44 -16
  83. package/src/main-api/{IRenderDriver.ts → ICoreDriver.ts} +4 -2
  84. package/src/main-api/INode.ts +2 -2
  85. package/src/main-api/RendererMain.ts +23 -6
  86. package/src/render-drivers/main/{MainRenderDriver.ts → MainCoreDriver.ts} +17 -4
  87. package/src/render-drivers/threadx/{ThreadXRenderDriver.ts → ThreadXCoreDriver.ts} +23 -7
  88. package/src/render-drivers/threadx/ThreadXRendererMessage.ts +11 -0
  89. package/src/render-drivers/threadx/worker/renderer.ts +10 -0
  90. package/src/utils.ts +2 -1
  91. package/dist/src/main-api/IRenderDriver.js.map +0 -1
  92. package/dist/src/render-drivers/main/MainRenderDriver.js.map +0 -1
  93. package/dist/src/render-drivers/threadx/ThreadXRenderDriver.js.map +0 -1
@@ -28,6 +28,8 @@ import {
28
28
  getNormalizedRgbaComponents,
29
29
  type Rect,
30
30
  getNormalizedAlphaComponent,
31
+ type BoundWithValid,
32
+ createBound,
31
33
  } from '../../lib/utils.js';
32
34
  import type { ImageTexture } from '../../textures/ImageTexture.js';
33
35
  import type { TrFontFace } from '../font-face-types/TrFontFace.js';
@@ -86,13 +88,24 @@ export interface CanvasTextRendererState extends TextRendererState {
86
88
  lightning2TextRenderer: LightningTextTextureRenderer;
87
89
  renderInfo: RenderInfo | undefined;
88
90
  renderWindow: Bound | undefined;
91
+ visibleWindow: BoundWithValid;
89
92
  }
90
93
 
94
+ /**
95
+ * Ephemeral bounds object used for intersection calculations
96
+ *
97
+ * @remarks
98
+ * Used to avoid creating a new object every time we need to intersect
99
+ * element bounds.
100
+ */
101
+ const tmpElementBounds = createBound(0, 0, 0, 0);
102
+
91
103
  export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
92
104
  protected canvas: OffscreenCanvas | HTMLCanvasElement;
93
105
  protected context:
94
106
  | OffscreenCanvasRenderingContext2D
95
107
  | CanvasRenderingContext2D;
108
+ private rendererBounds: Bound;
96
109
 
97
110
  constructor(stage: Stage) {
98
111
  super(stage);
@@ -111,6 +124,12 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
111
124
  }
112
125
  assertTruthy(context);
113
126
  this.context = context;
127
+ this.rendererBounds = {
128
+ x1: 0,
129
+ y1: 0,
130
+ x2: this.stage.options.appWidth,
131
+ y2: this.stage.options.appHeight,
132
+ };
114
133
  }
115
134
 
116
135
  //#region Overrides
@@ -121,68 +140,70 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
121
140
  fontFamily: (state, value) => {
122
141
  state.props.fontFamily = value;
123
142
  state.fontInfo = undefined;
124
- this.markForReload(state);
143
+ this.invalidateLayoutCache(state);
125
144
  },
126
145
  fontWeight: (state, value) => {
127
146
  state.props.fontWeight = value;
128
147
  state.fontInfo = undefined;
129
- this.markForReload(state);
148
+ this.invalidateLayoutCache(state);
130
149
  },
131
150
  fontStyle: (state, value) => {
132
151
  state.props.fontStyle = value;
133
152
  state.fontInfo = undefined;
134
- this.markForReload(state);
153
+ this.invalidateLayoutCache(state);
135
154
  },
136
155
  fontStretch: (state, value) => {
137
156
  state.props.fontStretch = value;
138
157
  state.fontInfo = undefined;
139
- this.markForReload(state);
158
+ this.invalidateLayoutCache(state);
140
159
  },
141
160
  fontSize: (state, value) => {
142
161
  state.props.fontSize = value;
143
162
  state.fontInfo = undefined;
144
- this.markForReload(state);
163
+ this.invalidateLayoutCache(state);
145
164
  },
146
165
  text: (state, value) => {
147
166
  state.props.text = value;
148
- this.markForReload(state);
167
+ this.invalidateLayoutCache(state);
149
168
  },
150
169
  textAlign: (state, value) => {
151
170
  state.props.textAlign = value;
152
- this.markForReload(state);
171
+ this.invalidateLayoutCache(state);
153
172
  },
154
173
  color: (state, value) => {
155
174
  state.props.color = value;
156
- this.markForReload(state);
175
+ this.invalidateLayoutCache(state);
157
176
  },
158
177
  x: (state, value) => {
159
178
  state.props.x = value;
179
+ this.invalidateVisibleWindowCache(state);
160
180
  },
161
181
  y: (state, value) => {
162
182
  state.props.y = value;
183
+ this.invalidateVisibleWindowCache(state);
163
184
  },
164
185
  contain: (state, value) => {
165
186
  state.props.contain = value;
166
- this.markForReload(state);
187
+ this.invalidateLayoutCache(state);
167
188
  },
168
189
  width: (state, value) => {
169
190
  state.props.width = value;
170
- this.markForReload(state);
191
+ this.invalidateLayoutCache(state);
171
192
  },
172
193
  height: (state, value) => {
173
194
  state.props.height = value;
174
- this.markForReload(state);
195
+ this.invalidateLayoutCache(state);
175
196
  },
176
197
  offsetY: (state, value) => {
177
198
  state.props.offsetY = value;
178
- this.markForReload(state);
199
+ this.invalidateLayoutCache(state);
179
200
  },
180
201
  scrollY: (state, value) => {
181
202
  state.props.scrollY = value;
182
203
  },
183
204
  letterSpacing: (state, value) => {
184
205
  state.props.letterSpacing = value;
185
- this.markForReload(state);
206
+ this.invalidateLayoutCache(state);
186
207
  },
187
208
  // debug: (state, value) => {
188
209
  // state.props.debug = value;
@@ -217,6 +238,7 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
217
238
  return {
218
239
  props,
219
240
  status: 'initialState',
241
+ updateScheduled: false,
220
242
  emitter: new EventEmitter(),
221
243
  canvasPages: undefined,
222
244
  lightning2TextRenderer: new LightningTextTextureRenderer(
@@ -224,6 +246,13 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
224
246
  this.context,
225
247
  ),
226
248
  renderWindow: undefined,
249
+ visibleWindow: {
250
+ x1: 0,
251
+ y1: 0,
252
+ x2: 0,
253
+ y2: 0,
254
+ valid: false,
255
+ },
227
256
  renderInfo: undefined,
228
257
  forceFullLayoutCalc: false,
229
258
  textW: 0,
@@ -289,13 +318,13 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
289
318
  state.props.contain === 'none' ? undefined : state.props.width,
290
319
  letterSpacing: state.props.letterSpacing,
291
320
  };
292
- const renderInfoCalculateTime = performance.now();
321
+ // const renderInfoCalculateTime = performance.now();
293
322
  state.renderInfo = state.lightning2TextRenderer.calculateRenderInfo();
294
- console.log(
295
- 'Render info calculated in',
296
- performance.now() - renderInfoCalculateTime,
297
- 'ms',
298
- );
323
+ // console.log(
324
+ // 'Render info calculated in',
325
+ // performance.now() - renderInfoCalculateTime,
326
+ // 'ms',
327
+ // );
299
328
  state.textH = state.renderInfo.lineHeight * state.renderInfo.lines.length;
300
329
  state.textW = state.renderInfo.width;
301
330
 
@@ -304,26 +333,24 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
304
333
  }
305
334
 
306
335
  const { x, y, width, height, scrollY, contain } = state.props;
307
-
336
+ const { visibleWindow } = state;
308
337
  let { renderWindow, canvasPages } = state;
309
338
 
310
- // Figure out whats actually in the bounds of the renderer/canvas (visibleWindow)
311
- const rendererBounds: Bound = {
312
- x1: 0,
313
- y1: 0,
314
- x2: this.stage.options.appWidth,
315
- y2: this.stage.options.appHeight,
316
- };
317
- const elementBounds: Bound = {
318
- x1: x,
319
- y1: y,
320
- x2: contain !== 'none' ? x + width : Infinity,
321
- y2: contain === 'both' ? y + height : Infinity,
322
- };
323
- /**
324
- * Area that is visible on the screen.
325
- */
326
- const visibleWindow: Bound = intersectBound(rendererBounds, elementBounds);
339
+ if (!visibleWindow.valid) {
340
+ // Figure out whats actually in the bounds of the renderer/canvas (visibleWindow)
341
+ const elementBounds = createBound(
342
+ x,
343
+ y,
344
+ contain !== 'none' ? x + width : Infinity,
345
+ contain === 'both' ? y + height : Infinity,
346
+ tmpElementBounds,
347
+ );
348
+ /**
349
+ * Area that is visible on the screen.
350
+ */
351
+ intersectBound(this.rendererBounds, elementBounds, visibleWindow);
352
+ visibleWindow.valid = true;
353
+ }
327
354
 
328
355
  const visibleWindowHeight = visibleWindow.y2 - visibleWindow.y1;
329
356
 
@@ -331,9 +358,16 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
331
358
  visibleWindowHeight / state.renderInfo.lineHeight,
332
359
  );
333
360
 
334
- // Return early if we're still viewing inside the established render window
335
- // No need to re-render what we've already rendered
336
- if (renderWindow && canvasPages) {
361
+ if (visibleWindowHeight === 0) {
362
+ // Nothing to render. Clear any canvasPages and existing renderWindow
363
+ // Return early.
364
+ canvasPages = undefined;
365
+ renderWindow = undefined;
366
+ this.setStatus(state, 'loaded');
367
+ return;
368
+ } else if (renderWindow && canvasPages) {
369
+ // Return early if we're still viewing inside the established render window
370
+ // No need to re-render what we've already rendered
337
371
  const renderWindowScreenX1 = x + renderWindow.x1;
338
372
  const renderWindowScreenY1 = y - scrollY + renderWindow.y1;
339
373
  const renderWindowScreenX2 = x + renderWindow.x2;
@@ -344,8 +378,10 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
344
378
  renderWindowScreenX2 >= visibleWindow.x2 &&
345
379
  renderWindowScreenY1 <= visibleWindow.y1 &&
346
380
  renderWindowScreenY2 >= visibleWindow.y2
347
- )
381
+ ) {
382
+ this.setStatus(state, 'loaded');
348
383
  return;
384
+ }
349
385
  if (renderWindowScreenY2 < visibleWindow.y2) {
350
386
  // We've scrolled up, so we need to render the next page
351
387
  renderWindow.y1 += maxLinesPerCanvasPage * state.renderInfo.lineHeight;
@@ -400,7 +436,7 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
400
436
  ];
401
437
  state.canvasPages = canvasPages;
402
438
 
403
- const scrollYNearestPage = Math.ceil(scrollY / pageHeight) * pageHeight;
439
+ const scrollYNearestPage = page1Block * pageHeight;
404
440
 
405
441
  renderWindow = {
406
442
  x1: 0,
@@ -450,7 +486,7 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
450
486
  }
451
487
  pageInfo.valid = true;
452
488
  }
453
- console.log('pageDrawTime', performance.now() - pageDrawTime, 'ms');
489
+ // console.log('pageDrawTime', performance.now() - pageDrawTime, 'ms');
454
490
 
455
491
  // Report final status
456
492
  this.setStatus(state, 'loaded');
@@ -509,7 +545,7 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
509
545
  const combinedAlpha = alpha * getNormalizedAlphaComponent(color);
510
546
 
511
547
  if (canvasPages[0].valid) {
512
- this.stage.renderer.addRenderable({
548
+ this.stage.renderer.addQuad({
513
549
  alpha: combinedAlpha,
514
550
  clippingRect,
515
551
  colorBl: 0xffffffff,
@@ -532,7 +568,7 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
532
568
  });
533
569
  }
534
570
  if (canvasPages[1].valid) {
535
- this.stage.renderer.addRenderable({
571
+ this.stage.renderer.addQuad({
536
572
  alpha: combinedAlpha,
537
573
  clippingRect,
538
574
  colorBl: 0xffffffff,
@@ -555,7 +591,7 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
555
591
  });
556
592
  }
557
593
  if (canvasPages[2].valid) {
558
- this.stage.renderer.addRenderable({
594
+ this.stage.renderer.addQuad({
559
595
  alpha: combinedAlpha,
560
596
  clippingRect,
561
597
  colorBl: 0xffffffff,
@@ -612,9 +648,31 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
612
648
  }
613
649
  //#endregion Overrides
614
650
 
615
- private markForReload(state: CanvasTextRendererState): void {
651
+ /**
652
+ * Invalidate the visible window stored in the state. This will cause a new
653
+ * visible window to be calculated on the next update.
654
+ *
655
+ * @param state
656
+ */
657
+ protected invalidateVisibleWindowCache(state: CanvasTextRendererState): void {
658
+ state.visibleWindow.valid = false;
659
+ this.setStatus(state, 'loading');
660
+ this.scheduleUpdateState(state);
661
+ }
662
+
663
+ /**
664
+ * Invalidate the layout cache stored in the state. This will cause the text
665
+ * to be re-layed out on the next update.
666
+ *
667
+ * @remarks
668
+ * This also invalidates the visible window cache.
669
+ *
670
+ * @param state
671
+ */
672
+ private invalidateLayoutCache(state: CanvasTextRendererState): void {
616
673
  state.renderInfo = undefined;
617
674
  this.setStatus(state, 'loading');
675
+ this.scheduleUpdateState(state);
618
676
  }
619
677
 
620
678
  private onFontLoaded(
@@ -625,7 +683,7 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
625
683
  return;
626
684
  }
627
685
  state.fontInfo.loaded = true;
628
- this.updateState(state);
686
+ this.scheduleUpdateState(state);
629
687
  }
630
688
 
631
689
  private onFontLoadError(
@@ -645,7 +703,6 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
645
703
  `CanvasTextRenderer: Error loading font '${state.fontInfo.cssString}'`,
646
704
  error,
647
705
  );
648
-
649
- this.updateState(state);
706
+ this.scheduleUpdateState(state);
650
707
  }
651
708
  }
@@ -17,7 +17,13 @@
17
17
  * limitations under the License.
18
18
  */
19
19
 
20
- import { intersectBound, type Bound, type Rect } from '../../../lib/utils.js';
20
+ import {
21
+ intersectBound,
22
+ type Bound,
23
+ type Rect,
24
+ createBound,
25
+ type BoundWithValid,
26
+ } from '../../../lib/utils.js';
21
27
  import {
22
28
  TextRenderer,
23
29
  type TrProps,
@@ -73,6 +79,8 @@ export interface SdfTextRendererState extends TextRendererState {
73
79
 
74
80
  renderWindow: Bound | undefined;
75
81
 
82
+ visibleWindow: BoundWithValid;
83
+
76
84
  bufferNumFloats: number;
77
85
 
78
86
  bufferNumQuads: number;
@@ -90,6 +98,15 @@ export interface SdfTextRendererState extends TextRendererState {
90
98
  trFontFace: SdfTrFontFace | undefined;
91
99
  }
92
100
 
101
+ /**
102
+ * Ephemeral bounds object used for intersection calculations
103
+ *
104
+ * @remarks
105
+ * Used to avoid creating a new object every time we need to intersect
106
+ * element bounds.
107
+ */
108
+ const tmpElementBounds = createBound(0, 0, 0, 0);
109
+
93
110
  /**
94
111
  * Singleton class for rendering text using signed distance fields.
95
112
  *
@@ -103,10 +120,17 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
103
120
  private ssdfFontFamilies: FontFamilyMap = {};
104
121
  private msdfFontFamilies: FontFamilyMap = {};
105
122
  private sdfShader: SdfShader;
123
+ private rendererBounds: Bound;
106
124
 
107
125
  constructor(stage: Stage) {
108
126
  super(stage);
109
127
  this.sdfShader = this.stage.shManager.loadShader('SdfShader').shader;
128
+ this.rendererBounds = {
129
+ x1: 0,
130
+ y1: 0,
131
+ x2: this.stage.options.appWidth,
132
+ y2: this.stage.options.appHeight,
133
+ };
110
134
  }
111
135
 
112
136
  //#region Overrides
@@ -115,70 +139,75 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
115
139
  fontFamily: (state, value) => {
116
140
  state.props.fontFamily = value;
117
141
  state.trFontFace = undefined;
118
- this.invalidateCache(state);
142
+ this.invalidateLayoutCache(state);
119
143
  },
120
144
  fontWeight: (state, value) => {
121
145
  state.props.fontWeight = value;
122
146
  state.trFontFace = undefined;
123
- this.invalidateCache(state);
147
+ this.invalidateLayoutCache(state);
124
148
  },
125
149
  fontStyle: (state, value) => {
126
150
  state.props.fontStyle = value;
127
151
  state.trFontFace = undefined;
128
- this.invalidateCache(state);
152
+ this.invalidateLayoutCache(state);
129
153
  },
130
154
  fontStretch: (state, value) => {
131
155
  state.props.fontStretch = value;
132
156
  state.trFontFace = undefined;
133
- this.invalidateCache(state);
157
+ this.invalidateLayoutCache(state);
134
158
  },
135
159
  fontSize: (state, value) => {
136
160
  state.props.fontSize = value;
137
- this.invalidateCache(state);
161
+ this.invalidateLayoutCache(state);
138
162
  },
139
163
  text: (state, value) => {
140
164
  state.props.text = value;
141
- this.invalidateCache(state);
165
+ this.invalidateLayoutCache(state);
142
166
  },
143
167
  textAlign: (state, value) => {
144
168
  state.props.textAlign = value;
145
- this.invalidateCache(state);
169
+ this.invalidateLayoutCache(state);
146
170
  },
147
171
  color: (state, value) => {
148
172
  state.props.color = value;
149
173
  },
150
174
  x: (state, value) => {
151
175
  state.props.x = value;
176
+ this.invalidateVisibleWindowCache(state);
152
177
  },
153
178
  y: (state, value) => {
154
179
  state.props.y = value;
180
+ this.invalidateVisibleWindowCache(state);
155
181
  },
156
182
  contain: (state, value) => {
157
183
  state.props.contain = value;
158
- this.invalidateCache(state);
184
+ this.invalidateLayoutCache(state);
159
185
  },
160
186
  width: (state, value) => {
161
187
  state.props.width = value;
162
- this.invalidateCache(state);
188
+ this.invalidateLayoutCache(state);
163
189
  },
164
190
  height: (state, value) => {
165
191
  state.props.height = value;
166
- this.invalidateCache(state);
192
+ this.invalidateLayoutCache(state);
167
193
  },
168
194
  offsetY: (state, value) => {
169
195
  state.props.offsetY = value;
170
- this.invalidateCache(state);
196
+ this.invalidateLayoutCache(state);
171
197
  },
172
198
  scrollable: (state, value) => {
173
199
  state.props.scrollable = value;
174
- this.invalidateCache(state);
200
+ this.invalidateLayoutCache(state);
175
201
  },
176
202
  scrollY: (state, value) => {
177
203
  state.props.scrollY = value;
204
+ // Scrolling doesn't need to invalidate any caches, but it does need to
205
+ // schedule an update
206
+ this.scheduleUpdateState(state);
178
207
  },
179
208
  letterSpacing: (state, value) => {
180
209
  state.props.letterSpacing = value;
181
- this.invalidateCache(state);
210
+ this.invalidateLayoutCache(state);
182
211
  },
183
212
  debug: (state, value) => {
184
213
  state.props.debug = value;
@@ -229,10 +258,18 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
229
258
  return {
230
259
  props,
231
260
  status: 'initialState',
261
+ updateScheduled: false,
232
262
  emitter: new EventEmitter(),
233
263
  lineCache: [],
234
264
  forceFullLayoutCalc: false,
235
265
  renderWindow: undefined,
266
+ visibleWindow: {
267
+ x1: 0,
268
+ y1: 0,
269
+ x2: 0,
270
+ y2: 0,
271
+ valid: false,
272
+ },
236
273
  bufferNumFloats: 0,
237
274
  bufferNumQuads: 0,
238
275
  vertexBuffer: undefined,
@@ -255,7 +292,6 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
255
292
  }
256
293
 
257
294
  override updateState(state: SdfTextRendererState): void {
258
- const updateStartTime = performance.now();
259
295
  let { trFontFace } = state;
260
296
  const { textH, lineCache, debugData, forceFullLayoutCalc } = state;
261
297
  debugData.updateCount++;
@@ -280,9 +316,8 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
280
316
  // If the font hasn't been loaded yet, stop here.
281
317
  // Listen for the 'loaded' event and forward fontLoaded event
282
318
  if (!trFontFace.loaded) {
283
- trFontFace.on('loaded', function loadedHandler() {
284
- state.emitter.emit('fontLoaded', {});
285
- trFontFace?.off('fontLoaded', loadedHandler);
319
+ trFontFace.once('loaded', () => {
320
+ this.scheduleUpdateState(state);
286
321
  });
287
322
  return;
288
323
  }
@@ -317,23 +352,23 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
317
352
  vertexBuffer = new Float32Array(neededLength * 2);
318
353
  }
319
354
 
320
- // Figure out whats actually in the bounds of the renderer/canvas (visibleWindow)
321
- const rendererBounds: Bound = {
322
- x1: 0,
323
- y1: 0,
324
- x2: this.stage.options.appWidth,
325
- y2: this.stage.options.appHeight,
326
- };
327
- const elementBounds: Bound = {
328
- x1: x,
329
- y1: y,
330
- x2: contain !== 'none' ? x + width : Infinity,
331
- y2: contain === 'both' ? y + height : Infinity,
332
- };
333
- /**
334
- * Area that is visible on the screen.
335
- */
336
- const visibleWindow: Bound = intersectBound(rendererBounds, elementBounds);
355
+ const visibleWindow = state.visibleWindow;
356
+ // If the visibleWindow is not valid, calculate it
357
+ if (!visibleWindow.valid) {
358
+ // Figure out whats actually in the bounds of the renderer/canvas (visibleWindow)
359
+ const elementBounds = createBound(
360
+ x,
361
+ y,
362
+ contain !== 'none' ? x + width : Infinity,
363
+ contain === 'both' ? y + height : Infinity,
364
+ tmpElementBounds, // Prevent allocation by using this temp object
365
+ );
366
+ /**
367
+ * Area that is visible on the screen.
368
+ */
369
+ intersectBound(this.rendererBounds, elementBounds, state.visibleWindow);
370
+ visibleWindow.valid = true;
371
+ }
337
372
 
338
373
  // Return early if we're still viewing inside the established render window
339
374
  // No need to re-render what we've already rendered
@@ -344,10 +379,13 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
344
379
  x + renderWindow.x2 >= visibleWindow.x2 &&
345
380
  y - scrollY + renderWindow.y1 <= visibleWindow.y1 &&
346
381
  y - scrollY + renderWindow.y2 >= visibleWindow.y2
347
- )
382
+ ) {
383
+ this.setStatus(state, 'loaded');
348
384
  return;
385
+ }
349
386
  // Otherwise clear the renderWindow so it can be redone
350
- state.renderWindow = renderWindow = undefined;
387
+ renderWindow = state.renderWindow = undefined;
388
+ this.setStatus(state, 'loading');
351
389
  }
352
390
 
353
391
  const { offsetY, textAlign } = state.props;
@@ -456,10 +494,7 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
456
494
  textH = 0,
457
495
  distanceRange,
458
496
  vertexBuffer,
459
- bufferNumFloats,
460
497
  bufferUploaded,
461
- renderWindow,
462
- debugData,
463
498
  trFontFace,
464
499
  } = state;
465
500
 
@@ -540,7 +575,7 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
540
575
  renderOp.length = state.bufferNumFloats;
541
576
  renderOp.numQuads = state.bufferNumQuads;
542
577
 
543
- renderer.addRenderable(renderOp);
578
+ renderer.addRenderOp(renderOp);
544
579
 
545
580
  // const elementRect = {
546
581
  // x: x,
@@ -621,16 +656,32 @@ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
621
656
  }
622
657
 
623
658
  /**
624
- * Invalidate the cache stored in the state. This will cause the text to be
625
- * re-layed out on the next update.
659
+ * Invalidate the visible window stored in the state. This will cause a new
660
+ * visible window to be calculated on the next update.
661
+ *
662
+ * @param state
663
+ */
664
+ protected invalidateVisibleWindowCache(state: SdfTextRendererState): void {
665
+ state.visibleWindow.valid = false;
666
+ this.scheduleUpdateState(state);
667
+ }
668
+
669
+ /**
670
+ * Invalidate the layout cache stored in the state. This will cause the text
671
+ * to be re-layed out on the next update.
672
+ *
673
+ * @remarks
674
+ * This also invalidates the visible window cache.
626
675
  *
627
676
  * @param state
628
677
  */
629
- protected invalidateCache(state: SdfTextRendererState): void {
678
+ protected invalidateLayoutCache(state: SdfTextRendererState): void {
679
+ state.visibleWindow.valid = false;
630
680
  state.renderWindow = undefined;
631
681
  state.textH = undefined;
632
682
  state.textW = undefined;
633
683
  state.lineCache = [];
634
684
  this.setStatus(state, 'loading');
685
+ this.scheduleUpdateState(state);
635
686
  }
636
687
  }