@series-inc/stowkit-phaser-loader 0.1.18

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.
@@ -0,0 +1,623 @@
1
+ import { PerfLogger, AssetType, StowKitReader } from '@series-inc/stowkit-reader';
2
+ export { AssetType, PerfLogger } from '@series-inc/stowkit-reader';
3
+
4
+ /**
5
+ * BasisTranscoder - Manually decode KTX2 textures to WebGL textures
6
+ * Used for Phaser which doesn't have built-in KTX2 support
7
+ */
8
+ // Basis transcoder formats (from Three.js KTX2Loader.TranscoderFormat)
9
+ const BASIS_FORMAT = {
10
+ ETC1: 0,
11
+ ETC2: 1,
12
+ BC1: 2, // DXT1
13
+ BC3: 3, // DXT5
14
+ BC7_M5: 7, // BC7
15
+ PVRTC1_4_RGB: 8,
16
+ PVRTC1_4_RGBA: 9,
17
+ ASTC_4x4: 10,
18
+ RGBA32: 13};
19
+ class BasisTranscoder {
20
+ constructor(basisPath = '/basis/') {
21
+ this.module = null;
22
+ this.initialized = false;
23
+ this.initPromise = null;
24
+ this.basisPath = basisPath;
25
+ }
26
+ /**
27
+ * Initialize the Basis Universal transcoder
28
+ */
29
+ async init() {
30
+ if (this.initialized)
31
+ return;
32
+ if (this.initPromise)
33
+ return this.initPromise;
34
+ this.initPromise = (async () => {
35
+ try {
36
+ // Load the basis transcoder
37
+ const scriptPath = `${this.basisPath}basis_transcoder.js`;
38
+ // Import the basis module
39
+ const BasisModule = await this.loadBasisModule(scriptPath);
40
+ // Initialize
41
+ BasisModule.initializeBasis();
42
+ this.module = BasisModule;
43
+ this.initialized = true;
44
+ }
45
+ catch (error) {
46
+ console.error('[BasisTranscoder] Failed to initialize:', error);
47
+ throw error;
48
+ }
49
+ })();
50
+ return this.initPromise;
51
+ }
52
+ /**
53
+ * Load the basis transcoder module
54
+ */
55
+ async loadBasisModule(scriptPath) {
56
+ return new Promise((resolve, reject) => {
57
+ const script = document.createElement('script');
58
+ script.src = scriptPath;
59
+ script.async = true;
60
+ script.onload = () => {
61
+ // The basis transcoder exposes a global BASIS function
62
+ if (typeof window.BASIS === 'function') {
63
+ window.BASIS().then((module) => {
64
+ resolve(module);
65
+ }).catch(reject);
66
+ }
67
+ else {
68
+ reject(new Error('BASIS module not found'));
69
+ }
70
+ };
71
+ script.onerror = () => {
72
+ reject(new Error(`Failed to load ${scriptPath}`));
73
+ };
74
+ document.head.appendChild(script);
75
+ });
76
+ }
77
+ /**
78
+ * Transcode KTX2 data to a WebGL-compatible format
79
+ */
80
+ async transcodeKTX2(data, gl) {
81
+ if (!this.initialized) {
82
+ await this.init();
83
+ }
84
+ if (!this.module) {
85
+ throw new Error('Basis module not initialized');
86
+ }
87
+ // Create KTX2File from data
88
+ const ktx2File = new this.module.KTX2File(data);
89
+ try {
90
+ // Validate file
91
+ if (!ktx2File.isValid()) {
92
+ throw new Error('Invalid or unsupported KTX2 file');
93
+ }
94
+ // Get image dimensions
95
+ const width = ktx2File.getWidth();
96
+ const height = ktx2File.getHeight();
97
+ // Check if the texture has alpha
98
+ const hasAlpha = ktx2File.getHasAlpha();
99
+ // Check format type (UASTC or ETC1S)
100
+ const isUASTC = ktx2File.isUASTC();
101
+ const isETC1S = ktx2File.isETC1S();
102
+ // Start transcoding
103
+ if (!ktx2File.startTranscoding()) {
104
+ throw new Error('Failed to start KTX2 transcoding');
105
+ }
106
+ // Use first mip level, first layer, first face
107
+ const mip = 0;
108
+ const layer = 0;
109
+ const face = 0;
110
+ // Detect best format for this device and file type
111
+ const targetFormat = this.detectBestFormat(gl, hasAlpha, isETC1S);
112
+ // Get transcoded size
113
+ const transcodedSize = ktx2File.getImageTranscodedSizeInBytes(mip, layer, face, targetFormat.basisFormat);
114
+ // Allocate output buffer
115
+ const transcodedData = new Uint8Array(transcodedSize);
116
+ // Transcode (parameters: dst, mip, layer, face, format, unused1, unused2, unused3)
117
+ const result = ktx2File.transcodeImage(transcodedData, mip, layer, face, targetFormat.basisFormat, 0, -1, -1);
118
+ if (result === 0) {
119
+ throw new Error('KTX2 transcoding failed');
120
+ }
121
+ return {
122
+ data: transcodedData,
123
+ width,
124
+ height,
125
+ format: targetFormat.glFormat,
126
+ internalFormat: targetFormat.glInternalFormat,
127
+ compressed: targetFormat.compressed
128
+ };
129
+ }
130
+ finally {
131
+ ktx2File.close();
132
+ ktx2File.delete();
133
+ }
134
+ }
135
+ /**
136
+ * Detect the best compression format supported by the device
137
+ */
138
+ detectBestFormat(gl, hasAlpha = true, isETC1S = false) {
139
+ const isWebGL2 = gl instanceof WebGL2RenderingContext;
140
+ // Check for extensions
141
+ const s3tc = gl.getExtension('WEBGL_compressed_texture_s3tc') ||
142
+ gl.getExtension('WEBKIT_WEBGL_compressed_texture_s3tc');
143
+ const bptc = gl.getExtension('EXT_texture_compression_bptc');
144
+ const etc1 = gl.getExtension('WEBGL_compressed_texture_etc1');
145
+ const etc = gl.getExtension('WEBGL_compressed_texture_etc');
146
+ const astc = gl.getExtension('WEBGL_compressed_texture_astc');
147
+ const pvrtc = gl.getExtension('WEBGL_compressed_texture_pvrtc') ||
148
+ gl.getExtension('WEBKIT_WEBGL_compressed_texture_pvrtc');
149
+ // For ETC1S: Use BC1 (no alpha support in ETC1S)
150
+ // For UASTC with alpha: Use BC3/BC7
151
+ // For UASTC without alpha: Use BC1
152
+ // BC7 on desktop with WebGL2 (requires separate BPTC extension)
153
+ // Only use for UASTC with alpha
154
+ if (isWebGL2 && bptc && hasAlpha && !isETC1S) {
155
+ return {
156
+ basisFormat: BASIS_FORMAT.BC7_M5,
157
+ glFormat: gl.RGBA,
158
+ glInternalFormat: bptc.COMPRESSED_RGBA_BPTC_UNORM_EXT,
159
+ compressed: true
160
+ };
161
+ }
162
+ // BC3 (DXT5) on desktop - only for textures with alpha (UASTC)
163
+ if (s3tc && hasAlpha && !isETC1S) {
164
+ return {
165
+ basisFormat: BASIS_FORMAT.BC3,
166
+ glFormat: gl.RGBA,
167
+ glInternalFormat: s3tc.COMPRESSED_RGBA_S3TC_DXT5_EXT,
168
+ compressed: true
169
+ };
170
+ }
171
+ // BC1 (DXT1) on desktop - for ETC1S or textures without alpha
172
+ if (s3tc) {
173
+ return {
174
+ basisFormat: BASIS_FORMAT.BC1,
175
+ glFormat: gl.RGB,
176
+ glInternalFormat: s3tc.COMPRESSED_RGB_S3TC_DXT1_EXT,
177
+ compressed: true
178
+ };
179
+ }
180
+ // ASTC on mobile (best quality)
181
+ if (astc) {
182
+ return {
183
+ basisFormat: BASIS_FORMAT.ASTC_4x4,
184
+ glFormat: gl.RGBA,
185
+ glInternalFormat: astc.COMPRESSED_RGBA_ASTC_4x4_KHR,
186
+ compressed: true
187
+ };
188
+ }
189
+ // ETC2 on mobile with WebGL2
190
+ if (isWebGL2 && etc) {
191
+ return {
192
+ basisFormat: BASIS_FORMAT.ETC2,
193
+ glFormat: gl.RGBA,
194
+ glInternalFormat: etc.COMPRESSED_RGBA8_ETC2_EAC || 0x9278,
195
+ compressed: true
196
+ };
197
+ }
198
+ // ETC1 on mobile
199
+ if (etc1 || etc) {
200
+ return {
201
+ basisFormat: BASIS_FORMAT.ETC1,
202
+ glFormat: gl.RGB,
203
+ glInternalFormat: etc1?.COMPRESSED_RGB_ETC1_WEBGL || 0x8D64,
204
+ compressed: true
205
+ };
206
+ }
207
+ // PVRTC on iOS (last resort for compressed)
208
+ if (pvrtc) {
209
+ return {
210
+ basisFormat: hasAlpha ? BASIS_FORMAT.PVRTC1_4_RGBA : BASIS_FORMAT.PVRTC1_4_RGB,
211
+ glFormat: gl.RGBA,
212
+ glInternalFormat: pvrtc.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG,
213
+ compressed: true
214
+ };
215
+ }
216
+ // Fallback to uncompressed RGBA
217
+ return {
218
+ basisFormat: BASIS_FORMAT.RGBA32,
219
+ glFormat: gl.RGBA,
220
+ glInternalFormat: gl.RGBA,
221
+ compressed: false
222
+ };
223
+ }
224
+ /**
225
+ * Dispose resources
226
+ */
227
+ dispose() {
228
+ this.module = null;
229
+ this.initialized = false;
230
+ this.initPromise = null;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Represents an opened StowKit pack for Phaser
236
+ * Supports loading images and audio only (no 3D models)
237
+ */
238
+ class StowKitPhaserPack {
239
+ constructor(reader, transcoder, gl) {
240
+ this.textureCache = new Map();
241
+ this.transcodedDataCache = new Map();
242
+ this.reader = reader;
243
+ this.transcoder = transcoder;
244
+ this.tempGl = gl;
245
+ }
246
+ /**
247
+ * Load a texture by its canonical path/name
248
+ * Returns a Phaser texture
249
+ */
250
+ async loadTexture(assetPath, scene) {
251
+ const totalStart = performance.now();
252
+ // Check cache first
253
+ if (this.textureCache.has(assetPath)) {
254
+ PerfLogger.log(`[Perf] Texture cache hit: ${assetPath}`);
255
+ return this.textureCache.get(assetPath);
256
+ }
257
+ // Find asset by path
258
+ const assetIndex = this.reader.findAssetByPath(assetPath);
259
+ if (assetIndex < 0) {
260
+ throw new Error(`Texture not found: ${assetPath}`);
261
+ }
262
+ // Verify it's a texture
263
+ const info = this.reader.getAssetInfo(assetIndex);
264
+ if (!info || info.type !== AssetType.TEXTURE_2D) {
265
+ throw new Error(`Asset is not a texture: ${assetPath}`);
266
+ }
267
+ // Use the same logic as getPhaserTexture but with custom key
268
+ const key = assetPath;
269
+ const gl = scene.sys.game.renderer.gl;
270
+ // Read and transcode
271
+ const data = this.reader.readAssetData(assetIndex);
272
+ if (!data) {
273
+ throw new Error(`Failed to read texture data for ${assetPath}`);
274
+ }
275
+ const transcodeStart = performance.now();
276
+ const transcoded = await this.transcoder.transcodeKTX2(data, gl);
277
+ PerfLogger.log(`[Perf] Phaser Basis transcode: ${(performance.now() - transcodeStart).toFixed(2)}ms (${transcoded.width}x${transcoded.height})`);
278
+ // Format like Phaser's KTX parser
279
+ const compressedTextureData = {
280
+ mipmaps: [{
281
+ data: transcoded.data,
282
+ width: transcoded.width,
283
+ height: transcoded.height
284
+ }],
285
+ width: transcoded.width,
286
+ height: transcoded.height,
287
+ internalFormat: transcoded.internalFormat,
288
+ compressed: transcoded.compressed,
289
+ generateMipmap: false,
290
+ format: transcoded.internalFormat
291
+ };
292
+ const phaserTexture = scene.textures.addCompressedTexture(key, compressedTextureData);
293
+ this.textureCache.set(assetPath, phaserTexture);
294
+ PerfLogger.log(`[Perf] ===== Total Phaser texture load: ${(performance.now() - totalStart).toFixed(2)}ms =====`);
295
+ return phaserTexture;
296
+ }
297
+ /**
298
+ * Get a Phaser texture by index (for previews/demos)
299
+ * Creates compressed texture directly in Phaser's GL context
300
+ */
301
+ async getPhaserTexture(index, scene) {
302
+ const key = `texture_${index}`;
303
+ // Check if already exists in Phaser's texture manager
304
+ if (scene.textures.exists(key)) {
305
+ return scene.textures.get(key);
306
+ }
307
+ // Get Phaser's GL context
308
+ const gl = scene.sys.game.renderer.gl;
309
+ // Read and transcode the KTX2 data
310
+ const data = this.reader.readAssetData(index);
311
+ if (!data) {
312
+ throw new Error(`Failed to read texture data for index ${index}`);
313
+ }
314
+ // Transcode to compressed format (don't create the WebGL texture yet - let Phaser do it)
315
+ const transcoded = await this.transcoder.transcodeKTX2(data, gl);
316
+ // Format the data exactly like Phaser's KTX parser output
317
+ const compressedTextureData = {
318
+ mipmaps: [{
319
+ data: transcoded.data,
320
+ width: transcoded.width,
321
+ height: transcoded.height
322
+ }],
323
+ width: transcoded.width,
324
+ height: transcoded.height,
325
+ internalFormat: transcoded.internalFormat,
326
+ compressed: transcoded.compressed,
327
+ generateMipmap: false,
328
+ format: transcoded.internalFormat // Phaser uses this for compressionAlgorithm
329
+ };
330
+ // Add to Phaser's texture manager as a compressed texture
331
+ const phaserTexture = scene.textures.addCompressedTexture(key, compressedTextureData);
332
+ // Cache it
333
+ this.textureCache.set(key, phaserTexture);
334
+ return phaserTexture;
335
+ }
336
+ /**
337
+ * Load texture by index into a specific GL context
338
+ */
339
+ async loadTextureInContext(index, gl) {
340
+ // Read texture data
341
+ const data = this.reader.readAssetData(index);
342
+ if (!data) {
343
+ throw new Error(`Failed to read texture data for index ${index}`);
344
+ }
345
+ // Get metadata - all textures in .stow files are KTX2 format
346
+ const metadata = this.reader.parseTextureMetadata(index);
347
+ const isKtx2 = metadata?.channelFormat !== undefined;
348
+ if (isKtx2) {
349
+ // Transcode KTX2 to WebGL texture using the provided context
350
+ const transcoded = await this.transcoder.transcodeKTX2(data, gl);
351
+ // Create WebGL texture in the provided context
352
+ const texture = gl.createTexture();
353
+ if (!texture) {
354
+ throw new Error('Failed to create WebGL texture');
355
+ }
356
+ gl.bindTexture(gl.TEXTURE_2D, texture);
357
+ if (transcoded.compressed) {
358
+ gl.compressedTexImage2D(gl.TEXTURE_2D, 0, transcoded.internalFormat, transcoded.width, transcoded.height, 0, transcoded.data);
359
+ }
360
+ else {
361
+ gl.texImage2D(gl.TEXTURE_2D, 0, transcoded.internalFormat, transcoded.width, transcoded.height, 0, transcoded.format, gl.UNSIGNED_BYTE, transcoded.data);
362
+ }
363
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
364
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
365
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
366
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
367
+ gl.bindTexture(gl.TEXTURE_2D, null);
368
+ return {
369
+ texture,
370
+ width: transcoded.width,
371
+ height: transcoded.height
372
+ };
373
+ }
374
+ else {
375
+ // Handle uncompressed image data
376
+ return await this.loadUncompressedTextureInContext(data, gl);
377
+ }
378
+ }
379
+ /**
380
+ * Load uncompressed image (PNG, JPEG) as WebGL texture in a specific context
381
+ */
382
+ async loadUncompressedTextureInContext(data, gl) {
383
+ return new Promise((resolve, reject) => {
384
+ const blob = new Blob([data.buffer]);
385
+ const url = URL.createObjectURL(blob);
386
+ const img = new Image();
387
+ img.onload = () => {
388
+ const texture = gl.createTexture();
389
+ if (!texture) {
390
+ URL.revokeObjectURL(url);
391
+ reject(new Error('Failed to create WebGL texture'));
392
+ return;
393
+ }
394
+ gl.bindTexture(gl.TEXTURE_2D, texture);
395
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
396
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
397
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
398
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
399
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
400
+ gl.bindTexture(gl.TEXTURE_2D, null);
401
+ URL.revokeObjectURL(url);
402
+ resolve({
403
+ texture,
404
+ width: img.width,
405
+ height: img.height
406
+ });
407
+ };
408
+ img.onerror = () => {
409
+ URL.revokeObjectURL(url);
410
+ reject(new Error('Failed to load image'));
411
+ };
412
+ img.src = url;
413
+ });
414
+ }
415
+ /**
416
+ * Load audio by its canonical path/name
417
+ * Returns an AudioBuffer
418
+ */
419
+ async loadAudio(assetPath, audioContext) {
420
+ // Find asset by path
421
+ const assetIndex = this.reader.findAssetByPath(assetPath);
422
+ if (assetIndex < 0) {
423
+ throw new Error(`Audio not found: ${assetPath}`);
424
+ }
425
+ // Verify it's audio
426
+ const info = this.reader.getAssetInfo(assetIndex);
427
+ if (!info || info.type !== AssetType.AUDIO) {
428
+ throw new Error(`Asset is not audio: ${assetPath}`);
429
+ }
430
+ return await this.loadAudioByIndex(assetIndex, audioContext);
431
+ }
432
+ /**
433
+ * Load audio by index
434
+ */
435
+ async loadAudioByIndex(index, audioContext) {
436
+ const data = this.reader.readAssetData(index);
437
+ if (!data) {
438
+ throw new Error(`Failed to read audio data for index ${index}`);
439
+ }
440
+ // Use provided context or create a new one
441
+ const ctx = audioContext || new AudioContext();
442
+ // Decode audio data
443
+ const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
444
+ const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
445
+ return audioBuffer;
446
+ }
447
+ /**
448
+ * Create an HTML audio element for preview
449
+ */
450
+ async createAudioPreview(index) {
451
+ const data = this.reader.readAssetData(index);
452
+ if (!data) {
453
+ throw new Error(`Failed to read audio for index ${index}`);
454
+ }
455
+ const blob = new Blob([data.buffer], { type: 'audio/mp4' });
456
+ const url = URL.createObjectURL(blob);
457
+ const audio = document.createElement('audio');
458
+ audio.controls = true;
459
+ audio.src = url;
460
+ audio.addEventListener('ended', () => URL.revokeObjectURL(url));
461
+ audio.addEventListener('error', () => URL.revokeObjectURL(url));
462
+ return audio;
463
+ }
464
+ /**
465
+ * Get list of all assets in pack
466
+ */
467
+ listAssets() {
468
+ return this.reader.listAssets();
469
+ }
470
+ /**
471
+ * Get asset count
472
+ */
473
+ getAssetCount() {
474
+ return this.reader.getAssetCount();
475
+ }
476
+ /**
477
+ * Get asset info by index
478
+ */
479
+ getAssetInfo(index) {
480
+ return this.reader.getAssetInfo(index);
481
+ }
482
+ /**
483
+ * Get texture metadata
484
+ */
485
+ getTextureMetadata(index) {
486
+ return this.reader.parseTextureMetadata(index);
487
+ }
488
+ /**
489
+ * Get audio metadata
490
+ */
491
+ getAudioMetadata(index) {
492
+ return this.reader.parseAudioMetadata(index);
493
+ }
494
+ /**
495
+ * Close the pack and free resources
496
+ */
497
+ dispose() {
498
+ // Clear texture cache (Phaser manages texture disposal)
499
+ this.textureCache.clear();
500
+ this.transcodedDataCache.clear();
501
+ // Close reader
502
+ this.reader.close();
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Phaser loader for StowKit asset packs
508
+ * Supports images and audio only (no 3D models)
509
+ *
510
+ * Usage:
511
+ * ```typescript
512
+ * const pack = await StowKitPhaserLoader.load('assets.stow');
513
+ * const texture = await pack.loadTexture('textures/player');
514
+ * const audio = await pack.loadAudio('sounds/bgm');
515
+ * ```
516
+ */
517
+ class StowKitPhaserLoader {
518
+ /**
519
+ * Load a .stow pack file from a URL
520
+ */
521
+ static async load(url, options) {
522
+ // Initialize loaders if needed
523
+ if (!this.initialized) {
524
+ await this.initialize(options);
525
+ }
526
+ // Fetch the pack file
527
+ const response = await fetch(url);
528
+ if (!response.ok) {
529
+ throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
530
+ }
531
+ const arrayBuffer = await response.arrayBuffer();
532
+ // Create a new reader instance for this pack
533
+ const reader = new StowKitReader(this.wasmPath);
534
+ await reader.init();
535
+ await reader.open(arrayBuffer);
536
+ // Return pack wrapper with its own dedicated reader
537
+ return new StowKitPhaserPack(reader, this.transcoder, this.gl);
538
+ }
539
+ /**
540
+ * Load a .stow pack from memory (ArrayBuffer, Blob, or File)
541
+ */
542
+ static async loadFromMemory(data, options) {
543
+ // Initialize loaders if needed
544
+ if (!this.initialized) {
545
+ await this.initialize(options);
546
+ }
547
+ // Convert to ArrayBuffer if needed
548
+ let arrayBuffer;
549
+ if (data instanceof ArrayBuffer) {
550
+ arrayBuffer = data;
551
+ }
552
+ else if (typeof Blob !== 'undefined' && data instanceof Blob) {
553
+ arrayBuffer = await data.arrayBuffer();
554
+ }
555
+ else if ('arrayBuffer' in data && typeof data.arrayBuffer === 'function') {
556
+ arrayBuffer = await data.arrayBuffer();
557
+ }
558
+ else {
559
+ throw new Error('Data must be ArrayBuffer, Blob, or File');
560
+ }
561
+ // Create a new reader instance for this pack
562
+ const reader = new StowKitReader(this.wasmPath);
563
+ await reader.init();
564
+ await reader.open(arrayBuffer);
565
+ // Return pack wrapper with its own dedicated reader
566
+ return new StowKitPhaserPack(reader, this.transcoder, this.gl);
567
+ }
568
+ /**
569
+ * Initialize the loader (called automatically on first load)
570
+ */
571
+ static async initialize(options) {
572
+ this.wasmPath = options?.wasmPath || '/stowkit/stowkit_reader.wasm';
573
+ const basisPath = options?.basisPath || '/basis/';
574
+ // Get or create WebGL context (shared across all packs)
575
+ if (options?.gl) {
576
+ this.gl = options.gl;
577
+ this.ownGl = false;
578
+ }
579
+ else {
580
+ // Create a temporary canvas for WebGL operations
581
+ const canvas = document.createElement('canvas');
582
+ canvas.width = 1;
583
+ canvas.height = 1;
584
+ canvas.style.display = 'none';
585
+ document.body.appendChild(canvas);
586
+ this.gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
587
+ if (!this.gl) {
588
+ throw new Error('Failed to create WebGL context');
589
+ }
590
+ this.ownGl = true;
591
+ }
592
+ // Initialize basis transcoder (shared across all packs)
593
+ this.transcoder = new BasisTranscoder(basisPath);
594
+ await this.transcoder.init();
595
+ this.initialized = true;
596
+ }
597
+ /**
598
+ * Dispose of shared resources
599
+ */
600
+ static dispose() {
601
+ if (this.transcoder) {
602
+ this.transcoder.dispose();
603
+ this.transcoder = null;
604
+ }
605
+ // Only dispose GL if we created it
606
+ if (this.gl && this.ownGl) {
607
+ const canvas = this.gl.canvas;
608
+ if (canvas && canvas.parentNode) {
609
+ canvas.parentNode.removeChild(canvas);
610
+ }
611
+ this.gl = null;
612
+ }
613
+ this.initialized = false;
614
+ }
615
+ }
616
+ StowKitPhaserLoader.transcoder = null;
617
+ StowKitPhaserLoader.initialized = false;
618
+ StowKitPhaserLoader.gl = null;
619
+ StowKitPhaserLoader.ownGl = false;
620
+ StowKitPhaserLoader.wasmPath = '/stowkit/stowkit_reader.wasm';
621
+
622
+ export { BasisTranscoder, StowKitPhaserLoader, StowKitPhaserPack };
623
+ //# sourceMappingURL=stowkit-phaser-loader.esm.js.map