@solidtv/renderer 1.5.4 → 1.5.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 (65) hide show
  1. package/dist/exports/canvas-shaders.d.ts +1 -0
  2. package/dist/exports/canvas-shaders.js +1 -0
  3. package/dist/exports/canvas-shaders.js.map +1 -1
  4. package/dist/exports/index.d.ts +1 -0
  5. package/dist/exports/index.js +1 -0
  6. package/dist/exports/index.js.map +1 -1
  7. package/dist/exports/webgl-shaders.d.ts +1 -0
  8. package/dist/exports/webgl-shaders.js +1 -0
  9. package/dist/exports/webgl-shaders.js.map +1 -1
  10. package/dist/src/core/CoreShaderManager.js +16 -2
  11. package/dist/src/core/CoreShaderManager.js.map +1 -1
  12. package/dist/src/core/lib/WebGlContextWrapper.d.ts +13 -0
  13. package/dist/src/core/lib/WebGlContextWrapper.js +15 -0
  14. package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
  15. package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +10 -0
  16. package/dist/src/core/renderers/canvas/CanvasRenderer.js +19 -0
  17. package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
  18. package/dist/src/core/renderers/webgl/WebGlRenderer.js +14 -1
  19. package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
  20. package/dist/src/core/shaders/canvas/EdgeFade.d.ts +10 -0
  21. package/dist/src/core/shaders/canvas/EdgeFade.js +92 -0
  22. package/dist/src/core/shaders/canvas/EdgeFade.js.map +1 -0
  23. package/dist/src/core/shaders/canvas/LinearGradient.js +1 -18
  24. package/dist/src/core/shaders/canvas/LinearGradient.js.map +1 -1
  25. package/dist/src/core/shaders/templates/EdgeFadeTemplate.d.ts +32 -0
  26. package/dist/src/core/shaders/templates/EdgeFadeTemplate.js +9 -0
  27. package/dist/src/core/shaders/templates/EdgeFadeTemplate.js.map +1 -0
  28. package/dist/src/core/shaders/webgl/EdgeFade.d.ts +9 -0
  29. package/dist/src/core/shaders/webgl/EdgeFade.js +74 -0
  30. package/dist/src/core/shaders/webgl/EdgeFade.js.map +1 -0
  31. package/dist/src/core/shaders/webgl/LinearGradient.js +31 -51
  32. package/dist/src/core/shaders/webgl/LinearGradient.js.map +1 -1
  33. package/dist/src/core/shaders/webgl/RadialGradient.js +2 -2
  34. package/dist/src/core/shaders/webgl/RadialProgress.js +7 -0
  35. package/dist/src/core/shaders/webgl/RadialProgress.js.map +1 -1
  36. package/dist/src/core/text-rendering/SdfFontHandler.d.ts +1 -0
  37. package/dist/src/core/text-rendering/SdfFontHandler.js +88 -51
  38. package/dist/src/core/text-rendering/SdfFontHandler.js.map +1 -1
  39. package/dist/src/core/textures/ImageTexture.js +2 -1
  40. package/dist/src/core/textures/ImageTexture.js.map +1 -1
  41. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  42. package/exports/canvas-shaders.ts +1 -0
  43. package/exports/index.ts +1 -0
  44. package/exports/webgl-shaders.ts +1 -0
  45. package/package.json +1 -1
  46. package/src/core/CoreShaderManager.contextLoss.test.ts +72 -0
  47. package/src/core/CoreShaderManager.ts +15 -2
  48. package/src/core/lib/WebGlContextWrapper.ts +16 -0
  49. package/src/core/renderers/canvas/CanvasRenderer.ts +20 -0
  50. package/src/core/renderers/webgl/WebGlRenderer.contextLoss.test.ts +49 -0
  51. package/src/core/renderers/webgl/WebGlRenderer.ts +13 -1
  52. package/src/core/shaders/canvas/EdgeFade.ts +105 -0
  53. package/src/core/shaders/canvas/LinearGradient.test.ts +45 -0
  54. package/src/core/shaders/canvas/LinearGradient.ts +1 -18
  55. package/src/core/shaders/templates/EdgeFadeTemplate.test.ts +35 -0
  56. package/src/core/shaders/templates/EdgeFadeTemplate.ts +41 -0
  57. package/src/core/shaders/webgl/EdgeFade.ts +84 -0
  58. package/src/core/shaders/webgl/LinearGradient.test.ts +70 -0
  59. package/src/core/shaders/webgl/LinearGradient.ts +36 -51
  60. package/src/core/shaders/webgl/RadialGradient.ts +2 -2
  61. package/src/core/shaders/webgl/RadialProgress.ts +7 -0
  62. package/src/core/text-rendering/SdfFontHandler.ts +77 -30
  63. package/src/core/text-rendering/tests/SdfFontHandler.test.ts +181 -35
  64. package/src/core/textures/ImageTexture.test.ts +91 -0
  65. package/src/core/textures/ImageTexture.ts +4 -1
