@lightningjs/renderer 0.9.1 → 0.9.3

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 (97) hide show
  1. package/dist/exports/index.d.ts +3 -0
  2. package/dist/{src/core/text-rendering/renderers/SdfTextRenderer/internal/findNearestMultiple.js → exports/index.js} +4 -11
  3. package/dist/exports/index.js.map +1 -0
  4. package/dist/src/common/IAnimationController.d.ts +58 -1
  5. package/dist/src/common/IAnimationController.js +0 -18
  6. package/dist/src/common/IAnimationController.js.map +1 -1
  7. package/dist/src/core/CoreNode.js +40 -0
  8. package/dist/src/core/CoreNode.js.map +1 -1
  9. package/dist/src/core/CoreTextureManager.d.ts +35 -0
  10. package/dist/src/core/CoreTextureManager.js +1 -1
  11. package/dist/src/core/CoreTextureManager.js.map +1 -1
  12. package/dist/src/core/TextureList.js +34 -0
  13. package/dist/src/core/TextureList.js.map +1 -0
  14. package/dist/src/core/animations/CoreAnimation.d.ts +2 -2
  15. package/dist/src/core/animations/CoreAnimation.js +33 -10
  16. package/dist/src/core/animations/CoreAnimation.js.map +1 -1
  17. package/dist/src/core/animations/CoreAnimationController.d.ts +8 -12
  18. package/dist/src/core/animations/CoreAnimationController.js +42 -46
  19. package/dist/src/core/animations/CoreAnimationController.js.map +1 -1
  20. package/dist/src/core/lib/ImageWorker.d.ts +0 -1
  21. package/dist/src/core/lib/ImageWorker.js +8 -10
  22. package/dist/src/core/lib/ImageWorker.js.map +1 -1
  23. package/dist/src/core/lib/utils.d.ts +1 -0
  24. package/dist/src/core/lib/utils.js +20 -0
  25. package/dist/src/core/lib/utils.js.map +1 -1
  26. package/dist/src/core/renderers/webgl/WebGlCoreRenderer.js +25 -0
  27. package/dist/src/core/renderers/webgl/WebGlCoreRenderer.js.map +1 -1
  28. package/dist/src/core/renderers/webgl/newShader/effectsMock.d.ts +1 -0
  29. package/dist/src/core/renderers/webgl/newShader/effectsMock.js +36 -0
  30. package/dist/src/core/renderers/webgl/newShader/effectsMock.js.map +1 -0
  31. package/dist/src/core/renderers/webgl/shaders/effects/BorderEffect.js +2 -1
  32. package/dist/src/core/renderers/webgl/shaders/effects/BorderEffect.js.map +1 -1
  33. package/dist/src/core/textures/ImageTexture.d.ts +5 -1
  34. package/dist/src/core/textures/ImageTexture.js +20 -9
  35. package/dist/src/core/textures/ImageTexture.js.map +1 -1
  36. package/dist/src/core/utils.js +1 -6
  37. package/dist/src/core/utils.js.map +1 -1
  38. package/dist/src/main-api/INode.d.ts +1 -1
  39. package/dist/src/main-api/Renderer.d.ts +314 -0
  40. package/dist/src/main-api/Renderer.js +387 -0
  41. package/dist/src/main-api/Renderer.js.map +1 -0
  42. package/dist/src/main-api/utils.d.ts +2 -0
  43. package/dist/src/main-api/utils.js +34 -0
  44. package/dist/src/main-api/utils.js.map +1 -0
  45. package/dist/src/render-drivers/threadx/ThreadXMainAnimationController.d.ts +8 -4
  46. package/dist/src/render-drivers/threadx/ThreadXMainAnimationController.js +53 -24
  47. package/dist/src/render-drivers/threadx/ThreadXMainAnimationController.js.map +1 -1
  48. package/dist/src/render-drivers/threadx/worker/ThreadXRendererNode.js +6 -0
  49. package/dist/src/render-drivers/threadx/worker/ThreadXRendererNode.js.map +1 -1
  50. package/dist/src/render-drivers/utils.js +6 -1
  51. package/dist/src/render-drivers/utils.js.map +1 -1
  52. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  53. package/{dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/roundUpToMultiple.js → exports/index.ts} +4 -14
  54. package/package.json +1 -1
  55. package/src/common/IAnimationController.ts +60 -1
  56. package/src/core/CoreNode.ts +45 -0
  57. package/src/core/CoreTextureManager.ts +40 -1
  58. package/src/core/animations/CoreAnimation.ts +35 -11
  59. package/src/core/animations/CoreAnimationController.ts +48 -53
  60. package/src/core/lib/ImageWorker.ts +10 -13
  61. package/src/core/lib/utils.ts +25 -0
  62. package/src/core/renderers/webgl/WebGlCoreRenderer.ts +27 -0
  63. package/src/core/renderers/webgl/shaders/effects/BorderEffect.ts +2 -1
  64. package/src/core/textures/ImageTexture.ts +26 -9
  65. package/src/core/utils.ts +1 -7
  66. package/src/main-api/INode.ts +1 -1
  67. package/src/render-drivers/threadx/ThreadXMainAnimationController.ts +63 -27
  68. package/src/render-drivers/threadx/worker/ThreadXRendererNode.ts +6 -0
  69. package/src/render-drivers/utils.ts +6 -1
  70. package/dist/src/core/lib/WebGlContext.d.ts +0 -414
  71. package/dist/src/core/lib/WebGlContext.js +0 -640
  72. package/dist/src/core/lib/WebGlContext.js.map +0 -1
  73. package/dist/src/core/scene/Scene.d.ts +0 -59
  74. package/dist/src/core/scene/Scene.js +0 -106
  75. package/dist/src/core/scene/Scene.js.map +0 -1
  76. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/findNearestMultiple.d.ts +0 -8
  77. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/findNearestMultiple.js.map +0 -1
  78. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/SdfBufferHelper.d.ts +0 -19
  79. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/SdfBufferHelper.js +0 -84
  80. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/SdfBufferHelper.js.map +0 -1
  81. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/layoutLine.d.ts +0 -8
  82. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/layoutLine.js +0 -40
  83. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/layoutLine.js.map +0 -1
  84. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/layoutText2.d.ts +0 -2
  85. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/layoutText2.js +0 -41
  86. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/layoutText2.js.map +0 -1
  87. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/utils.d.ts +0 -1
  88. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/utils.js +0 -4
  89. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/utils.js.map +0 -1
  90. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2.js +0 -2
  91. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2.js.map +0 -1
  92. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/makeRenderWindow.d.ts +0 -20
  93. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/makeRenderWindow.js +0 -55
  94. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/makeRenderWindow.js.map +0 -1
  95. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/roundUpToMultiple.d.ts +0 -9
  96. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/roundUpToMultiple.js.map +0 -1
  97. /package/dist/src/core/{text-rendering/renderers/SdfTextRenderer/internal/layoutText2.d.ts → TextureList.d.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/renderer",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Lightning 3 Renderer",
5
5
  "type": "module",
6
6
  "main": "./dist/exports/index.js",
@@ -16,14 +16,73 @@
16
16
  * See the License for the specific language governing permissions and
17
17
  * limitations under the License.
18
18
  */
19
+ import type { IEventEmitter } from '@lightningjs/threadx';
19
20
 
20
21
  export type AnimationControllerState = 'running' | 'paused' | 'stopped';
21
22
 
22
- export interface IAnimationController {
23
+ /**
24
+ * Animation Controller interface
25
+ *
26
+ * @remarks
27
+ * This interface is used to control animations. It provides methods to start,
28
+ * stop, pause, and restore animations. It also provides a way to wait for the
29
+ * animation to stop.
30
+ *
31
+ * This interface extends the `IEventEmitter` interface, which means you can
32
+ * listen to these events emitted by the animation controller:
33
+ * - `animating` - Emitted when the animation finishes it's delay phase and
34
+ * starts animating.
35
+ * - `stopped` - Emitted when the animation stops either by calling the `stop()`
36
+ * method or when the animation finishes naturally.
37
+ */
38
+ export interface IAnimationController extends IEventEmitter {
39
+ /**
40
+ * Start the animation
41
+ *
42
+ * @remarks
43
+ * If the animation is paused this method will resume the animation.
44
+ */
23
45
  start(): IAnimationController;
46
+ /**
47
+ * Stop the animation
48
+ *
49
+ * @remarks
50
+ * Resets the animation to the start state
51
+ */
24
52
  stop(): IAnimationController;
53
+ /**
54
+ * Pause the animation
55
+ */
25
56
  pause(): IAnimationController;
57
+ /**
58
+ * Restore the animation to the original values
59
+ */
26
60
  restore(): IAnimationController;
61
+
62
+ /**
63
+ * Promise that resolves when the last active animation is stopped (including
64
+ * when the animation finishes naturally).
65
+ *
66
+ * @remarks
67
+ * The Promise returned by this method is reset every time the animation
68
+ * enters a new start/stop cycle. This means you must call `start()` before
69
+ * calling this method if you want to wait for the animation to stop.
70
+ *
71
+ * This method always returns a resolved promise if the animation is currently
72
+ * in a stopped state.
73
+ *
74
+ * @returns
75
+ */
27
76
  waitUntilStopped(): Promise<void>;
77
+
78
+ /**
79
+ * Current state of the animation
80
+ *
81
+ * @remarks
82
+ * - `stopped` - The animation is currently stopped (at the beggining or end
83
+ * of the animation)
84
+ * - `running` - The animation is currently running
85
+ * - `paused` - The animation is currently paused
86
+ */
28
87
  readonly state: AnimationControllerState;
29
88
  }
@@ -339,6 +339,7 @@ export class CoreNode extends EventEmitter implements ICoreNode {
339
339
 
340
340
  private onTextureLoaded: TextureLoadedEventHandler = (target, dimensions) => {
341
341
  this.autosizeNode(dimensions);
342
+
342
343
  // Texture was loaded. In case the RAF loop has already stopped, we request
343
344
  // a render to ensure the texture is rendered.
344
345
  this.stage.requestRender();
@@ -353,6 +354,11 @@ export class CoreNode extends EventEmitter implements ICoreNode {
353
354
  type: 'texture',
354
355
  dimensions,
355
356
  } satisfies NodeTextureLoadedPayload);
357
+
358
+ // Trigger a local update if the texture is loaded and the resizeMode is 'contain'
359
+ if (this.props.textureOptions?.resizeMode?.type === 'contain') {
360
+ this.setUpdateType(UpdateType.Local);
361
+ }
356
362
  };
357
363
 
358
364
  private onTextureFailed: TextureFailedEventHandler = (target, error) => {
@@ -431,6 +437,45 @@ export class CoreNode extends EventEmitter implements ICoreNode {
431
437
  .multiply(this.scaleRotateTransform)
432
438
  .translate(-pivotTranslateX, -pivotTranslateY);
433
439
 
440
+ // Handle 'contain' resize mode
441
+ const { width, height } = this.props;
442
+ const texture = this.props.texture;
443
+ if (
444
+ texture &&
445
+ texture.dimensions &&
446
+ this.props.textureOptions?.resizeMode?.type === 'contain'
447
+ ) {
448
+ let resizeModeScaleX = 1;
449
+ let resizeModeScaleY = 1;
450
+ let extraX = 0;
451
+ let extraY = 0;
452
+ const { width: tw, height: th } = texture.dimensions;
453
+ const txAspectRatio = tw / th;
454
+ const nodeAspectRatio = width / height;
455
+ if (txAspectRatio > nodeAspectRatio) {
456
+ // Texture is wider than node
457
+ // Center the node vertically (shift down by extraY)
458
+ // Scale the node vertically to maintain original aspect ratio
459
+ const scaleX = width / tw;
460
+ const scaledTxHeight = th * scaleX;
461
+ extraY = (height - scaledTxHeight) / 2;
462
+ resizeModeScaleY = scaledTxHeight / height;
463
+ } else {
464
+ // Texture is taller than node (or equal)
465
+ // Center the node horizontally (shift right by extraX)
466
+ // Scale the node horizontally to maintain original aspect ratio
467
+ const scaleY = height / th;
468
+ const scaledTxWidth = tw * scaleY;
469
+ extraX = (width - scaledTxWidth) / 2;
470
+ resizeModeScaleX = scaledTxWidth / width;
471
+ }
472
+
473
+ // Apply the extra translation and scale to the local transform
474
+ this.localTransform
475
+ .translate(extraX, extraY)
476
+ .scale(resizeModeScaleX, resizeModeScaleY);
477
+ }
478
+
434
479
  this.setUpdateType(UpdateType.Global);
435
480
  }
436
481
 
@@ -57,6 +57,36 @@ export interface TextureManagerDebugInfo {
57
57
  idCacheSize: number;
58
58
  }
59
59
 
60
+ export type ResizeModeOptions =
61
+ | {
62
+ /**
63
+ * Specifies that the image should be resized to cover the specified dimensions.
64
+ */
65
+ type: 'cover';
66
+ /**
67
+ * The horizontal clipping position
68
+ * To clip the left, set clipX to 0. To clip the right, set clipX to 1.
69
+ * clipX 0.5 will clip a equal amount from left and right
70
+ *
71
+ * @defaultValue 0.5
72
+ */
73
+ clipX?: number;
74
+ /**
75
+ * The vertical clipping position
76
+ * To clip the top, set clipY to 0. To clip the bottom, set clipY to 1.
77
+ * clipY 0.5 will clip a equal amount from top and bottom
78
+ *
79
+ * @defaultValue 0.5
80
+ */
81
+ clipY?: number;
82
+ }
83
+ | {
84
+ /**
85
+ * Specifies that the image should be resized to fit within the specified dimensions.
86
+ */
87
+ type: 'contain';
88
+ };
89
+
60
90
  /**
61
91
  * Universal options for all texture types
62
92
  *
@@ -129,6 +159,15 @@ export interface TextureOptions {
129
159
  * @defaultValue `false`
130
160
  */
131
161
  flipY?: boolean;
162
+
163
+ /**
164
+ * You can use resizeMode to determine the clipping automatically from the width
165
+ * and height of the source texture. This can be convenient if you are unsure about
166
+ * the exact image sizes but want the image to cover a specific area.
167
+ *
168
+ * The resize modes cover and contain are supported
169
+ */
170
+ resizeMode?: ResizeModeOptions;
132
171
  }
133
172
 
134
173
  export class CoreTextureManager {
@@ -161,7 +200,7 @@ export class CoreTextureManager {
161
200
 
162
201
  constructor(numImageWorkers: number) {
163
202
  // Register default known texture types
164
- if (this.hasCreateImageBitmap && this.hasWorker) {
203
+ if (this.hasCreateImageBitmap && this.hasWorker && numImageWorkers > 0) {
165
204
  this.imageWorkerManager = new ImageWorkerManager(numImageWorkers);
166
205
  }
167
206
 
@@ -36,6 +36,7 @@ export interface AnimationSettings {
36
36
  export class CoreAnimation extends EventEmitter {
37
37
  public propStartValues: Partial<INodeAnimatableProps> = {};
38
38
  public restoreValues: Partial<INodeAnimatableProps> = {};
39
+ public settings: AnimationSettings;
39
40
  private progress = 0;
40
41
  private delayFor = 0;
41
42
  private timingFunction: (t: number) => number | undefined;
@@ -44,7 +45,7 @@ export class CoreAnimation extends EventEmitter {
44
45
  constructor(
45
46
  private node: CoreNode,
46
47
  private props: Partial<INodeAnimatableProps>,
47
- public settings: Partial<AnimationSettings>,
48
+ settings: Partial<AnimationSettings>,
48
49
  ) {
49
50
  super();
50
51
  this.propStartValues = {};
@@ -53,12 +54,19 @@ export class CoreAnimation extends EventEmitter {
53
54
  this.propStartValues[propName] = node[propName];
54
55
  });
55
56
 
56
- this.timingFunction = (t: number) => t;
57
-
58
- if (settings.easing && typeof settings.easing === 'string') {
59
- this.timingFunction = getTimingFunction(settings.easing);
60
- }
61
- this.delayFor = settings.delay || 0;
57
+ const easing = settings.easing || 'linear';
58
+ const delay = settings.delay ?? 0;
59
+ this.settings = {
60
+ duration: settings.duration ?? 0,
61
+ delay,
62
+ easing,
63
+ loop: settings.loop ?? false,
64
+ repeat: settings.repeat ?? 0,
65
+ repeatDelay: settings.repeatDelay ?? 0,
66
+ stopMethod: settings.stopMethod ?? false,
67
+ };
68
+ this.timingFunction = getTimingFunction(easing);
69
+ this.delayFor = delay;
62
70
  }
63
71
 
64
72
  reset() {
@@ -96,24 +104,40 @@ export class CoreAnimation extends EventEmitter {
96
104
  }
97
105
  }
98
106
 
99
- applyEasing(p: number, s: number, e: number): number {
107
+ private applyEasing(p: number, s: number, e: number): number {
100
108
  return (this.timingFunction(p) || p) * (e - s) + s;
101
109
  }
102
110
 
103
111
  update(dt: number) {
104
112
  const { duration, loop, easing, stopMethod } = this.settings;
105
- if (!duration) {
113
+ const { delayFor } = this;
114
+ if (duration === 0 && delayFor === 0) {
106
115
  this.emit('finished', {});
107
116
  return;
108
117
  }
109
118
 
110
119
  if (this.delayFor > 0) {
111
120
  this.delayFor -= dt;
121
+ if (this.delayFor >= 0) {
122
+ // Either no or more delay left. Exit.
123
+ return;
124
+ } else {
125
+ // We went beyond the delay time, add it back to dt so we can continue
126
+ // with the animation.
127
+ dt = -this.delayFor;
128
+ this.delayFor = 0;
129
+ }
130
+ }
131
+
132
+ if (duration === 0) {
133
+ // No duration, we are done.
134
+ this.emit('finished', {});
112
135
  return;
113
136
  }
114
137
 
115
- if (this.delayFor <= 0 && this.progress === 0) {
116
- this.emit('start', {});
138
+ if (this.progress === 0) {
139
+ // Progress is 0, we are starting the post-delay part of the animation.
140
+ this.emit('animating', {});
117
141
  }
118
142
 
119
143
  this.progress += dt / duration;
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/unbound-method */
1
2
  /*
2
3
  * If not stated otherwise in this file or this component's LICENSE file the
3
4
  * following copyright and licenses apply:
@@ -24,50 +25,48 @@ import type {
24
25
  import type { AnimationManager } from './AnimationManager.js';
25
26
  import type { CoreAnimation } from './CoreAnimation.js';
26
27
  import { assertTruthy } from '../../utils.js';
28
+ import { EventEmitter } from '../../common/EventEmitter.js';
27
29
 
28
- export class CoreAnimationController implements IAnimationController {
29
- startedPromise: Promise<void> | null = null;
30
- /**
31
- * If this is null, then the animation hasn't started yet.
32
- */
33
- startedResolve: ((scope?: any) => void) | null = null;
34
-
35
- stoppedPromise: Promise<void> | null = null;
30
+ export class CoreAnimationController
31
+ extends EventEmitter
32
+ implements IAnimationController
33
+ {
34
+ stoppedPromise: Promise<void>;
36
35
  /**
37
36
  * If this is null, then the animation is in a finished / stopped state.
38
37
  */
39
38
  stoppedResolve: (() => void) | null = null;
39
+ state: AnimationControllerState;
40
40
 
41
41
  constructor(
42
42
  private manager: AnimationManager,
43
43
  private animation: CoreAnimation,
44
44
  ) {
45
+ super();
45
46
  this.state = 'stopped';
46
- }
47
+ // Initial stopped promise is resolved (since the animation is stopped)
48
+ this.stoppedPromise = Promise.resolve();
47
49
 
48
- state: AnimationControllerState;
50
+ // Bind event handlers
51
+ this.onAnimating = this.onAnimating.bind(this);
52
+ this.onFinished = this.onFinished.bind(this);
53
+ }
49
54
 
50
55
  start(): IAnimationController {
51
- this.makeStartedPromise();
52
- this.animation.once('start', this.started.bind(this));
53
-
54
- this.makeStoppedPromise();
55
- this.animation.once('finished', this.finished.bind(this));
56
-
57
- // prevent registering the same animation twice
58
- if (!this.manager.activeAnimations.has(this.animation)) {
59
- this.manager.registerAnimation(this.animation);
56
+ if (this.state !== 'running') {
57
+ this.makeStoppedPromise();
58
+ this.registerAnimation();
59
+ this.state = 'running';
60
60
  }
61
-
62
- this.state = 'running';
63
61
  return this;
64
62
  }
65
63
 
66
64
  stop(): IAnimationController {
67
- this.manager.unregisterAnimation(this.animation);
65
+ this.unregisterAnimation();
68
66
  if (this.stoppedResolve !== null) {
69
67
  this.stoppedResolve();
70
68
  this.stoppedResolve = null;
69
+ this.emit('stopped', this);
71
70
  }
72
71
  this.animation.reset();
73
72
  this.state = 'stopped';
@@ -75,7 +74,7 @@ export class CoreAnimationController implements IAnimationController {
75
74
  }
76
75
 
77
76
  pause(): IAnimationController {
78
- this.manager.unregisterAnimation(this.animation);
77
+ this.unregisterAnimation();
79
78
  this.state = 'paused';
80
79
  return this;
81
80
  }
@@ -86,26 +85,24 @@ export class CoreAnimationController implements IAnimationController {
86
85
  return this;
87
86
  }
88
87
 
89
- waitUntilStarted(): Promise<void> {
90
- this.makeStartedPromise();
91
- const promise = this.startedPromise;
92
- assertTruthy(promise);
93
- return promise;
88
+ waitUntilStopped(): Promise<void> {
89
+ return this.stoppedPromise;
94
90
  }
95
91
 
96
- waitUntilStopped(): Promise<void> {
97
- this.makeStoppedPromise();
98
- const promise = this.stoppedPromise;
99
- assertTruthy(promise);
100
- return promise;
92
+ private registerAnimation(): void {
93
+ // Hook up event listeners
94
+ this.animation.once('finished', this.onFinished);
95
+ this.animation.on('animating', this.onAnimating);
96
+ // Then register the animation
97
+ this.manager.registerAnimation(this.animation);
101
98
  }
102
99
 
103
- private makeStartedPromise(): void {
104
- if (this.startedResolve === null) {
105
- this.startedPromise = new Promise((resolve) => {
106
- this.startedResolve = resolve;
107
- });
108
- }
100
+ private unregisterAnimation(): void {
101
+ // First unregister the animation
102
+ this.manager.unregisterAnimation(this.animation);
103
+ // Then unhook event listeners
104
+ this.animation.off('finished', this.onFinished);
105
+ this.animation.off('animating', this.onAnimating);
109
106
  }
110
107
 
111
108
  private makeStoppedPromise(): void {
@@ -116,33 +113,31 @@ export class CoreAnimationController implements IAnimationController {
116
113
  }
117
114
  }
118
115
 
119
- private started(): void {
120
- assertTruthy(this.startedResolve);
121
- // resolve promise (and pass current this to continue to the chain)
122
- this.startedResolve(this);
123
- this.startedResolve = null;
124
- }
125
-
126
- private finished(): void {
116
+ private onFinished(this: CoreAnimationController): void {
127
117
  assertTruthy(this.stoppedResolve);
128
118
  // If the animation is looping, then we need to restart it.
129
119
  const { loop, stopMethod } = this.animation.settings;
130
120
 
131
121
  if (stopMethod === 'reverse') {
132
122
  this.animation.reverse();
133
- this.start();
134
123
  return;
135
124
  }
136
125
 
137
- // resolve promise
138
- this.stoppedResolve();
139
- this.stoppedResolve = null;
140
-
141
126
  if (loop) {
142
127
  return;
143
128
  }
144
129
 
145
130
  // unregister animation
146
- this.manager.unregisterAnimation(this.animation);
131
+ this.unregisterAnimation();
132
+
133
+ // resolve promise
134
+ this.stoppedResolve();
135
+ this.stoppedResolve = null;
136
+ this.emit('stopped', this);
137
+ this.state = 'stopped';
138
+ }
139
+
140
+ private onAnimating(this: CoreAnimationController): void {
141
+ this.emit('animating', this);
147
142
  }
148
143
  }
@@ -119,15 +119,10 @@ export class ImageWorkerManager {
119
119
  return workers;
120
120
  }
121
121
 
122
- private getNextWorker(): Worker {
122
+ private getNextWorker(): Worker | undefined {
123
123
  const worker = this.workers[this.workerIndex];
124
124
  this.workerIndex = (this.workerIndex + 1) % this.workers.length;
125
- return worker!;
126
- }
127
-
128
- private convertUrlToAbsolute(url: string): string {
129
- const absoluteUrl = new URL(url, self.location.href);
130
- return absoluteUrl.href;
125
+ return worker;
131
126
  }
132
127
 
133
128
  getImage(
@@ -137,14 +132,16 @@ export class ImageWorkerManager {
137
132
  return new Promise((resolve, reject) => {
138
133
  try {
139
134
  if (this.workers) {
140
- const absoluteSrcUrl = this.convertUrlToAbsolute(src);
141
135
  const id = this.nextId++;
142
136
  this.messageManager[id] = [resolve, reject];
143
- this.getNextWorker().postMessage({
144
- id,
145
- src: absoluteSrcUrl,
146
- premultiplyAlpha,
147
- });
137
+ const nextWorker = this.getNextWorker();
138
+ if (nextWorker) {
139
+ nextWorker.postMessage({
140
+ id,
141
+ src: src,
142
+ premultiplyAlpha,
143
+ });
144
+ }
148
145
  }
149
146
  } catch (error) {
150
147
  reject(error);
@@ -248,3 +248,28 @@ export function isBoundPositive(bound: Bound): boolean {
248
248
  export function isRectPositive(rect: Rect): boolean {
249
249
  return rect.width > 0 && rect.height > 0;
250
250
  }
251
+
252
+ export function convertUrlToAbsolute(url: string): string {
253
+ // handle local file imports
254
+ if (self.location.protocol === 'file:') {
255
+ const path = self.location.pathname.split('/');
256
+ path.pop();
257
+ const basePath = path.join('/');
258
+ const baseUrl = self.location.protocol + '//' + basePath;
259
+
260
+ // check if url has a leading dot
261
+ if (url.charAt(0) === '.') {
262
+ url = url.slice(1);
263
+ }
264
+
265
+ // check if url has a leading slash
266
+ if (url.charAt(0) === '/') {
267
+ url = url.slice(1);
268
+ }
269
+
270
+ return baseUrl + '/' + url;
271
+ }
272
+
273
+ const absoluteUrl = new URL(url, self.location.href);
274
+ return absoluteUrl.href;
275
+ }
@@ -50,6 +50,7 @@ import { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js';
50
50
  import { RenderTexture } from '../../textures/RenderTexture.js';
51
51
  import type { CoreNode } from '../../CoreNode.js';
52
52
  import { WebGlCoreCtxRenderTexture } from './WebGlCoreCtxRenderTexture.js';
53
+ import { ImageTexture } from '../../textures/ImageTexture.js';
53
54
 
54
55
  const WORDS_PER_QUAD = 24;
55
56
  // const BYTES_PER_QUAD = WORDS_PER_QUAD * 4;
@@ -298,6 +299,32 @@ export class WebGlCoreRenderer extends CoreRenderer {
298
299
  texture = texture.parentTexture;
299
300
  }
300
301
 
302
+ const resizeMode = textureOptions?.resizeMode ?? false;
303
+
304
+ if (texture instanceof ImageTexture) {
305
+ if (resizeMode && texture.dimensions) {
306
+ const { width: tw, height: th } = texture.dimensions;
307
+ if (resizeMode.type === 'cover') {
308
+ const scaleX = width / tw;
309
+ const scaleY = height / th;
310
+ const scale = Math.max(scaleX, scaleY);
311
+ const precision = 1 / scale;
312
+ // Determine based on width
313
+ if (scale && scaleX && scaleX < scale) {
314
+ const desiredSize = precision * width;
315
+ texCoordX1 = (1 - desiredSize / tw) * (resizeMode.clipX ?? 0.5);
316
+ texCoordX2 = texCoordX1 + desiredSize / tw;
317
+ }
318
+ // Determine based on height
319
+ if (scale && scaleY && scaleY < scale) {
320
+ const desiredSize = precision * height;
321
+ texCoordY1 = (1 - desiredSize / th) * (resizeMode.clipY ?? 0.5);
322
+ texCoordY2 = texCoordY1 + desiredSize / th;
323
+ }
324
+ }
325
+ }
326
+ }
327
+
301
328
  // Flip texture coordinates if dictated by texture options
302
329
  if (flipX) {
303
330
  [texCoordX1, texCoordX2] = [texCoordX2, texCoordX1];
@@ -76,7 +76,8 @@ export class BorderEffect extends ShaderEffect {
76
76
  };
77
77
 
78
78
  static override onEffectMask = `
79
- float mask = clamp(shaderMask + width, 0.0, 1.0) - clamp(shaderMask, 0.0, 1.0);
79
+ float intR = shaderMask + 1.0;
80
+ float mask = clamp(intR + width, 0.0, 1.0) - clamp(intR, 0.0, 1.0);
80
81
  return mix(shaderColor, mix(shaderColor, maskColor, maskColor.a), mask);
81
82
  `;
82
83
 
@@ -23,6 +23,7 @@ import {
23
23
  isCompressedTextureContainer,
24
24
  loadCompressedTexture,
25
25
  } from '../lib/textureCompression.js';
26
+ import { convertUrlToAbsolute } from '../lib/utils.js';
26
27
 
27
28
  /**
28
29
  * Properties of the {@link ImageTexture}
@@ -37,7 +38,7 @@ export interface ImageTextureProps {
37
38
  *
38
39
  * @default ''
39
40
  */
40
- src?: string | ImageData;
41
+ src?: string | ImageData | (() => ImageData);
41
42
  /**
42
43
  * Whether to premultiply the alpha channel into the color channels of the
43
44
  * image.
@@ -50,6 +51,10 @@ export interface ImageTextureProps {
50
51
  * @default true
51
52
  */
52
53
  premultiplyAlpha?: boolean | null;
54
+ /**
55
+ * `ImageData` textures are not cached unless a `key` is provided
56
+ */
57
+ key?: string | null;
53
58
  }
54
59
 
55
60
  /**
@@ -85,9 +90,16 @@ export class ImageTexture extends Texture {
85
90
  data: null,
86
91
  };
87
92
  }
88
- if (src instanceof ImageData) {
93
+
94
+ if (typeof src !== 'string') {
95
+ if (src instanceof ImageData) {
96
+ return {
97
+ data: src,
98
+ premultiplyAlpha,
99
+ };
100
+ }
89
101
  return {
90
- data: src,
102
+ data: src(),
91
103
  premultiplyAlpha,
92
104
  };
93
105
  }
@@ -97,13 +109,16 @@ export class ImageTexture extends Texture {
97
109
  return loadCompressedTexture(src);
98
110
  }
99
111
 
112
+ // Convert relative URL to absolute URL
113
+ const absoluteSrc = convertUrlToAbsolute(src);
114
+
100
115
  if (this.txManager.imageWorkerManager) {
101
116
  return await this.txManager.imageWorkerManager.getImage(
102
- src,
117
+ absoluteSrc,
103
118
  premultiplyAlpha,
104
119
  );
105
120
  } else if (this.txManager.hasCreateImageBitmap) {
106
- const response = await fetch(src);
121
+ const response = await fetch(absoluteSrc);
107
122
  const blob = await response.blob();
108
123
  const hasAlphaChannel =
109
124
  premultiplyAlpha ?? this.hasAlphaChannel(blob.type);
@@ -120,7 +135,7 @@ export class ImageTexture extends Texture {
120
135
  if (!(src.substr(0, 5) === 'data:')) {
121
136
  img.crossOrigin = 'Anonymous';
122
137
  }
123
- img.src = src;
138
+ img.src = absoluteSrc;
124
139
  await new Promise<void>((resolve, reject) => {
125
140
  img.onload = () => resolve();
126
141
  img.onerror = () => reject(new Error(`Failed to load image`));
@@ -137,11 +152,12 @@ export class ImageTexture extends Texture {
137
152
 
138
153
  static override makeCacheKey(props: ImageTextureProps): string | false {
139
154
  const resolvedProps = ImageTexture.resolveDefaults(props);
140
- // ImageTextures sourced by ImageData are non-cacheable
141
- if (resolvedProps.src instanceof ImageData) {
155
+ // Only cache key-able textures; prioritise key
156
+ const key = resolvedProps.key || resolvedProps.src;
157
+ if (typeof key !== 'string') {
142
158
  return false;
143
159
  }
144
- return `ImageTexture,${resolvedProps.src},${resolvedProps.premultiplyAlpha}`;
160
+ return `ImageTexture,${key},${resolvedProps.premultiplyAlpha ?? 'true'}`;
145
161
  }
146
162
 
147
163
  static override resolveDefaults(
@@ -150,6 +166,7 @@ export class ImageTexture extends Texture {
150
166
  return {
151
167
  src: props.src ?? '',
152
168
  premultiplyAlpha: props.premultiplyAlpha ?? true, // null,
169
+ key: props.key ?? null,
153
170
  };
154
171
  }
155
172