@@ -109,6 +109,12 @@ export interface SdfFont {
109
109
  maxCharHeight: number;
110
110
  }
111
111
 
112
+ // Number of times a failed font load is automatically retried before
113
+ // loadFont() finally rejects. Counts reloads *after* the initial attempt
114
+ // (matching the `maxRetryCount` = retries convention used for textures), so
115
+ // the load is attempted up to MAX_FONT_LOAD_RETRIES + 1 times in total.
116
+ export const MAX_FONT_LOAD_RETRIES = 3;
117
+
112
118
  //global state variables for SdfFontHandler
113
119
  const fontCache = new Map<string, SdfFont>();
114
120
  const fontLoadPromises = new Map<string, Promise<void>>();
@@ -320,10 +326,18 @@ export const loadFont = (
320
326
  );
321
327
  }
322
328
 
323
- const nwff: CoreTextNode[] = (nodesWaitingForFont[fontFamily] = []);
324
- // Create loading promise
325
- const loadPromise = (async (): Promise<void> => {
326
- const fontData = await new Promise<SdfFontData>((resolve, reject) => {
329
+ // Reuse an existing waiter list. A previous load attempt for this font may
330
+ // have failed and left nodes parked here; overwriting the list would strand
331
+ // them, so a successful retry could never wake them. The list is consumed
332
+ // (and deleted) on the next successful load.
333
+ let nwff = nodesWaitingForFont[fontFamily];
334
+ if (nwff === undefined) {
335
+ nwff = nodesWaitingForFont[fontFamily] = [];
336
+ }
337
+ // One attempt at fetching + decoding the JSON atlas description. A fresh
338
+ // XHR runs per attempt so a transient network/parse failure can recover.
339
+ const fetchFontData = (): Promise<SdfFontData> =>
340
+ new Promise<SdfFontData>((resolve, reject) => {
327
341
  const xhr = new XMLHttpRequest();
328
342
  xhr.open('GET', atlasDataUrl, true);
329
343
  xhr.responseType = 'json';
@@ -352,16 +366,23 @@ export const loadFont = (
352
366
  };
353
367
  xhr.send(null);
354
368
  });
369
+
370
+ // One attempt at loading the atlas texture for the given font data. On
371
+ // success it processes + caches the font and wakes parked nodes. On failure
372
+ // it drops the dead atlas texture (createTexture caches by `src`, so the
373
+ // next attempt must evict it to build a fresh one) and rejects.
374
+ const loadAtlas = (fontData: SdfFontData): Promise<void> => {
355
375
  if (!fontData || !fontData.chars) {
356
- throw new Error('Invalid SDF font data format');
376
+ return Promise.reject(new Error('Invalid SDF font data format'));
357
377
  }
358
378
 
359
379
  // Atlas texture should be provided externally
360
380
  if (!atlasUrl) {
361
- throw new Error('Atlas texture must be provided for SDF fonts');
381
+ return Promise.reject(
382
+ new Error('Atlas texture must be provided for SDF fonts'),
383
+ );
362
384
  }
363
385
 
364
- // Wait for atlas texture to load
365
386
  return new Promise<void>((resolve, reject) => {
366
387
  // create new atlas texture using ImageTexture
367
388
  const atlasTexture = stage.txManager.createTexture('ImageTexture', {
@@ -372,46 +393,72 @@ export const loadFont = (
372
393
  atlasTexture.setRenderableOwner(fontFamily, true);
373
394
  atlasTexture.preventCleanup = true; // Prevent automatic cleanup
374
395
 
375
- if (atlasTexture.state === 'loaded') {
376
- // If already loaded, process immediately
377
- processFontData(fontFamily, fontData, atlasTexture, metrics);
378
- fontLoadPromises.delete(fontFamily);
379
-
380
- for (let key in nwff) {
381
- nwff[key]!.setUpdateType(UpdateType.Local);
382
- }
383
- delete nodesWaitingForFont[fontFamily];
384
- return resolve();
385
- }
386
-
387
- atlasTexture.on('loaded', () => {
396
+ const onLoaded = () => {
388
397
  // Process and cache font data
389
398
  processFontData(fontFamily, fontData, atlasTexture, metrics);
390
399
 
391
- // remove from promises
392
- fontLoadPromises.delete(fontFamily);
393
-
394
400
  for (let key in nwff) {
395
401
  nwff[key]!.setUpdateType(UpdateType.Local);
396
402
  }
397
403
  delete nodesWaitingForFont[fontFamily];
398
404
  resolve();
399
- });
405
+ };
406
+
407
+ if (atlasTexture.state === 'loaded') {
408
+ // If already loaded, process immediately
409
+ onLoaded();
410
+ return;
411
+ }
412
+
413
+ atlasTexture.on('loaded', onLoaded);
400
414
 
401
415
  // EventEmitter invokes listeners as (target, data), so the error payload
402
416
  // is the SECOND argument. The first arg is the Texture that emitted the
403
417
  // event. Reading it as the only param (the previous behavior) rejected
404
418
  // and logged the Texture instead of the actual TextureError.
405
419
  atlasTexture.on('failed', (_target, error: TextureError) => {
406
- // Cleanup on error
407
- fontLoadPromises.delete(fontFamily);
408
- if (fontCache[fontFamily]) {
409
- delete fontCache[fontFamily];
410
- }
411
- console.error(`Failed to load SDF font: ${fontFamily}`, error);
420
+ // Drop the failed atlas so a retry builds a fresh texture rather than
421
+ // getting this dead instance back from the createTexture key-cache.
422
+ atlasTexture.setRenderableOwner(fontFamily, false);
423
+ stage.txManager.removeTextureFromCache(atlasTexture);
412
424
  reject(error);
413
425
  });
414
426
  });
427
+ };
428
+
429
+ // Initial attempt plus up to MAX_FONT_LOAD_RETRIES automatic reloads.
430
+ const loadPromise = (async (): Promise<void> => {
431
+ let lastError: unknown;
432
+ for (let attempt = 0; attempt <= MAX_FONT_LOAD_RETRIES; attempt++) {
433
+ try {
434
+ await loadAtlas(await fetchFontData());
435
+ // Success: clear the in-flight marker (the font now lives in fontCache)
436
+ // — parked nodes were already woken inside loadAtlas.
437
+ fontLoadPromises.delete(fontFamily);
438
+ return;
439
+ } catch (error) {
440
+ lastError = error;
441
+ if (attempt < MAX_FONT_LOAD_RETRIES) {
442
+ console.warn(
443
+ `SDF font "${fontFamily}" failed to load (attempt ${
444
+ attempt + 1
445
+ } of ${MAX_FONT_LOAD_RETRIES + 1}), retrying.`,
446
+ error,
447
+ );
448
+ }
449
+ }
450
+ }
451
+
452
+ // Every attempt failed. Clear the in-flight marker so the font can be
453
+ // requested again and drop any partial cache entry. nodesWaitingForFont
454
+ // is deliberately kept: nodes parked here must survive so a later
455
+ // loadFont() (which reuses the list) can still wake them if the font
456
+ // eventually loads. The list shrinks as nodes self-remove via
457
+ // stopWaitingForFont on destroy.
458
+ fontLoadPromises.delete(fontFamily);
459
+ fontCache.delete(fontFamily);
460
+ console.error(`Failed to load SDF font: ${fontFamily}`, lastError);
461
+ throw lastError;
415
462
  })();
416
463
 
417
464
  fontLoadPromises.set(fontFamily, loadPromise);
@@ -1,5 +1,10 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { loadFont } from '../SdfFontHandler.js';
2
+ import {
3
+ loadFont,
4
+ waitingForFont,
5
+ isFontLoaded,
6
+ MAX_FONT_LOAD_RETRIES,
7
+ } from '../SdfFontHandler.js';
3
8
  import { EventEmitter } from '../../../common/EventEmitter.js';
4
9
  import { TextureError, TextureErrorCode } from '../../TextureError.js';
5
10
  import type { Stage } from '../../Stage.js';
@@ -14,7 +19,19 @@ class FakeXHR {
14
19
  onerror: (() => void) | null = null;
15
20
  open(): void {}
16
21
  send(): void {
17
- this.response = { chars: [{}] };
22
+ // Enough shape for processFontData to run on the success path without
23
+ // throwing: a chars array, an (empty) kernings array, and metrics so it
24
+ // skips the atlas-derived cap/x-height branches.
25
+ this.response = {
26
+ chars: [{}],
27
+ kernings: [],
28
+ lightningMetrics: {
29
+ ascender: 800,
30
+ descender: -200,
31
+ lineGap: 200,
32
+ unitsPerEm: 1000,
33
+ },
34
+ };
18
35
  if (this.onload !== null) {
19
36
  this.onload();
20
37
  }
@@ -34,15 +51,59 @@ function makeFakeTexture() {
34
51
  return tex;
35
52
  }
36
53
 
54
+ type FakeTexture = ReturnType<typeof makeFakeTexture>;
55
+
56
+ // A stage whose txManager hands out a fresh fake texture per loadFont attempt
57
+ // (so each retry has its own texture to fail/succeed) and records them.
58
+ function makeStage(): { stage: Stage; textures: FakeTexture[] } {
59
+ const textures: FakeTexture[] = [];
60
+ const stage = {
61
+ txManager: {
62
+ createTexture: () => {
63
+ const t = makeFakeTexture();
64
+ textures.push(t);
65
+ return t;
66
+ },
67
+ // loadFont evicts a failed atlas before retrying; a no-op is enough here.
68
+ removeTextureFromCache: () => {},
69
+ },
70
+ } as unknown as Stage;
71
+ return { stage, textures };
72
+ }
73
+
74
+ const opts = (fontFamily: string) =>
75
+ ({
76
+ fontFamily,
77
+ atlasUrl: 'atlas.png',
78
+ atlasDataUrl: 'atlas.json',
79
+ } as Parameters<typeof loadFont>[1]);
80
+
37
81
  // Drain microtasks + one macrotask so the async loader reaches listener setup.
38
82
  const flush = (): Promise<void> => new Promise((r) => setTimeout(r, 0));
39
83
 
40
- describe('SdfFontHandler loadFont failed event argument', () => {
84
+ // Fail every attempt: each iteration waits for the next attempt to wire up its
85
+ // atlas listener, then fires 'failed' on that attempt's texture.
86
+ async function failAllAttempts(
87
+ textures: FakeTexture[],
88
+ error: TextureError,
89
+ attempts: number,
90
+ ): Promise<void> {
91
+ for (let i = 0; i < attempts; i++) {
92
+ await flush();
93
+ textures[i]!.emit('failed', error);
94
+ }
95
+ }
96
+
97
+ const TOTAL_ATTEMPTS = MAX_FONT_LOAD_RETRIES + 1; // initial + retries
98
+
99
+ describe('SdfFontHandler loadFont — failure after exhausting retries', () => {
41
100
  let errSpy: ReturnType<typeof vi.spyOn>;
101
+ let warnSpy: ReturnType<typeof vi.spyOn>;
42
102
  let originalXHR: unknown;
43
103
 
44
104
  beforeEach(() => {
45
105
  errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
106
+ warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
46
107
  originalXHR = (globalThis as unknown as { XMLHttpRequest: unknown })
47
108
  .XMLHttpRequest;
48
109
  (globalThis as unknown as { XMLHttpRequest: unknown }).XMLHttpRequest =
@@ -51,62 +112,147 @@ describe('SdfFontHandler loadFont — failed event argument', () => {
51
112
 
52
113
  afterEach(() => {
53
114
  errSpy.mockRestore();
115
+ warnSpy.mockRestore();
54
116
  (globalThis as unknown as { XMLHttpRequest: unknown }).XMLHttpRequest =
55
117
  originalXHR;
56
118
  });
57
119
 
58
- it('rejects with the TextureError (second emit arg), not the emitting texture', async () => {
59
- const tex = makeFakeTexture();
60
- const stage = {
61
- txManager: { createTexture: () => tex },
62
- } as unknown as Stage;
120
+ it('rejects with the last TextureError (second emit arg), not the emitting texture', async () => {
121
+ const { stage, textures } = makeStage();
122
+ const promise = loadFont(stage, opts('TestSdfFailFont'));
63
123
 
64
- const promise = loadFont(stage, {
65
- fontFamily: 'TestSdfFailFont',
66
- atlasUrl: 'atlas.png',
67
- atlasDataUrl: 'atlas.json',
68
- } as Parameters<typeof loadFont>[1]);
124
+ const error = new TextureError(
125
+ TextureErrorCode.TEXTURE_UPLOAD_FAILED,
126
+ 'boom',
127
+ );
69
128
 
70
- await flush();
129
+ // Attach the rejection assertion before driving failures.
130
+ const assertion = expect(promise).rejects.toBe(error);
131
+ await failAllAttempts(textures, error, TOTAL_ATTEMPTS);
132
+ await assertion;
133
+ });
134
+
135
+ it('logs the error, not the texture, after the final attempt', async () => {
136
+ const { stage, textures } = makeStage();
137
+ const promise = loadFont(stage, opts('TestSdfFailFontLog'));
71
138
 
72
139
  const error = new TextureError(
73
140
  TextureErrorCode.TEXTURE_UPLOAD_FAILED,
74
141
  'boom',
75
142
  );
143
+ const rejected = promise.catch(() => {});
144
+ await failAllAttempts(textures, error, TOTAL_ATTEMPTS);
145
+ await rejected;
76
146
 
77
- // Attach the rejection assertion before emitting so the handler is ready.
78
- const assertion = expect(promise).rejects.toBe(error);
147
+ const lastCall = errSpy.mock.calls[errSpy.mock.calls.length - 1]!;
148
+ expect(lastCall[1]).toBe(error);
149
+ expect(lastCall[1]).not.toBe(textures[textures.length - 1]);
150
+ });
79
151
 
80
- // EventEmitter calls listeners as (target, data) -> (tex, error).
81
- tex.emit('failed', error);
152
+ it('attempts the load exactly initial + MAX_FONT_LOAD_RETRIES times', async () => {
153
+ const { stage, textures } = makeStage();
154
+ const promise = loadFont(stage, opts('TestSdfAttemptCount'));
82
155
 
83
- await assertion;
156
+ const error = new TextureError(
157
+ TextureErrorCode.TEXTURE_UPLOAD_FAILED,
158
+ 'boom',
159
+ );
160
+ const rejected = promise.catch(() => {});
161
+ await failAllAttempts(textures, error, TOTAL_ATTEMPTS);
162
+ await rejected;
163
+
164
+ // One texture is created per attempt; no further attempts after exhaustion.
165
+ expect(textures.length).toBe(TOTAL_ATTEMPTS);
166
+ // A warning per retried attempt, error only on the final failure.
167
+ expect(warnSpy).toHaveBeenCalledTimes(MAX_FONT_LOAD_RETRIES);
168
+ expect(errSpy).toHaveBeenCalledTimes(1);
169
+ });
170
+ });
171
+
172
+ describe('SdfFontHandler loadFont — automatic retry', () => {
173
+ let errSpy: ReturnType<typeof vi.spyOn>;
174
+ let warnSpy: ReturnType<typeof vi.spyOn>;
175
+ let originalXHR: unknown;
176
+
177
+ beforeEach(() => {
178
+ errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
179
+ warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
180
+ originalXHR = (globalThis as unknown as { XMLHttpRequest: unknown })
181
+ .XMLHttpRequest;
182
+ (globalThis as unknown as { XMLHttpRequest: unknown }).XMLHttpRequest =
183
+ FakeXHR;
184
+ });
185
+
186
+ afterEach(() => {
187
+ errSpy.mockRestore();
188
+ warnSpy.mockRestore();
189
+ (globalThis as unknown as { XMLHttpRequest: unknown }).XMLHttpRequest =
190
+ originalXHR;
84
191
  });
85
192
 
86
- it('logs the error, not the texture, on failure', async () => {
87
- const tex = makeFakeTexture();
88
- const stage = {
89
- txManager: { createTexture: () => tex },
90
- } as unknown as Stage;
193
+ it('recovers when a retry succeeds and wakes nodes parked before the failure', async () => {
194
+ const { stage, textures } = makeStage();
195
+ const fontFamily = 'RetryRecoverFont';
196
+
197
+ const promise = loadFont(stage, opts(fontFamily));
91
198
 
92
- const promise = loadFont(stage, {
93
- fontFamily: 'TestSdfFailFontLog',
94
- atlasUrl: 'atlas.png',
95
- atlasDataUrl: 'atlas.json',
96
- } as Parameters<typeof loadFont>[1]);
199
+ // Park a node while the font is loading (waiter list exists synchronously).
200
+ const node = { id: 1, setUpdateType: vi.fn() };
201
+ waitingForFont(
202
+ fontFamily,
203
+ node as unknown as Parameters<typeof waitingForFont>[1],
204
+ );
97
205
 
206
+ // First attempt fails...
98
207
  await flush();
208
+ textures[0]!.emit(
209
+ 'failed',
210
+ new TextureError(TextureErrorCode.TEXTURE_UPLOAD_FAILED, 'boom'),
211
+ );
99
212
 
213
+ // ...the automatic retry succeeds.
214
+ await flush();
215
+ textures[1]!.emit('loaded');
216
+ await promise;
217
+
218
+ expect(isFontLoaded(fontFamily)).toBe(true);
219
+ expect(node.setUpdateType).toHaveBeenCalledTimes(1);
220
+ // Only two attempts were needed.
221
+ expect(textures.length).toBe(2);
222
+ expect(warnSpy).toHaveBeenCalledTimes(1);
223
+ expect(errSpy).not.toHaveBeenCalled();
224
+ });
225
+
226
+ it('keeps parked nodes after every retry fails so a later loadFont still wakes them', async () => {
227
+ const { stage, textures } = makeStage();
228
+ const fontFamily = 'RetryExhaustReuseFont';
100
229
  const error = new TextureError(
101
230
  TextureErrorCode.TEXTURE_UPLOAD_FAILED,
102
231
  'boom',
103
232
  );
104
- const rejected = promise.catch(() => {});
105
- tex.emit('failed', error);
106
- await rejected;
107
233
 
108
- const lastCall = errSpy.mock.calls[errSpy.mock.calls.length - 1]!;
109
- expect(lastCall[1]).toBe(error);
110
- expect(lastCall[1]).not.toBe(tex);
234
+ const first = loadFont(stage, opts(fontFamily));
235
+ const firstRejected = first.catch(() => {});
236
+
237
+ const node = { id: 1, setUpdateType: vi.fn() };
238
+ waitingForFont(
239
+ fontFamily,
240
+ node as unknown as Parameters<typeof waitingForFont>[1],
241
+ );
242
+
243
+ await failAllAttempts(textures, error, TOTAL_ATTEMPTS);
244
+ await firstRejected;
245
+
246
+ expect(isFontLoaded(fontFamily)).toBe(false);
247
+ expect(node.setUpdateType).not.toHaveBeenCalled();
248
+
249
+ // A fresh load reuses the still-parked node and wakes it on success.
250
+ const second = loadFont(stage, opts(fontFamily));
251
+ await flush();
252
+ textures[TOTAL_ATTEMPTS]!.emit('loaded');
253
+ await second;
254
+
255
+ expect(isFontLoaded(fontFamily)).toBe(true);
256
+ expect(node.setUpdateType).toHaveBeenCalledTimes(1);
111
257
  });
112
258
  });
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { ImageTexture } from './ImageTexture.js';
3
+ import type { CoreTextureManager } from '../CoreTextureManager.js';
4
+
5
+ describe('ImageTexture.createImageBitmap', () => {
6
+ beforeEach(() => {
7
+ vi.stubGlobal('ImageBitmap', class {});
8
+ });
9
+
10
+ it('passes options to createImageBitmap when no crop is requested and options/full are supported', async () => {
11
+ const createImageBitmapMock = vi.fn(() =>
12
+ Promise.resolve({ close: () => {} }),
13
+ );
14
+ const txManager = {
15
+ imageBitmapSupported: {
16
+ basic: true,
17
+ options: true,
18
+ full: true,
19
+ premultiplyHonored: false,
20
+ },
21
+ platform: {
22
+ createImageBitmap: createImageBitmapMock,
23
+ },
24
+ } as unknown as CoreTextureManager;
25
+
26
+ const props = ImageTexture.resolveDefaults({
27
+ src: 'test.png',
28
+ premultiplyAlpha: true,
29
+ });
30
+ const texture = new ImageTexture(txManager, props);
31
+
32
+ const blob = new Blob([], { type: 'image/png' });
33
+ const result = await texture.createImageBitmap(
34
+ blob,
35
+ true,
36
+ null,
37
+ null,
38
+ null,
39
+ null,
40
+ );
41
+
42
+ // It should have called createImageBitmap with the options object
43
+ expect(createImageBitmapMock).toHaveBeenCalledWith(blob, {
44
+ premultiplyAlpha: 'none',
45
+ colorSpaceConversion: 'none',
46
+ imageOrientation: 'none',
47
+ });
48
+
49
+ // Since premultiplyHonored is false, useGlPremultiply should be true
50
+ expect(result.premultiplyAlpha).toBe(true);
51
+ });
52
+
53
+ it('falls back to basic createImageBitmap when options are not supported', async () => {
54
+ const createImageBitmapMock = vi.fn(() =>
55
+ Promise.resolve({ close: () => {} }),
56
+ );
57
+ const txManager = {
58
+ imageBitmapSupported: {
59
+ basic: true,
60
+ options: false,
61
+ full: false,
62
+ premultiplyHonored: null,
63
+ },
64
+ platform: {
65
+ createImageBitmap: createImageBitmapMock,
66
+ },
67
+ } as unknown as CoreTextureManager;
68
+
69
+ const props = ImageTexture.resolveDefaults({
70
+ src: 'test.png',
71
+ premultiplyAlpha: true,
72
+ });
73
+ const texture = new ImageTexture(txManager, props);
74
+
75
+ const blob = new Blob([], { type: 'image/png' });
76
+ const result = await texture.createImageBitmap(
77
+ blob,
78
+ true,
79
+ null,
80
+ null,
81
+ null,
82
+ null,
83
+ );
84
+
85
+ // It should have called basic createImageBitmap without options
86
+ expect(createImageBitmapMock).toHaveBeenCalledWith(blob);
87
+
88
+ // And premultiplyAlpha should be false (browser default is assumed to premultiply)
89
+ expect(result.premultiplyAlpha).toBe(false);
90
+ });
91
+ });
@@ -219,7 +219,10 @@ export class ImageTexture extends Texture {
219
219
  },
220
220
  );
221
221
  return { data: bitmap, premultiplyAlpha: useGlPremultiply };
222
- } else if (imageBitmapSupported.basic === true) {
222
+ } else if (
223
+ imageBitmapSupported.options === false &&
224
+ imageBitmapSupported.full === false
225
+ ) {
223
226
  // basic createImageBitmap without options or crop
224
227
  // this is supported for Chrome v50 to v52/54 that doesn't support options.
225
228
  // The browser default premultiplies, so WebGL must not premultiply again.