bloody-engine 1.0.1 → 1.0.2

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,2364 @@
1
+ import createGL from "gl";
2
+ import * as fs from "fs/promises";
3
+ import * as path from "path";
4
+ class BrowserRenderingContext {
5
+ constructor(options) {
6
+ this.isBrowser = true;
7
+ if (!options.canvas) {
8
+ this.canvas = document.createElement("canvas");
9
+ document.body.appendChild(this.canvas);
10
+ } else {
11
+ this.canvas = options.canvas;
12
+ }
13
+ this.width = options.width;
14
+ this.height = options.height;
15
+ this.canvas.width = this.width;
16
+ this.canvas.height = this.height;
17
+ const contextAttributes = {
18
+ alpha: false,
19
+ ...options.contextAttributes
20
+ };
21
+ const glContext = this.canvas.getContext("webgl", contextAttributes);
22
+ if (!glContext) {
23
+ throw new Error("Failed to initialize WebGL context in browser");
24
+ }
25
+ this.glContext = glContext;
26
+ }
27
+ resize(width, height) {
28
+ this.width = width;
29
+ this.height = height;
30
+ this.canvas.width = width;
31
+ this.canvas.height = height;
32
+ this.glContext.viewport(0, 0, width, height);
33
+ }
34
+ getViewport() {
35
+ return { width: this.width, height: this.height };
36
+ }
37
+ clear(color) {
38
+ if (color) {
39
+ this.glContext.clearColor(color.r, color.g, color.b, color.a);
40
+ }
41
+ this.glContext.clear(
42
+ this.glContext.COLOR_BUFFER_BIT | this.glContext.DEPTH_BUFFER_BIT
43
+ );
44
+ }
45
+ present() {
46
+ }
47
+ dispose() {
48
+ if (this.canvas.parentElement) {
49
+ this.canvas.parentElement.removeChild(this.canvas);
50
+ }
51
+ }
52
+ /**
53
+ * Get the underlying canvas element (browser-specific)
54
+ */
55
+ getCanvas() {
56
+ return this.canvas;
57
+ }
58
+ }
59
+ class NodeRenderingContext {
60
+ constructor(options) {
61
+ this.isBrowser = false;
62
+ this.width = options.width;
63
+ this.height = options.height;
64
+ const glContext = createGL(this.width, this.height, {
65
+ preserveDrawingBuffer: options.preserveDrawingBuffer ?? true,
66
+ ...options.contextAttributes
67
+ });
68
+ if (!glContext) {
69
+ throw new Error("Failed to initialize WebGL context in Node.js");
70
+ }
71
+ this.glContext = glContext;
72
+ }
73
+ resize(width, height) {
74
+ this.width = width;
75
+ this.height = height;
76
+ console.warn(
77
+ "NodeRenderingContext: Resize requested but not supported. Consider recreating context."
78
+ );
79
+ }
80
+ getViewport() {
81
+ return { width: this.width, height: this.height };
82
+ }
83
+ clear(color) {
84
+ if (color) {
85
+ this.glContext.clearColor(color.r, color.g, color.b, color.a);
86
+ }
87
+ this.glContext.clear(
88
+ this.glContext.COLOR_BUFFER_BIT | this.glContext.DEPTH_BUFFER_BIT
89
+ );
90
+ }
91
+ present() {
92
+ this.glContext.flush();
93
+ }
94
+ dispose() {
95
+ this.glContext.flush();
96
+ }
97
+ /**
98
+ * Read the current framebuffer contents as RGBA pixel data
99
+ * Used for capturing frames for display or saving
100
+ */
101
+ readPixels() {
102
+ const pixelData = new Uint8Array(this.width * this.height * 4);
103
+ this.glContext.readPixels(
104
+ 0,
105
+ 0,
106
+ this.width,
107
+ this.height,
108
+ this.glContext.RGBA,
109
+ this.glContext.UNSIGNED_BYTE,
110
+ pixelData
111
+ );
112
+ return pixelData;
113
+ }
114
+ }
115
+ class RenderingContextFactory {
116
+ /**
117
+ * Detect if running in a browser environment
118
+ */
119
+ static isBrowserEnvironment() {
120
+ return typeof window !== "undefined" && typeof document !== "undefined";
121
+ }
122
+ /**
123
+ * Create a rendering context appropriate for the current environment
124
+ */
125
+ static createContext(options) {
126
+ if (this.isBrowserEnvironment()) {
127
+ return new BrowserRenderingContext(options);
128
+ } else {
129
+ return new NodeRenderingContext(options);
130
+ }
131
+ }
132
+ /**
133
+ * Create a browser-specific rendering context
134
+ */
135
+ static createBrowserContext(options) {
136
+ return new BrowserRenderingContext(options);
137
+ }
138
+ /**
139
+ * Create a Node.js-specific rendering context
140
+ */
141
+ static createNodeContext(options) {
142
+ return new NodeRenderingContext(options);
143
+ }
144
+ }
145
+ class Shader {
146
+ /**
147
+ * Create a new shader program
148
+ * @param gl WebGL rendering context
149
+ * @param vertexSource Raw vertex shader source code
150
+ * @param fragmentSource Raw fragment shader source code
151
+ * @param isBrowser Whether running in browser environment (affects precision header)
152
+ */
153
+ constructor(gl, vertexSource, fragmentSource, isBrowser) {
154
+ this.gl = gl;
155
+ const processedVertexSource = this.injectPrecisionHeader(
156
+ vertexSource,
157
+ isBrowser
158
+ );
159
+ const processedFragmentSource = this.injectPrecisionHeader(
160
+ fragmentSource,
161
+ isBrowser
162
+ );
163
+ this.vertexShader = this.compileShader(
164
+ processedVertexSource,
165
+ gl.VERTEX_SHADER
166
+ );
167
+ this.fragmentShader = this.compileShader(
168
+ processedFragmentSource,
169
+ gl.FRAGMENT_SHADER
170
+ );
171
+ this.program = this.linkProgram(this.vertexShader, this.fragmentShader);
172
+ }
173
+ /**
174
+ * Inject precision header for ES and desktop OpenGL differences
175
+ * @param source Original shader source
176
+ * @param isBrowser Whether in browser (WebGL ES) or Node (desktop OpenGL)
177
+ * @returns Processed shader source with precision header
178
+ */
179
+ injectPrecisionHeader(source, isBrowser) {
180
+ if (source.includes("#ifdef GL_ES") || source.includes("precision")) {
181
+ return source;
182
+ }
183
+ if (isBrowser) {
184
+ const precisionHeader = `#ifdef GL_ES
185
+ precision highp float;
186
+ #endif
187
+ `;
188
+ return precisionHeader + source;
189
+ } else {
190
+ const precisionHeader = `#ifdef GL_ES
191
+ precision highp float;
192
+ #endif
193
+ `;
194
+ return precisionHeader + source;
195
+ }
196
+ }
197
+ /**
198
+ * Compile a single shader (vertex or fragment)
199
+ * @param source Shader source code
200
+ * @param type gl.VERTEX_SHADER or gl.FRAGMENT_SHADER
201
+ * @returns Compiled shader
202
+ */
203
+ compileShader(source, type) {
204
+ const shader = this.gl.createShader(type);
205
+ if (!shader) {
206
+ throw new Error(`Failed to create shader of type ${type}`);
207
+ }
208
+ this.gl.shaderSource(shader, source);
209
+ this.gl.compileShader(shader);
210
+ const compiled = this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS);
211
+ if (!compiled) {
212
+ const infoLog = this.gl.getShaderInfoLog(shader);
213
+ const shaderType = type === this.gl.VERTEX_SHADER ? "vertex" : "fragment";
214
+ this.gl.deleteShader(shader);
215
+ throw new Error(
216
+ `Failed to compile ${shaderType} shader:
217
+ ${infoLog}
218
+
219
+ Source:
220
+ ${source}`
221
+ );
222
+ }
223
+ return shader;
224
+ }
225
+ /**
226
+ * Link vertex and fragment shaders into a program
227
+ * @param vertexShader Compiled vertex shader
228
+ * @param fragmentShader Compiled fragment shader
229
+ * @returns Linked shader program
230
+ */
231
+ linkProgram(vertexShader, fragmentShader) {
232
+ const program = this.gl.createProgram();
233
+ if (!program) {
234
+ throw new Error("Failed to create shader program");
235
+ }
236
+ this.gl.attachShader(program, vertexShader);
237
+ this.gl.attachShader(program, fragmentShader);
238
+ this.gl.linkProgram(program);
239
+ const linked = this.gl.getProgramParameter(program, this.gl.LINK_STATUS);
240
+ if (!linked) {
241
+ const infoLog = this.gl.getProgramInfoLog(program);
242
+ this.gl.deleteProgram(program);
243
+ this.gl.deleteShader(vertexShader);
244
+ this.gl.deleteShader(fragmentShader);
245
+ throw new Error(`Failed to link shader program:
246
+ ${infoLog}`);
247
+ }
248
+ return program;
249
+ }
250
+ /**
251
+ * Get the compiled shader program
252
+ */
253
+ getProgram() {
254
+ return this.program;
255
+ }
256
+ /**
257
+ * Get uniform location by name
258
+ * @param name Uniform variable name
259
+ */
260
+ getUniformLocation(name) {
261
+ return this.gl.getUniformLocation(this.program, name);
262
+ }
263
+ /**
264
+ * Get attribute location by name
265
+ * @param name Attribute variable name
266
+ */
267
+ getAttributeLocation(name) {
268
+ return this.gl.getAttribLocation(this.program, name);
269
+ }
270
+ /**
271
+ * Use this shader program
272
+ */
273
+ use() {
274
+ this.gl.useProgram(this.program);
275
+ }
276
+ /**
277
+ * Clean up shader resources
278
+ */
279
+ dispose() {
280
+ this.gl.deleteProgram(this.program);
281
+ this.gl.deleteShader(this.vertexShader);
282
+ this.gl.deleteShader(this.fragmentShader);
283
+ }
284
+ }
285
+ class GraphicsDevice {
286
+ constructor(width, height) {
287
+ this.context = RenderingContextFactory.createContext({
288
+ width,
289
+ height,
290
+ preserveDrawingBuffer: true
291
+ });
292
+ }
293
+ /**
294
+ * Get the underlying WebGL rendering context
295
+ */
296
+ getGLContext() {
297
+ return this.context.glContext;
298
+ }
299
+ /**
300
+ * Get the rendering context
301
+ */
302
+ getRenderingContext() {
303
+ return this.context;
304
+ }
305
+ /**
306
+ * Get current width
307
+ */
308
+ getWidth() {
309
+ return this.context.width;
310
+ }
311
+ /**
312
+ * Get current height
313
+ */
314
+ getHeight() {
315
+ return this.context.height;
316
+ }
317
+ /**
318
+ * Get viewport dimensions
319
+ */
320
+ getViewport() {
321
+ return this.context.getViewport();
322
+ }
323
+ /**
324
+ * Check if running in browser
325
+ */
326
+ isBrowser() {
327
+ return this.context.isBrowser;
328
+ }
329
+ /**
330
+ * Resize the graphics device
331
+ */
332
+ resize(width, height) {
333
+ this.context.resize(width, height);
334
+ }
335
+ /**
336
+ * Clear the rendering surface
337
+ */
338
+ clear(color) {
339
+ this.context.clear(color);
340
+ }
341
+ /**
342
+ * Present the rendered frame
343
+ */
344
+ present() {
345
+ this.context.present();
346
+ }
347
+ /**
348
+ * Cleanup and release resources
349
+ */
350
+ dispose() {
351
+ this.context.dispose();
352
+ }
353
+ /**
354
+ * Create a shader program
355
+ * @param vertexSource Vertex shader source code
356
+ * @param fragmentSource Fragment shader source code
357
+ * @returns Compiled and linked shader program
358
+ */
359
+ createShader(vertexSource, fragmentSource) {
360
+ return new Shader(
361
+ this.context.glContext,
362
+ vertexSource,
363
+ fragmentSource,
364
+ this.context.isBrowser
365
+ );
366
+ }
367
+ }
368
+ class Texture {
369
+ /**
370
+ * Create a texture from pixel data
371
+ * @param gl WebGL context
372
+ * @param width Texture width
373
+ * @param height Texture height
374
+ * @param data Pixel data (Uint8Array RGBA)
375
+ */
376
+ constructor(gl, width, height, data) {
377
+ this.gl = gl;
378
+ this.width = width;
379
+ this.height = height;
380
+ const tex = gl.createTexture();
381
+ if (!tex) {
382
+ throw new Error("Failed to create texture");
383
+ }
384
+ this.texture = tex;
385
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
386
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
387
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
388
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
389
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
390
+ if (data) {
391
+ gl.texImage2D(
392
+ gl.TEXTURE_2D,
393
+ 0,
394
+ gl.RGBA,
395
+ width,
396
+ height,
397
+ 0,
398
+ gl.RGBA,
399
+ gl.UNSIGNED_BYTE,
400
+ data
401
+ );
402
+ } else {
403
+ gl.texImage2D(
404
+ gl.TEXTURE_2D,
405
+ 0,
406
+ gl.RGBA,
407
+ width,
408
+ height,
409
+ 0,
410
+ gl.RGBA,
411
+ gl.UNSIGNED_BYTE,
412
+ null
413
+ );
414
+ }
415
+ gl.bindTexture(gl.TEXTURE_2D, null);
416
+ }
417
+ /**
418
+ * Create a solid color texture
419
+ * @param gl WebGL context
420
+ * @param width Texture width
421
+ * @param height Texture height
422
+ * @param r Red (0-255)
423
+ * @param g Green (0-255)
424
+ * @param b Blue (0-255)
425
+ * @param a Alpha (0-255)
426
+ */
427
+ static createSolid(gl, width, height, r, g, b, a = 255) {
428
+ const pixelCount = width * height;
429
+ const data = new Uint8Array(pixelCount * 4);
430
+ for (let i = 0; i < pixelCount; i++) {
431
+ const offset = i * 4;
432
+ data[offset] = r;
433
+ data[offset + 1] = g;
434
+ data[offset + 2] = b;
435
+ data[offset + 3] = a;
436
+ }
437
+ return new Texture(gl, width, height, data);
438
+ }
439
+ /**
440
+ * Create a checkerboard texture
441
+ * @param gl WebGL context
442
+ * @param width Texture width
443
+ * @param height Texture height
444
+ * @param squareSize Size of each square
445
+ */
446
+ static createCheckerboard(gl, width, height, squareSize = 32) {
447
+ const data = new Uint8Array(width * height * 4);
448
+ for (let y = 0; y < height; y++) {
449
+ for (let x = 0; x < width; x++) {
450
+ const squareX = Math.floor(x / squareSize);
451
+ const squareY = Math.floor(y / squareSize);
452
+ const isWhite = (squareX + squareY) % 2 === 0;
453
+ const offset = (y * width + x) * 4;
454
+ const color = isWhite ? 255 : 0;
455
+ data[offset] = color;
456
+ data[offset + 1] = color;
457
+ data[offset + 2] = color;
458
+ data[offset + 3] = 255;
459
+ }
460
+ }
461
+ return new Texture(gl, width, height, data);
462
+ }
463
+ /**
464
+ * Create a gradient texture
465
+ * @param gl WebGL context
466
+ * @param width Texture width
467
+ * @param height Texture height
468
+ */
469
+ static createGradient(gl, width, height) {
470
+ const data = new Uint8Array(width * height * 4);
471
+ for (let y = 0; y < height; y++) {
472
+ for (let x = 0; x < width; x++) {
473
+ const offset = (y * width + x) * 4;
474
+ data[offset] = Math.floor(x / width * 255);
475
+ data[offset + 1] = Math.floor(y / height * 255);
476
+ data[offset + 2] = 128;
477
+ data[offset + 3] = 255;
478
+ }
479
+ }
480
+ return new Texture(gl, width, height, data);
481
+ }
482
+ /**
483
+ * Bind this texture to a texture unit
484
+ * @param unit Texture unit (0-7 typically)
485
+ */
486
+ bind(unit = 0) {
487
+ this.gl.activeTexture(this.gl.TEXTURE0 + unit);
488
+ this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
489
+ }
490
+ /**
491
+ * Unbind texture
492
+ */
493
+ unbind() {
494
+ this.gl.bindTexture(this.gl.TEXTURE_2D, null);
495
+ }
496
+ /**
497
+ * Get the underlying WebGL texture
498
+ */
499
+ getHandle() {
500
+ return this.texture;
501
+ }
502
+ /**
503
+ * Get texture dimensions
504
+ */
505
+ getDimensions() {
506
+ return { width: this.width, height: this.height };
507
+ }
508
+ /**
509
+ * Clean up texture resources
510
+ */
511
+ dispose() {
512
+ this.gl.deleteTexture(this.texture);
513
+ }
514
+ }
515
+ class VertexBuffer {
516
+ constructor(gl, data, stride = 0) {
517
+ this.gl = gl;
518
+ this.stride = stride;
519
+ const floatsPerVertex = stride > 0 ? stride / 4 : 3;
520
+ this.vertexCount = data.length / floatsPerVertex;
521
+ const buf = gl.createBuffer();
522
+ if (!buf) {
523
+ throw new Error("Failed to create vertex buffer");
524
+ }
525
+ this.buffer = buf;
526
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
527
+ gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
528
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
529
+ }
530
+ /**
531
+ * Bind buffer for rendering
532
+ */
533
+ bind() {
534
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
535
+ }
536
+ /**
537
+ * Unbind buffer
538
+ */
539
+ unbind() {
540
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
541
+ }
542
+ /**
543
+ * Get vertex count
544
+ */
545
+ getVertexCount() {
546
+ return this.vertexCount;
547
+ }
548
+ /**
549
+ * Get stride
550
+ */
551
+ getStride() {
552
+ return this.stride;
553
+ }
554
+ /**
555
+ * Clean up resources
556
+ */
557
+ dispose() {
558
+ this.gl.deleteBuffer(this.buffer);
559
+ }
560
+ }
561
+ class IndexBuffer {
562
+ constructor(gl, data) {
563
+ this.gl = gl;
564
+ this.indexCount = data.length;
565
+ const buf = gl.createBuffer();
566
+ if (!buf) {
567
+ throw new Error("Failed to create index buffer");
568
+ }
569
+ this.buffer = buf;
570
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.buffer);
571
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW);
572
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
573
+ }
574
+ /**
575
+ * Bind buffer for rendering
576
+ */
577
+ bind() {
578
+ this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.buffer);
579
+ }
580
+ /**
581
+ * Unbind buffer
582
+ */
583
+ unbind() {
584
+ this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, null);
585
+ }
586
+ /**
587
+ * Get index count
588
+ */
589
+ getIndexCount() {
590
+ return this.indexCount;
591
+ }
592
+ /**
593
+ * Clean up resources
594
+ */
595
+ dispose() {
596
+ this.gl.deleteBuffer(this.buffer);
597
+ }
598
+ }
599
+ const buffer = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
600
+ __proto__: null,
601
+ IndexBuffer,
602
+ VertexBuffer
603
+ }, Symbol.toStringTag, { value: "Module" }));
604
+ class BatchRenderer {
605
+ /**
606
+ * Create a new batch renderer (V1)
607
+ * @param gl WebGL rendering context
608
+ * @param shader Shader program to use
609
+ * @param maxQuads Maximum number of quads to batch (default 1000)
610
+ */
611
+ constructor(gl, shader, maxQuads = 1e3) {
612
+ this.vertexBuffer = null;
613
+ this.quads = [];
614
+ this.isDirty = false;
615
+ this.verticesPerQuad = 6;
616
+ this.floatsPerVertex = 5;
617
+ this.texture = null;
618
+ this.gl = gl;
619
+ this.shader = shader;
620
+ this.maxQuads = maxQuads;
621
+ const totalFloats = maxQuads * this.verticesPerQuad * this.floatsPerVertex;
622
+ this.vertexData = new Float32Array(totalFloats);
623
+ const buf = gl.createBuffer();
624
+ if (!buf) {
625
+ throw new Error("Failed to create vertex buffer");
626
+ }
627
+ this.vertexBuffer = buf;
628
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
629
+ gl.bufferData(gl.ARRAY_BUFFER, this.vertexData.byteLength, gl.DYNAMIC_DRAW);
630
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
631
+ }
632
+ /**
633
+ * Set the texture for batch rendering
634
+ * @param texture The texture to use when rendering
635
+ */
636
+ setTexture(texture) {
637
+ this.texture = texture;
638
+ }
639
+ /**
640
+ * Add a quad to the batch
641
+ * @param quad Quad instance to add
642
+ */
643
+ addQuad(quad) {
644
+ if (this.quads.length >= this.maxQuads) {
645
+ console.warn(`Batch renderer at max capacity (${this.maxQuads})`);
646
+ return;
647
+ }
648
+ this.quads.push(quad);
649
+ this.isDirty = true;
650
+ }
651
+ /**
652
+ * Clear all quads from the batch
653
+ */
654
+ clear() {
655
+ this.quads = [];
656
+ this.isDirty = true;
657
+ }
658
+ /**
659
+ * Get number of quads currently in batch
660
+ */
661
+ getQuadCount() {
662
+ return this.quads.length;
663
+ }
664
+ /**
665
+ * Update the batch - rebuilds vertex buffer if quads changed
666
+ */
667
+ update() {
668
+ if (!this.isDirty || this.quads.length === 0) {
669
+ return;
670
+ }
671
+ let vertexIndex = 0;
672
+ for (const quad of this.quads) {
673
+ const vertices = this.generateQuadVertices(quad);
674
+ for (const vertex of vertices) {
675
+ this.vertexData[vertexIndex++] = vertex[0];
676
+ this.vertexData[vertexIndex++] = vertex[1];
677
+ this.vertexData[vertexIndex++] = vertex[2];
678
+ this.vertexData[vertexIndex++] = vertex[3];
679
+ this.vertexData[vertexIndex++] = vertex[4];
680
+ }
681
+ }
682
+ if (this.vertexBuffer) {
683
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
684
+ this.gl.bufferSubData(
685
+ this.gl.ARRAY_BUFFER,
686
+ 0,
687
+ this.vertexData.subarray(0, vertexIndex)
688
+ );
689
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
690
+ }
691
+ this.isDirty = false;
692
+ }
693
+ /**
694
+ * Render the batch
695
+ * @param camera Optional camera for view transform (defaults to identity matrix)
696
+ */
697
+ render(camera) {
698
+ if (this.quads.length === 0) {
699
+ return;
700
+ }
701
+ this.update();
702
+ this.shader.use();
703
+ if (this.vertexBuffer) {
704
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
705
+ const posAttr = this.shader.getAttributeLocation("aPosition");
706
+ const texCoordAttr = this.shader.getAttributeLocation("aTexCoord");
707
+ if (posAttr !== -1) {
708
+ this.gl.enableVertexAttribArray(posAttr);
709
+ this.gl.vertexAttribPointer(
710
+ posAttr,
711
+ 3,
712
+ // 3 floats (x, y, z)
713
+ this.gl.FLOAT,
714
+ false,
715
+ this.floatsPerVertex * 4,
716
+ // stride
717
+ 0
718
+ // offset
719
+ );
720
+ }
721
+ if (texCoordAttr !== -1) {
722
+ this.gl.enableVertexAttribArray(texCoordAttr);
723
+ this.gl.vertexAttribPointer(
724
+ texCoordAttr,
725
+ 2,
726
+ // 2 floats (u, v)
727
+ this.gl.FLOAT,
728
+ false,
729
+ this.floatsPerVertex * 4,
730
+ // stride
731
+ 3 * 4
732
+ // offset after position
733
+ );
734
+ }
735
+ if (this.texture) {
736
+ this.texture.bind(0);
737
+ const textureUniform = this.shader.getUniformLocation("uTexture");
738
+ if (textureUniform !== null) {
739
+ this.gl.uniform1i(textureUniform, 0);
740
+ }
741
+ }
742
+ const matrixUniform = this.shader.getUniformLocation("uMatrix");
743
+ if (matrixUniform !== null) {
744
+ const matrix = camera ? camera.getViewMatrix() : new Float32Array([
745
+ 1,
746
+ 0,
747
+ 0,
748
+ 0,
749
+ 0,
750
+ 1,
751
+ 0,
752
+ 0,
753
+ 0,
754
+ 0,
755
+ 1,
756
+ 0,
757
+ 0,
758
+ 0,
759
+ 0,
760
+ 1
761
+ ]);
762
+ this.gl.uniformMatrix4fv(matrixUniform, false, matrix);
763
+ }
764
+ const vertexCount = this.quads.length * this.verticesPerQuad;
765
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, vertexCount);
766
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
767
+ }
768
+ }
769
+ /**
770
+ * Generate vertices for a quad with rotation applied
771
+ * Returns 6 vertices (2 triangles)
772
+ * @private
773
+ */
774
+ generateQuadVertices(quad) {
775
+ const { x, y, width, height, rotation } = quad;
776
+ const halfW = width / 2;
777
+ const halfH = height / 2;
778
+ const cos = Math.cos(rotation);
779
+ const sin = Math.sin(rotation);
780
+ const rotatePoint = (px, py) => {
781
+ return [px * cos - py * sin, px * sin + py * cos];
782
+ };
783
+ const corners = [
784
+ [-halfW, -halfH],
785
+ // bottom-left
786
+ [halfW, -halfH],
787
+ // bottom-right
788
+ [halfW, halfH],
789
+ // top-right
790
+ [halfW, halfH],
791
+ // top-right (duplicate)
792
+ [-halfW, halfH],
793
+ // top-left
794
+ [-halfW, -halfH]
795
+ // bottom-left (duplicate)
796
+ ];
797
+ const texCoords = [
798
+ [0, 0],
799
+ // bottom-left
800
+ [1, 0],
801
+ // bottom-right
802
+ [1, 1],
803
+ // top-right
804
+ [1, 1],
805
+ // top-right
806
+ [0, 1],
807
+ // top-left
808
+ [0, 0]
809
+ // bottom-left
810
+ ];
811
+ const vertices = [];
812
+ for (let i = 0; i < corners.length; i++) {
813
+ const [localX, localY] = corners[i];
814
+ const [rotX, rotY] = rotatePoint(localX, localY);
815
+ const [u, v] = texCoords[i];
816
+ vertices.push([x + rotX, y + rotY, 0, u, v]);
817
+ }
818
+ return vertices;
819
+ }
820
+ /**
821
+ * Dispose resources
822
+ */
823
+ dispose() {
824
+ if (this.vertexBuffer) {
825
+ this.gl.deleteBuffer(this.vertexBuffer);
826
+ this.vertexBuffer = null;
827
+ }
828
+ }
829
+ }
830
+ class SpriteBatchRenderer {
831
+ /**
832
+ * Create a new sprite batch renderer (V2)
833
+ * @param gl WebGL rendering context
834
+ * @param shader Shader program to use (should be SHADERS_V2)
835
+ * @param maxQuads Maximum number of quads to batch (default 1000)
836
+ */
837
+ constructor(gl, shader, maxQuads = 1e3) {
838
+ this.vertexBuffer = null;
839
+ this.quads = [];
840
+ this.isDirty = false;
841
+ this.verticesPerQuad = 6;
842
+ this.floatsPerVertex = 10;
843
+ this.texture = null;
844
+ this.depthTestEnabled = true;
845
+ this.gl = gl;
846
+ this.shader = shader;
847
+ this.maxQuads = maxQuads;
848
+ const totalFloats = maxQuads * this.verticesPerQuad * this.floatsPerVertex;
849
+ this.vertexData = new Float32Array(totalFloats);
850
+ const buf = gl.createBuffer();
851
+ if (!buf) {
852
+ throw new Error("Failed to create vertex buffer");
853
+ }
854
+ this.vertexBuffer = buf;
855
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
856
+ gl.bufferData(gl.ARRAY_BUFFER, this.vertexData.byteLength, gl.DYNAMIC_DRAW);
857
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
858
+ }
859
+ /**
860
+ * Set the texture for batch rendering
861
+ * @param texture The texture to use when rendering
862
+ */
863
+ setTexture(texture) {
864
+ this.texture = texture;
865
+ }
866
+ /**
867
+ * Add a sprite quad to the batch
868
+ * @param quad Sprite quad instance to add
869
+ */
870
+ addQuad(quad) {
871
+ if (this.quads.length >= this.maxQuads) {
872
+ console.warn(`Sprite batch renderer at max capacity (${this.maxQuads})`);
873
+ return;
874
+ }
875
+ this.quads.push(quad);
876
+ this.isDirty = true;
877
+ }
878
+ /**
879
+ * Clear all quads from the batch
880
+ */
881
+ clear() {
882
+ this.quads = [];
883
+ this.isDirty = true;
884
+ }
885
+ /**
886
+ * Get number of quads currently in batch
887
+ */
888
+ getQuadCount() {
889
+ return this.quads.length;
890
+ }
891
+ /**
892
+ * Update the batch - rebuilds vertex buffer if quads changed
893
+ */
894
+ update() {
895
+ if (!this.isDirty || this.quads.length === 0) {
896
+ return;
897
+ }
898
+ let vertexIndex = 0;
899
+ for (const quad of this.quads) {
900
+ const {
901
+ x,
902
+ y,
903
+ z = 0,
904
+ width,
905
+ height,
906
+ rotation,
907
+ color = { r: 1, g: 1, b: 1, a: 1 },
908
+ uvRect = { uMin: 0, vMin: 0, uMax: 1, vMax: 1 },
909
+ texIndex = 0
910
+ } = quad;
911
+ const vertices = this.generateQuadVertices({
912
+ x,
913
+ y,
914
+ z,
915
+ width,
916
+ height,
917
+ rotation,
918
+ color,
919
+ uvRect,
920
+ texIndex
921
+ });
922
+ for (const vertex of vertices) {
923
+ this.vertexData[vertexIndex++] = vertex.x;
924
+ this.vertexData[vertexIndex++] = vertex.y;
925
+ this.vertexData[vertexIndex++] = vertex.z;
926
+ this.vertexData[vertexIndex++] = vertex.u;
927
+ this.vertexData[vertexIndex++] = vertex.v;
928
+ this.vertexData[vertexIndex++] = vertex.r;
929
+ this.vertexData[vertexIndex++] = vertex.g;
930
+ this.vertexData[vertexIndex++] = vertex.b;
931
+ this.vertexData[vertexIndex++] = vertex.a;
932
+ this.vertexData[vertexIndex++] = vertex.texIndex;
933
+ }
934
+ }
935
+ if (this.vertexBuffer) {
936
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
937
+ this.gl.bufferSubData(
938
+ this.gl.ARRAY_BUFFER,
939
+ 0,
940
+ this.vertexData.subarray(0, vertexIndex)
941
+ );
942
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
943
+ }
944
+ this.isDirty = false;
945
+ }
946
+ /**
947
+ * Set whether depth testing is enabled
948
+ * When enabled, sprites with lower Z values appear behind sprites with higher Z values
949
+ * @param enabled Whether to enable depth testing (default true)
950
+ */
951
+ setDepthTestEnabled(enabled) {
952
+ this.depthTestEnabled = enabled;
953
+ }
954
+ /**
955
+ * Render the batch
956
+ * @param camera Optional camera for view transform (defaults to identity matrix)
957
+ */
958
+ render(camera) {
959
+ if (this.quads.length === 0) {
960
+ return;
961
+ }
962
+ this.update();
963
+ this.shader.use();
964
+ if (this.depthTestEnabled) {
965
+ this.gl.enable(this.gl.DEPTH_TEST);
966
+ this.gl.depthFunc(this.gl.LEQUAL);
967
+ } else {
968
+ this.gl.disable(this.gl.DEPTH_TEST);
969
+ }
970
+ if (this.vertexBuffer) {
971
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
972
+ const posAttr = this.shader.getAttributeLocation("aPosition");
973
+ const texCoordAttr = this.shader.getAttributeLocation("aTexCoord");
974
+ const colorAttr = this.shader.getAttributeLocation("aColor");
975
+ const texIndexAttr = this.shader.getAttributeLocation("aTexIndex");
976
+ const stride = this.floatsPerVertex * 4;
977
+ if (posAttr !== -1) {
978
+ this.gl.enableVertexAttribArray(posAttr);
979
+ this.gl.vertexAttribPointer(
980
+ posAttr,
981
+ 3,
982
+ // 3 floats (x, y, z)
983
+ this.gl.FLOAT,
984
+ false,
985
+ stride,
986
+ 0
987
+ // offset
988
+ );
989
+ }
990
+ if (texCoordAttr !== -1) {
991
+ this.gl.enableVertexAttribArray(texCoordAttr);
992
+ this.gl.vertexAttribPointer(
993
+ texCoordAttr,
994
+ 2,
995
+ // 2 floats (u, v)
996
+ this.gl.FLOAT,
997
+ false,
998
+ stride,
999
+ 3 * 4
1000
+ // offset after position
1001
+ );
1002
+ }
1003
+ if (colorAttr !== -1) {
1004
+ this.gl.enableVertexAttribArray(colorAttr);
1005
+ this.gl.vertexAttribPointer(
1006
+ colorAttr,
1007
+ 4,
1008
+ // 4 floats (r, g, b, a)
1009
+ this.gl.FLOAT,
1010
+ false,
1011
+ stride,
1012
+ 5 * 4
1013
+ // offset after texCoord
1014
+ );
1015
+ }
1016
+ if (texIndexAttr !== -1) {
1017
+ this.gl.enableVertexAttribArray(texIndexAttr);
1018
+ this.gl.vertexAttribPointer(
1019
+ texIndexAttr,
1020
+ 1,
1021
+ // 1 float (texIndex)
1022
+ this.gl.FLOAT,
1023
+ false,
1024
+ stride,
1025
+ 9 * 4
1026
+ // offset after color
1027
+ );
1028
+ }
1029
+ if (this.texture) {
1030
+ this.texture.bind(0);
1031
+ const textureUniform = this.shader.getUniformLocation("uTexture");
1032
+ if (textureUniform !== null) {
1033
+ this.gl.uniform1i(textureUniform, 0);
1034
+ }
1035
+ }
1036
+ const matrixUniform = this.shader.getUniformLocation("uMatrix");
1037
+ if (matrixUniform !== null) {
1038
+ const matrix = camera ? camera.getViewMatrix() : new Float32Array([
1039
+ 1,
1040
+ 0,
1041
+ 0,
1042
+ 0,
1043
+ 0,
1044
+ 1,
1045
+ 0,
1046
+ 0,
1047
+ 0,
1048
+ 0,
1049
+ 1,
1050
+ 0,
1051
+ 0,
1052
+ 0,
1053
+ 0,
1054
+ 1
1055
+ ]);
1056
+ this.gl.uniformMatrix4fv(matrixUniform, false, matrix);
1057
+ }
1058
+ const vertexCount = this.quads.length * this.verticesPerQuad;
1059
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, vertexCount);
1060
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
1061
+ }
1062
+ }
1063
+ /**
1064
+ * Generate vertices for a quad with rotation applied
1065
+ * Returns 6 vertices (2 triangles)
1066
+ * @private
1067
+ */
1068
+ generateQuadVertices(instance) {
1069
+ const { x, y, z, width, height, rotation, color, uvRect, texIndex } = instance;
1070
+ const halfW = width / 2;
1071
+ const halfH = height / 2;
1072
+ const cos = Math.cos(rotation);
1073
+ const sin = Math.sin(rotation);
1074
+ const rotatePoint = (px, py) => {
1075
+ return [px * cos - py * sin, px * sin + py * cos];
1076
+ };
1077
+ const corners = [
1078
+ [-halfW, -halfH],
1079
+ // bottom-left
1080
+ [halfW, -halfH],
1081
+ // bottom-right
1082
+ [halfW, halfH],
1083
+ // top-right
1084
+ [halfW, halfH],
1085
+ // top-right (duplicate)
1086
+ [-halfW, halfH],
1087
+ // top-left
1088
+ [-halfW, -halfH]
1089
+ // bottom-left (duplicate)
1090
+ ];
1091
+ const texCoords = [
1092
+ [uvRect.uMin, uvRect.vMin],
1093
+ // bottom-left
1094
+ [uvRect.uMax, uvRect.vMin],
1095
+ // bottom-right
1096
+ [uvRect.uMax, uvRect.vMax],
1097
+ // top-right
1098
+ [uvRect.uMax, uvRect.vMax],
1099
+ // top-right
1100
+ [uvRect.uMin, uvRect.vMax],
1101
+ // top-left
1102
+ [uvRect.uMin, uvRect.vMin]
1103
+ // bottom-left
1104
+ ];
1105
+ const vertices = [];
1106
+ for (let i = 0; i < corners.length; i++) {
1107
+ const [localX, localY] = corners[i];
1108
+ const [rotX, rotY] = rotatePoint(localX, localY);
1109
+ const [u, v] = texCoords[i];
1110
+ vertices.push({
1111
+ x: x + rotX,
1112
+ y: y + rotY,
1113
+ z,
1114
+ u,
1115
+ v,
1116
+ r: color.r,
1117
+ g: color.g,
1118
+ b: color.b,
1119
+ a: color.a,
1120
+ texIndex
1121
+ });
1122
+ }
1123
+ return vertices;
1124
+ }
1125
+ /**
1126
+ * Dispose resources
1127
+ */
1128
+ dispose() {
1129
+ if (this.vertexBuffer) {
1130
+ this.gl.deleteBuffer(this.vertexBuffer);
1131
+ this.vertexBuffer = null;
1132
+ }
1133
+ }
1134
+ }
1135
+ class Matrix4 {
1136
+ /**
1137
+ * Create an identity matrix
1138
+ * @returns 4x4 identity matrix in column-major order
1139
+ */
1140
+ static identity() {
1141
+ return new Float32Array([
1142
+ 1,
1143
+ 0,
1144
+ 0,
1145
+ 0,
1146
+ // column 0
1147
+ 0,
1148
+ 1,
1149
+ 0,
1150
+ 0,
1151
+ // column 1
1152
+ 0,
1153
+ 0,
1154
+ 1,
1155
+ 0,
1156
+ // column 2
1157
+ 0,
1158
+ 0,
1159
+ 0,
1160
+ 1
1161
+ // column 3
1162
+ ]);
1163
+ }
1164
+ /**
1165
+ * Create a translation matrix
1166
+ * @param x Translation along X axis
1167
+ * @param y Translation along Y axis
1168
+ * @param z Translation along Z axis (default 0)
1169
+ * @returns 4x4 translation matrix in column-major order
1170
+ */
1171
+ static translation(x, y, z = 0) {
1172
+ return new Float32Array([
1173
+ 1,
1174
+ 0,
1175
+ 0,
1176
+ 0,
1177
+ // column 0
1178
+ 0,
1179
+ 1,
1180
+ 0,
1181
+ 0,
1182
+ // column 1
1183
+ 0,
1184
+ 0,
1185
+ 1,
1186
+ 0,
1187
+ // column 2
1188
+ x,
1189
+ y,
1190
+ z,
1191
+ 1
1192
+ // column 3
1193
+ ]);
1194
+ }
1195
+ /**
1196
+ * Create a scale matrix
1197
+ * @param x Scale factor along X axis
1198
+ * @param y Scale factor along Y axis
1199
+ * @param z Scale factor along Z axis (default 1)
1200
+ * @returns 4x4 scale matrix in column-major order
1201
+ */
1202
+ static scale(x, y, z = 1) {
1203
+ return new Float32Array([
1204
+ x,
1205
+ 0,
1206
+ 0,
1207
+ 0,
1208
+ // column 0
1209
+ 0,
1210
+ y,
1211
+ 0,
1212
+ 0,
1213
+ // column 1
1214
+ 0,
1215
+ 0,
1216
+ z,
1217
+ 0,
1218
+ // column 2
1219
+ 0,
1220
+ 0,
1221
+ 0,
1222
+ 1
1223
+ // column 3
1224
+ ]);
1225
+ }
1226
+ /**
1227
+ * Multiply two matrices (result = a * b)
1228
+ * @param a First matrix (left operand)
1229
+ * @param b Second matrix (right operand)
1230
+ * @returns Result of matrix multiplication in column-major order
1231
+ */
1232
+ static multiply(a, b) {
1233
+ const result = new Float32Array(16);
1234
+ for (let col = 0; col < 4; col++) {
1235
+ for (let row = 0; row < 4; row++) {
1236
+ let sum = 0;
1237
+ for (let k = 0; k < 4; k++) {
1238
+ sum += a[k * 4 + row] * b[col * 4 + k];
1239
+ }
1240
+ result[col * 4 + row] = sum;
1241
+ }
1242
+ }
1243
+ return result;
1244
+ }
1245
+ /**
1246
+ * Create a view matrix from camera position and zoom
1247
+ * The view matrix transforms world coordinates to camera/eye coordinates
1248
+ *
1249
+ * View = Translation(-cameraX, -cameraY, 0) * Scale(zoom, zoom, 1)
1250
+ *
1251
+ * @param x Camera X position (translation will be negative)
1252
+ * @param y Camera Y position (translation will be negative)
1253
+ * @param zoom Camera zoom level (1.0 = no zoom, >1 = zoom in, <1 = zoom out)
1254
+ * @returns 4x4 view matrix in column-major order
1255
+ */
1256
+ static createViewMatrix(x, y, zoom) {
1257
+ const translation = Matrix4.translation(-x, -y, 0);
1258
+ const scale = Matrix4.scale(zoom, zoom, 1);
1259
+ return Matrix4.multiply(translation, scale);
1260
+ }
1261
+ }
1262
+ class Camera {
1263
+ /**
1264
+ * Create a new camera
1265
+ * @param x Initial X position (default 0)
1266
+ * @param y Initial Y position (default 0)
1267
+ * @param zoom Initial zoom level (default 1.0)
1268
+ */
1269
+ constructor(x = 0, y = 0, zoom = 1) {
1270
+ this._viewMatrix = null;
1271
+ this._viewMatrixDirty = true;
1272
+ this._x = x;
1273
+ this._y = y;
1274
+ this._zoom = zoom;
1275
+ }
1276
+ /**
1277
+ * Get the camera X position
1278
+ */
1279
+ get x() {
1280
+ return this._x;
1281
+ }
1282
+ /**
1283
+ * Set the camera X position
1284
+ */
1285
+ set x(value) {
1286
+ this._x = value;
1287
+ this._viewMatrixDirty = true;
1288
+ }
1289
+ /**
1290
+ * Get the camera Y position
1291
+ */
1292
+ get y() {
1293
+ return this._y;
1294
+ }
1295
+ /**
1296
+ * Set the camera Y position
1297
+ */
1298
+ set y(value) {
1299
+ this._y = value;
1300
+ this._viewMatrixDirty = true;
1301
+ }
1302
+ /**
1303
+ * Get the camera zoom level
1304
+ */
1305
+ get zoom() {
1306
+ return this._zoom;
1307
+ }
1308
+ /**
1309
+ * Set the camera zoom level
1310
+ * Values: 1.0 = no zoom, >1 = zoom in, <1 = zoom out
1311
+ */
1312
+ set zoom(value) {
1313
+ this._zoom = Math.max(1e-3, value);
1314
+ this._viewMatrixDirty = true;
1315
+ }
1316
+ /**
1317
+ * Set both X and Y position at once
1318
+ * @param x New X position
1319
+ * @param y New Y position
1320
+ */
1321
+ setPosition(x, y) {
1322
+ this._x = x;
1323
+ this._y = y;
1324
+ this._viewMatrixDirty = true;
1325
+ }
1326
+ /**
1327
+ * Move the camera by a relative offset
1328
+ * @param dx X offset to add to current position
1329
+ * @param dy Y offset to add to current position
1330
+ */
1331
+ move(dx, dy) {
1332
+ this._x += dx;
1333
+ this._y += dy;
1334
+ this._viewMatrixDirty = true;
1335
+ }
1336
+ /**
1337
+ * Scale the zoom by a factor
1338
+ * @param factor Multiplier for current zoom (e.g., 1.1 to zoom in 10%)
1339
+ */
1340
+ zoomBy(factor) {
1341
+ this._zoom = Math.max(1e-3, this._zoom * factor);
1342
+ this._viewMatrixDirty = true;
1343
+ }
1344
+ /**
1345
+ * Reset camera to default position and zoom
1346
+ */
1347
+ reset() {
1348
+ this._x = 0;
1349
+ this._y = 0;
1350
+ this._zoom = 1;
1351
+ this._viewMatrixDirty = true;
1352
+ }
1353
+ /**
1354
+ * Get the view matrix for this camera
1355
+ * The view matrix transforms world coordinates to camera space
1356
+ * Caches the result until camera properties change
1357
+ *
1358
+ * @returns 4x4 view matrix in column-major order
1359
+ */
1360
+ getViewMatrix() {
1361
+ if (this._viewMatrixDirty || this._viewMatrix === null) {
1362
+ this._viewMatrix = Matrix4.createViewMatrix(this._x, this._y, this._zoom);
1363
+ this._viewMatrixDirty = false;
1364
+ }
1365
+ return this._viewMatrix;
1366
+ }
1367
+ /**
1368
+ * Convert screen coordinates to world coordinates
1369
+ * Useful for mouse picking and interaction
1370
+ *
1371
+ * @param screenX Screen X coordinate (pixels)
1372
+ * @param screenY Screen Y coordinate (pixels)
1373
+ * @param viewportWidth Viewport width in pixels
1374
+ * @param viewportHeight Viewport height in pixels
1375
+ * @returns World coordinates {x, y}
1376
+ */
1377
+ screenToWorld(screenX, screenY, viewportWidth, viewportHeight) {
1378
+ const centeredX = screenX - viewportWidth / 2;
1379
+ const centeredY = screenY - viewportHeight / 2;
1380
+ const worldX = centeredX / this._zoom + this._x;
1381
+ const worldY = centeredY / this._zoom + this._y;
1382
+ return { x: worldX, y: worldY };
1383
+ }
1384
+ /**
1385
+ * Convert world coordinates to screen coordinates
1386
+ * Useful for UI positioning and debug rendering
1387
+ *
1388
+ * @param worldX World X coordinate
1389
+ * @param worldY World Y coordinate
1390
+ * @param viewportWidth Viewport width in pixels
1391
+ * @param viewportHeight Viewport height in pixels
1392
+ * @returns Screen coordinates {x, y} in pixels
1393
+ */
1394
+ worldToScreen(worldX, worldY, viewportWidth, viewportHeight) {
1395
+ const centeredX = (worldX - this._x) * this._zoom;
1396
+ const centeredY = (worldY - this._y) * this._zoom;
1397
+ const screenX = centeredX + viewportWidth / 2;
1398
+ const screenY = centeredY + viewportHeight / 2;
1399
+ return { x: screenX, y: screenY };
1400
+ }
1401
+ }
1402
+ class BrowserResourceLoader {
1403
+ /**
1404
+ * Create a new browser resource loader
1405
+ * @param baseUrl Optional base URL for resolving relative paths (defaults to current origin)
1406
+ * @param timeout Default timeout for requests in milliseconds (default: 10000)
1407
+ */
1408
+ constructor(baseUrl = "", timeout = 1e4) {
1409
+ this.baseUrl = baseUrl || this.getCurrentOrigin();
1410
+ this.defaultTimeout = timeout;
1411
+ }
1412
+ /**
1413
+ * Get the current origin (protocol + host + port)
1414
+ */
1415
+ getCurrentOrigin() {
1416
+ return typeof window !== "undefined" ? window.location.origin : "http://localhost";
1417
+ }
1418
+ /**
1419
+ * Resolve a relative path against the base URL
1420
+ * @param path Relative or absolute path
1421
+ * @returns Resolved absolute URL
1422
+ */
1423
+ resolvePath(path2) {
1424
+ try {
1425
+ if (path2.startsWith("http://") || path2.startsWith("https://")) {
1426
+ return path2;
1427
+ }
1428
+ if (path2.startsWith("//")) {
1429
+ return window.location.protocol + path2;
1430
+ }
1431
+ if (path2.startsWith("/")) {
1432
+ return this.baseUrl + path2;
1433
+ }
1434
+ return `${this.baseUrl}/${path2}`;
1435
+ } catch {
1436
+ return path2;
1437
+ }
1438
+ }
1439
+ /**
1440
+ * Load a single resource from a URL
1441
+ * @param path URL or relative path to the resource
1442
+ * @param options Optional loading configuration
1443
+ * @returns Promise resolving to the resource content
1444
+ */
1445
+ async load(path2, options) {
1446
+ const url = this.resolvePath(path2);
1447
+ try {
1448
+ const fetchOptions = {
1449
+ credentials: options?.credentials || "same-origin"
1450
+ };
1451
+ if (options?.headers) {
1452
+ fetchOptions.headers = options.headers;
1453
+ }
1454
+ const controller = new AbortController();
1455
+ const timeoutId = setTimeout(() => controller.abort(), this.defaultTimeout);
1456
+ fetchOptions.signal = controller.signal;
1457
+ const response = await fetch(url, fetchOptions);
1458
+ clearTimeout(timeoutId);
1459
+ if (!response.ok) {
1460
+ throw new Error(
1461
+ `HTTP ${response.status}: ${response.statusText} for URL: ${url}`
1462
+ );
1463
+ }
1464
+ const text = await response.text();
1465
+ return text;
1466
+ } catch (error) {
1467
+ if (error instanceof Error) {
1468
+ if (error.name === "AbortError") {
1469
+ throw new Error(
1470
+ `Request timeout after ${this.defaultTimeout}ms for URL: ${url}`
1471
+ );
1472
+ }
1473
+ throw new Error(`Failed to load resource from ${url}: ${error.message}`);
1474
+ }
1475
+ throw new Error(`Failed to load resource from ${url}: Unknown error`);
1476
+ }
1477
+ }
1478
+ /**
1479
+ * Load multiple resources in parallel
1480
+ * @param paths Array of URLs or paths
1481
+ * @param options Optional loading configuration
1482
+ * @returns Promise resolving to array of load results
1483
+ */
1484
+ async loadMultiple(paths, options) {
1485
+ const promises = paths.map(async (path2) => {
1486
+ try {
1487
+ const data = await this.load(path2, options);
1488
+ return {
1489
+ data,
1490
+ path: path2,
1491
+ success: true
1492
+ };
1493
+ } catch (error) {
1494
+ return {
1495
+ data: "",
1496
+ path: path2,
1497
+ success: false,
1498
+ error: error instanceof Error ? error.message : String(error)
1499
+ };
1500
+ }
1501
+ });
1502
+ return Promise.all(promises);
1503
+ }
1504
+ /**
1505
+ * Check if the path is valid for loading in the browser
1506
+ * @param path URL or path to check
1507
+ * @returns true if the path can be loaded
1508
+ */
1509
+ canLoad(path2) {
1510
+ const validPatterns = [
1511
+ /^https?:\/\//i,
1512
+ // Absolute HTTP(S) URLs
1513
+ /^\/\//,
1514
+ // Protocol-relative URLs
1515
+ /^\//,
1516
+ // Absolute paths
1517
+ /^\.\.?\//
1518
+ // Relative paths starting with ./ or ../
1519
+ ];
1520
+ const hasFileExtension = /\.[a-z0-9]+$/i.test(path2);
1521
+ return validPatterns.some((pattern) => pattern.test(path2)) || hasFileExtension;
1522
+ }
1523
+ /**
1524
+ * Set a new base URL for resolving relative paths
1525
+ * @param baseUrl New base URL
1526
+ */
1527
+ setBaseUrl(baseUrl) {
1528
+ this.baseUrl = baseUrl;
1529
+ }
1530
+ /**
1531
+ * Get the current base URL
1532
+ * @returns Current base URL
1533
+ */
1534
+ getBaseUrl() {
1535
+ return this.baseUrl;
1536
+ }
1537
+ /**
1538
+ * Set the default request timeout
1539
+ * @param timeout Timeout in milliseconds
1540
+ */
1541
+ setTimeout(timeout) {
1542
+ this.defaultTimeout = timeout;
1543
+ }
1544
+ }
1545
+ const browserResourceLoader = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1546
+ __proto__: null,
1547
+ BrowserResourceLoader
1548
+ }, Symbol.toStringTag, { value: "Module" }));
1549
+ class NodeResourceLoader {
1550
+ /**
1551
+ * Create a new Node.js resource loader
1552
+ * @param baseDir Optional base directory for resolving relative paths (defaults to current working directory)
1553
+ */
1554
+ constructor(baseDir = process.cwd()) {
1555
+ this.baseDir = baseDir;
1556
+ }
1557
+ /**
1558
+ * Resolve a relative path against the base directory
1559
+ * @param filePath Relative or absolute file path
1560
+ * @returns Resolved absolute file path
1561
+ */
1562
+ resolvePath(filePath) {
1563
+ if (path.isAbsolute(filePath)) {
1564
+ return path.normalize(filePath);
1565
+ }
1566
+ return path.normalize(path.join(this.baseDir, filePath));
1567
+ }
1568
+ /**
1569
+ * Load a single resource from a file
1570
+ * @param filePath File path (relative or absolute)
1571
+ * @param options Optional loading configuration
1572
+ * @returns Promise resolving to the file content
1573
+ */
1574
+ async load(filePath, options) {
1575
+ const resolvedPath = this.resolvePath(filePath);
1576
+ const encoding = options?.encoding || "utf-8";
1577
+ try {
1578
+ const content = await fs.readFile(resolvedPath, encoding);
1579
+ return content;
1580
+ } catch (error) {
1581
+ if (error instanceof Error) {
1582
+ const errorCode = error.code;
1583
+ if (errorCode === "ENOENT") {
1584
+ throw new Error(
1585
+ `File not found: ${resolvedPath} (resolved from: ${filePath})`
1586
+ );
1587
+ }
1588
+ if (errorCode === "EACCES") {
1589
+ throw new Error(
1590
+ `Permission denied reading file: ${resolvedPath}`
1591
+ );
1592
+ }
1593
+ if (errorCode === "EISDIR") {
1594
+ throw new Error(
1595
+ `Path is a directory, not a file: ${resolvedPath}`
1596
+ );
1597
+ }
1598
+ throw new Error(
1599
+ `Failed to load resource from ${resolvedPath}: ${error.message}`
1600
+ );
1601
+ }
1602
+ throw new Error(
1603
+ `Failed to load resource from ${resolvedPath}: Unknown error`
1604
+ );
1605
+ }
1606
+ }
1607
+ /**
1608
+ * Load multiple resources in parallel
1609
+ * @param filePaths Array of file paths
1610
+ * @param options Optional loading configuration
1611
+ * @returns Promise resolving to array of load results
1612
+ */
1613
+ async loadMultiple(filePaths, options) {
1614
+ const promises = filePaths.map(async (filePath) => {
1615
+ try {
1616
+ const data = await this.load(filePath, options);
1617
+ return {
1618
+ data,
1619
+ path: filePath,
1620
+ success: true
1621
+ };
1622
+ } catch (error) {
1623
+ return {
1624
+ data: "",
1625
+ path: filePath,
1626
+ success: false,
1627
+ error: error instanceof Error ? error.message : String(error)
1628
+ };
1629
+ }
1630
+ });
1631
+ return Promise.all(promises);
1632
+ }
1633
+ /**
1634
+ * Check if the path is valid for loading in Node.js
1635
+ * @param filePath File path to check
1636
+ * @returns true if the path can be loaded
1637
+ */
1638
+ canLoad(filePath) {
1639
+ const validPatterns = [
1640
+ /^\//,
1641
+ // Unix absolute paths
1642
+ /^[a-zA-Z]:/,
1643
+ // Windows absolute paths (e.g., C:\)
1644
+ /^\.\.?\//,
1645
+ // Relative paths starting with ./ or ../
1646
+ /^[^/\\]+\//
1647
+ // Relative paths without explicit prefix (e.g., "shaders/")
1648
+ ];
1649
+ return validPatterns.some((pattern) => pattern.test(filePath));
1650
+ }
1651
+ /**
1652
+ * Check if a file exists without loading it
1653
+ * @param filePath File path to check
1654
+ * @returns Promise resolving to true if file exists
1655
+ */
1656
+ async exists(filePath) {
1657
+ const resolvedPath = this.resolvePath(filePath);
1658
+ try {
1659
+ await fs.access(resolvedPath, fs.constants.F_OK);
1660
+ return true;
1661
+ } catch {
1662
+ return false;
1663
+ }
1664
+ }
1665
+ /**
1666
+ * Get file statistics (size, modification time, etc.)
1667
+ * @param filePath File path to check
1668
+ * @returns Promise resolving to file stats
1669
+ */
1670
+ async getStats(filePath) {
1671
+ const resolvedPath = this.resolvePath(filePath);
1672
+ return fs.stat(resolvedPath);
1673
+ }
1674
+ /**
1675
+ * Set a new base directory for resolving relative paths
1676
+ * @param baseDir New base directory
1677
+ */
1678
+ setBaseDir(baseDir) {
1679
+ this.baseDir = baseDir;
1680
+ }
1681
+ /**
1682
+ * Get the current base directory
1683
+ * @returns Current base directory
1684
+ */
1685
+ getBaseDir() {
1686
+ return this.baseDir;
1687
+ }
1688
+ /**
1689
+ * List all files in a directory
1690
+ * @param dirPath Directory path to list
1691
+ * @param recursive Whether to recursively list subdirectories (default: false)
1692
+ * @returns Promise resolving to array of file paths
1693
+ */
1694
+ async listDirectory(dirPath, recursive = false) {
1695
+ const resolvedPath = this.resolvePath(dirPath);
1696
+ const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
1697
+ const files = [];
1698
+ for (const entry of entries) {
1699
+ const fullPath = path.join(resolvedPath, entry.name);
1700
+ if (entry.isDirectory() && recursive) {
1701
+ const subFiles = await this.listDirectory(fullPath, true);
1702
+ files.push(...subFiles);
1703
+ } else if (entry.isFile()) {
1704
+ files.push(fullPath);
1705
+ }
1706
+ }
1707
+ return files;
1708
+ }
1709
+ }
1710
+ const nodeResourceLoader = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1711
+ __proto__: null,
1712
+ NodeResourceLoader
1713
+ }, Symbol.toStringTag, { value: "Module" }));
1714
+ var Environment = /* @__PURE__ */ ((Environment2) => {
1715
+ Environment2["BROWSER"] = "browser";
1716
+ Environment2["NODE"] = "node";
1717
+ Environment2["UNKNOWN"] = "unknown";
1718
+ return Environment2;
1719
+ })(Environment || {});
1720
+ class ResourceLoaderFactory {
1721
+ /**
1722
+ * Detect the current runtime environment
1723
+ * @returns The detected environment type
1724
+ */
1725
+ static detectEnvironment() {
1726
+ if (typeof window !== "undefined" && typeof window.document !== "undefined" && typeof fetch !== "undefined") {
1727
+ return "browser";
1728
+ }
1729
+ if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
1730
+ return "node";
1731
+ }
1732
+ return "unknown";
1733
+ }
1734
+ /**
1735
+ * Check if the current environment is a browser
1736
+ * @returns true if running in a browser
1737
+ */
1738
+ static isBrowser() {
1739
+ return this.detectEnvironment() === "browser";
1740
+ }
1741
+ /**
1742
+ * Check if the current environment is Node.js
1743
+ * @returns true if running in Node.js
1744
+ */
1745
+ static isNode() {
1746
+ return this.detectEnvironment() === "node";
1747
+ }
1748
+ /**
1749
+ * Create a resource loader for the current environment
1750
+ * @param options Optional factory configuration
1751
+ * @returns A resource loader instance appropriate for the current platform
1752
+ * @throws Error if the environment is not supported
1753
+ */
1754
+ static async create(options) {
1755
+ const environment = options?.forceEnvironment || this.detectEnvironment();
1756
+ switch (environment) {
1757
+ case "browser":
1758
+ return await this.createBrowserLoader(options);
1759
+ case "node":
1760
+ return await this.createNodeLoader(options);
1761
+ case "unknown":
1762
+ throw new Error(
1763
+ "Unsupported environment: Unable to determine runtime environment. Please specify forceEnvironment in options."
1764
+ );
1765
+ default:
1766
+ throw new Error(`Unsupported environment: ${environment}`);
1767
+ }
1768
+ }
1769
+ /**
1770
+ * Create a browser resource loader
1771
+ * @param options Optional factory configuration
1772
+ * @returns A browser resource loader instance
1773
+ */
1774
+ static async createBrowserLoader(options) {
1775
+ const { BrowserResourceLoader: Loader } = await Promise.resolve().then(() => browserResourceLoader);
1776
+ return new Loader(options?.baseUrl, options?.timeout);
1777
+ }
1778
+ /**
1779
+ * Create a Node.js resource loader
1780
+ * @param options Optional factory configuration
1781
+ * @returns A Node.js resource loader instance
1782
+ */
1783
+ static async createNodeLoader(options) {
1784
+ const { NodeResourceLoader: Loader } = await Promise.resolve().then(() => nodeResourceLoader);
1785
+ return new Loader(options?.baseDir);
1786
+ }
1787
+ /**
1788
+ * Create a resource loader with automatic fallback
1789
+ * If the preferred loader is not available, falls back to the available loader
1790
+ * @param preferredEnvironment Preferred environment
1791
+ * @param options Optional factory configuration
1792
+ * @returns A resource loader instance
1793
+ */
1794
+ static async createWithFallback(preferredEnvironment, options) {
1795
+ try {
1796
+ options = { ...options, forceEnvironment: preferredEnvironment };
1797
+ return await this.create(options);
1798
+ } catch {
1799
+ return await this.create({ ...options, forceEnvironment: void 0 });
1800
+ }
1801
+ }
1802
+ }
1803
+ async function createResourceLoader(options) {
1804
+ return await ResourceLoaderFactory.create(options);
1805
+ }
1806
+ const resourceLoaderFactory = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1807
+ __proto__: null,
1808
+ Environment,
1809
+ ResourceLoaderFactory,
1810
+ createResourceLoader
1811
+ }, Symbol.toStringTag, { value: "Module" }));
1812
+ class ResourceCache {
1813
+ constructor(enabled = true) {
1814
+ this.cache = /* @__PURE__ */ new Map();
1815
+ this.enabled = enabled;
1816
+ }
1817
+ get(key) {
1818
+ if (!this.enabled) return void 0;
1819
+ return this.cache.get(key);
1820
+ }
1821
+ set(key, value) {
1822
+ if (this.enabled) {
1823
+ this.cache.set(key, value);
1824
+ }
1825
+ }
1826
+ has(key) {
1827
+ if (!this.enabled) return false;
1828
+ return this.cache.has(key);
1829
+ }
1830
+ clear() {
1831
+ this.cache.clear();
1832
+ }
1833
+ size() {
1834
+ return this.cache.size;
1835
+ }
1836
+ enable() {
1837
+ this.enabled = true;
1838
+ }
1839
+ disable() {
1840
+ this.enabled = false;
1841
+ }
1842
+ }
1843
+ class ResourcePipeline {
1844
+ /**
1845
+ * Create a new resource loading pipeline
1846
+ * @param loader Resource loader instance
1847
+ * @param options Pipeline configuration options
1848
+ */
1849
+ constructor(loader, options) {
1850
+ this.loader = loader;
1851
+ this.concurrency = options?.concurrency ?? 10;
1852
+ this.cache = new ResourceCache(options?.cache ?? true);
1853
+ }
1854
+ /**
1855
+ * Load a single resource with caching support
1856
+ * @param path Resource path or URL
1857
+ * @param options Optional loading options
1858
+ * @returns Promise resolving to the resource content
1859
+ */
1860
+ async load(path2, options) {
1861
+ const cached = this.cache.get(path2);
1862
+ if (cached !== void 0) {
1863
+ return cached;
1864
+ }
1865
+ const content = await this.loader.load(path2, options);
1866
+ this.cache.set(path2, content);
1867
+ return content;
1868
+ }
1869
+ /**
1870
+ * Load multiple resources with concurrency control
1871
+ * @param paths Array of resource paths
1872
+ * @param options Optional loading options
1873
+ * @returns Promise resolving to batch load result
1874
+ */
1875
+ async loadBatch(paths, options) {
1876
+ const succeeded = /* @__PURE__ */ new Map();
1877
+ const failed = /* @__PURE__ */ new Map();
1878
+ for (let i = 0; i < paths.length; i += this.concurrency) {
1879
+ const batch = paths.slice(i, i + this.concurrency);
1880
+ const results = await this.loader.loadMultiple(batch, options);
1881
+ for (const result of results) {
1882
+ if (result.success) {
1883
+ succeeded.set(result.path, result.data);
1884
+ this.cache.set(result.path, result.data);
1885
+ } else {
1886
+ failed.set(result.path, result.error || "Unknown error");
1887
+ }
1888
+ }
1889
+ }
1890
+ return {
1891
+ succeeded,
1892
+ failed,
1893
+ total: paths.length,
1894
+ successCount: succeeded.size,
1895
+ failureCount: failed.size
1896
+ };
1897
+ }
1898
+ /**
1899
+ * Load a shader from separate vertex and fragment files
1900
+ * @param vertexPath Path to vertex shader file
1901
+ * @param fragmentPath Path to fragment shader file
1902
+ * @param options Optional loading options
1903
+ * @returns Promise resolving to shader source code
1904
+ */
1905
+ async loadShader(vertexPath, fragmentPath, options) {
1906
+ const [vertex, fragment] = await Promise.all([
1907
+ this.load(vertexPath, options),
1908
+ this.load(fragmentPath, options)
1909
+ ]);
1910
+ return { vertex, fragment };
1911
+ }
1912
+ /**
1913
+ * Load multiple shaders
1914
+ * @param shaders Array of shader definitions
1915
+ * @param options Optional loading options
1916
+ * @returns Promise resolving to array of named shader sources
1917
+ */
1918
+ async loadShaders(shaders, options) {
1919
+ const results = await Promise.all(
1920
+ shaders.map(async (shader) => {
1921
+ const source = await this.loadShader(
1922
+ shader.vertex,
1923
+ shader.fragment,
1924
+ options
1925
+ );
1926
+ return {
1927
+ name: shader.name,
1928
+ ...source
1929
+ };
1930
+ })
1931
+ );
1932
+ return results;
1933
+ }
1934
+ /**
1935
+ * Load resources from a manifest file
1936
+ * @param manifestPath Path to JSON manifest file
1937
+ * @param options Optional loading options
1938
+ * @returns Promise resolving to batch load result
1939
+ */
1940
+ async loadFromManifest(manifestPath, options) {
1941
+ const manifestContent = await this.load(manifestPath, options);
1942
+ const manifest = JSON.parse(manifestContent);
1943
+ return this.loadBatch(manifest.resources, options);
1944
+ }
1945
+ /**
1946
+ * Preload resources for faster access later
1947
+ * @param paths Array of resource paths to preload
1948
+ * @param options Optional loading options
1949
+ * @returns Promise resolving when all resources are loaded
1950
+ */
1951
+ async preload(paths, options) {
1952
+ await this.loadBatch(paths, options);
1953
+ }
1954
+ /**
1955
+ * Check if a resource is cached
1956
+ * @param path Resource path
1957
+ * @returns true if the resource is in the cache
1958
+ */
1959
+ isCached(path2) {
1960
+ return this.cache.has(path2);
1961
+ }
1962
+ /**
1963
+ * Get a resource from cache without loading
1964
+ * @param path Resource path
1965
+ * @returns Cached content or undefined if not cached
1966
+ */
1967
+ getCached(path2) {
1968
+ return this.cache.get(path2);
1969
+ }
1970
+ /**
1971
+ * Clear the resource cache
1972
+ */
1973
+ clearCache() {
1974
+ this.cache.clear();
1975
+ }
1976
+ /**
1977
+ * Get cache statistics
1978
+ * @returns Number of cached resources
1979
+ */
1980
+ getCacheSize() {
1981
+ return this.cache.size();
1982
+ }
1983
+ /**
1984
+ * Enable caching
1985
+ */
1986
+ enableCache() {
1987
+ this.cache.enable();
1988
+ }
1989
+ /**
1990
+ * Disable caching
1991
+ */
1992
+ disableCache() {
1993
+ this.cache.disable();
1994
+ }
1995
+ /**
1996
+ * Set the maximum concurrency for batch operations
1997
+ * @param concurrency Maximum concurrent loads
1998
+ */
1999
+ setConcurrency(concurrency) {
2000
+ this.concurrency = Math.max(1, concurrency);
2001
+ }
2002
+ /**
2003
+ * Get the underlying resource loader
2004
+ * @returns The resource loader instance
2005
+ */
2006
+ getLoader() {
2007
+ return this.loader;
2008
+ }
2009
+ }
2010
+ async function createResourcePipeline(options) {
2011
+ const { ResourceLoaderFactory: ResourceLoaderFactory2 } = await Promise.resolve().then(() => resourceLoaderFactory);
2012
+ const loader = await ResourceLoaderFactory2.create({
2013
+ baseUrl: options?.baseUrl,
2014
+ baseDir: options?.baseDir,
2015
+ timeout: options?.timeout
2016
+ });
2017
+ return new ResourcePipeline(loader, options);
2018
+ }
2019
+ const SCENE_CONFIG = {
2020
+ width: 800,
2021
+ height: 600
2022
+ };
2023
+ const GEOMETRY = {
2024
+ quad: {
2025
+ vertices: new Float32Array([
2026
+ // Position TexCoord
2027
+ -0.5,
2028
+ -0.5,
2029
+ 0,
2030
+ 0,
2031
+ 0,
2032
+ // Bottom-left
2033
+ 0.5,
2034
+ -0.5,
2035
+ 0,
2036
+ 1,
2037
+ 0,
2038
+ // Bottom-right
2039
+ 0.5,
2040
+ 0.5,
2041
+ 0,
2042
+ 1,
2043
+ 1,
2044
+ // Top-right
2045
+ 0.5,
2046
+ 0.5,
2047
+ 0,
2048
+ 1,
2049
+ 1,
2050
+ // Top-right
2051
+ -0.5,
2052
+ 0.5,
2053
+ 0,
2054
+ 0,
2055
+ 1,
2056
+ // Top-left
2057
+ -0.5,
2058
+ -0.5,
2059
+ 0,
2060
+ 0,
2061
+ 0
2062
+ // Bottom-left
2063
+ ]),
2064
+ stride: 5 * 4
2065
+ // 5 floats per vertex × 4 bytes
2066
+ }
2067
+ };
2068
+ const TEXTURE_CONFIG = {
2069
+ size: 256
2070
+ };
2071
+ const DEMO_CONFIG = {
2072
+ shaders: [
2073
+ {
2074
+ name: "basic",
2075
+ vertex: "resources/shaders/basic.vert",
2076
+ fragment: "resources/shaders/basic.frag"
2077
+ },
2078
+ {
2079
+ name: "glow",
2080
+ vertex: "resources/shaders/glow.vert",
2081
+ fragment: "resources/shaders/glow.frag"
2082
+ }
2083
+ ],
2084
+ // Additional resources to demonstrate batch loading
2085
+ resources: [
2086
+ "resources/shaders/basic.vert",
2087
+ "resources/shaders/basic.frag",
2088
+ "resources/shaders/glow.vert",
2089
+ "resources/shaders/glow.frag"
2090
+ ]
2091
+ };
2092
+ async function runBrowserResourceLoaderDemo() {
2093
+ console.log("🩸 Bloody Engine - Resource Loader Demo");
2094
+ console.log("==========================================\n");
2095
+ const env = ResourceLoaderFactory.detectEnvironment();
2096
+ console.log(`✓ Environment detected: ${env}`);
2097
+ if (env !== Environment.BROWSER) {
2098
+ console.warn("⚠ This demo is designed for browser environment");
2099
+ return;
2100
+ }
2101
+ console.log("\n1. Creating Resource Pipeline...");
2102
+ const pipeline = await createResourcePipeline({
2103
+ concurrency: 5,
2104
+ cache: true,
2105
+ timeout: 1e4,
2106
+ baseUrl: window.location.origin
2107
+ });
2108
+ console.log("✓ Resource pipeline created");
2109
+ console.log(` - Concurrency: 5`);
2110
+ console.log(` - Caching: enabled`);
2111
+ console.log("\n2. Batch Loading Resources...");
2112
+ console.log(`Loading ${DEMO_CONFIG.resources.length} resources...`);
2113
+ const batchResult = await pipeline.loadBatch(DEMO_CONFIG.resources);
2114
+ console.log(`✓ Batch loading complete`);
2115
+ console.log(` - Succeeded: ${batchResult.successCount}`);
2116
+ console.log(` - Failed: ${batchResult.failureCount}`);
2117
+ if (batchResult.failureCount > 0) {
2118
+ console.log("\n❌ Failed resources:");
2119
+ for (const [path2, error] of batchResult.failed) {
2120
+ console.log(` - ${path2}: ${error}`);
2121
+ }
2122
+ console.log("\n⚠️ Falling back to inline shaders...");
2123
+ }
2124
+ console.log("\n3. Loading Shaders...");
2125
+ const shaders = await pipeline.loadShaders(DEMO_CONFIG.shaders);
2126
+ console.log(`✓ Loaded ${shaders.length} shaders:`);
2127
+ for (const shader2 of shaders) {
2128
+ console.log(` - ${shader2.name}:`);
2129
+ console.log(` Vertex: ${shader2.vertex.length} chars`);
2130
+ console.log(` Fragment: ${shader2.fragment.length} chars`);
2131
+ }
2132
+ console.log("\n4. Testing Cache...");
2133
+ const cachedSize = pipeline.getCacheSize();
2134
+ console.log(`✓ Cache contains ${cachedSize} resources`);
2135
+ for (const shaderConfig of DEMO_CONFIG.shaders) {
2136
+ const vertexCached = pipeline.isCached(shaderConfig.vertex);
2137
+ const fragmentCached = pipeline.isCached(shaderConfig.fragment);
2138
+ console.log(` - ${shaderConfig.name}:`);
2139
+ console.log(` Vertex cached: ${vertexCached}`);
2140
+ console.log(` Fragment cached: ${fragmentCached}`);
2141
+ }
2142
+ console.log("\n5. Initializing Graphics Device...");
2143
+ const gdevice = new GraphicsDevice(SCENE_CONFIG.width, SCENE_CONFIG.height);
2144
+ const gl = gdevice.getGLContext();
2145
+ console.log("✓ Graphics device initialized");
2146
+ console.log(` - Resolution: ${SCENE_CONFIG.width}x${SCENE_CONFIG.height}`);
2147
+ console.log("\n6. Creating Shader from Loaded Source...");
2148
+ let glowShader = shaders.find((s) => s.name === "glow");
2149
+ if (!glowShader || !glowShader.vertex || !glowShader.fragment) {
2150
+ console.warn("⚠️ Glow shader not loaded or empty, using inline fallback");
2151
+ glowShader = {
2152
+ name: "glow",
2153
+ vertex: `attribute vec3 aPosition;
2154
+ attribute vec2 aTexCoord;
2155
+
2156
+ varying vec2 vTexCoord;
2157
+ varying float vDistance;
2158
+
2159
+ uniform mat4 uMatrix;
2160
+
2161
+ void main() {
2162
+ gl_Position = uMatrix * vec4(aPosition, 1.0);
2163
+ vTexCoord = aTexCoord;
2164
+ vDistance = length(aTexCoord - vec2(0.5, 0.5));
2165
+ }`,
2166
+ fragment: `precision mediump float;
2167
+
2168
+ varying vec2 vTexCoord;
2169
+ varying float vDistance;
2170
+ uniform sampler2D uTexture;
2171
+ uniform vec3 uColor;
2172
+ uniform float uGlowIntensity;
2173
+
2174
+ void main() {
2175
+ vec4 texColor = texture2D(uTexture, vTexCoord);
2176
+ // Better glow falloff - keeps minimum brightness
2177
+ float glow = 1.0 - (vDistance * 0.7);
2178
+ glow = max(0.5, glow);
2179
+ vec3 glowColor = texColor.rgb * uColor * glow * uGlowIntensity;
2180
+ gl_FragColor = vec4(glowColor, texColor.a);
2181
+ }`
2182
+ };
2183
+ }
2184
+ const shader = gdevice.createShader(glowShader.vertex, glowShader.fragment);
2185
+ console.log("✓ Shader compiled from loaded source code");
2186
+ console.log(` - Vertex shader: compiled`);
2187
+ console.log(` - Fragment shader: compiled`);
2188
+ console.log(` - Program: linked`);
2189
+ console.log("\n7. Creating Texture...");
2190
+ const texture = Texture.createGradient(
2191
+ gl,
2192
+ TEXTURE_CONFIG.size,
2193
+ TEXTURE_CONFIG.size
2194
+ );
2195
+ console.log("✓ Gradient texture created");
2196
+ console.log(` - Size: ${TEXTURE_CONFIG.size}x${TEXTURE_CONFIG.size}`);
2197
+ console.log("\n8. Creating Geometry Buffers...");
2198
+ const { VertexBuffer: VertexBuffer2 } = await Promise.resolve().then(() => buffer);
2199
+ const quadBuffer = new VertexBuffer2(
2200
+ gl,
2201
+ GEOMETRY.quad.vertices,
2202
+ GEOMETRY.quad.stride
2203
+ );
2204
+ console.log("✓ Quad buffer created");
2205
+ console.log(` - Vertices: ${quadBuffer.getVertexCount()}`);
2206
+ console.log("\n9. Setting up Rendering...");
2207
+ shader.use();
2208
+ const posAttr = shader.getAttributeLocation("aPosition");
2209
+ const texCoordAttr = shader.getAttributeLocation("aTexCoord");
2210
+ const textureUniform = shader.getUniformLocation("uTexture");
2211
+ const matrixUniform = shader.getUniformLocation("uMatrix");
2212
+ const colorUniform = shader.getUniformLocation("uColor");
2213
+ const glowIntensityUniform = shader.getUniformLocation("uGlowIntensity");
2214
+ quadBuffer.bind();
2215
+ gl.enableVertexAttribArray(posAttr);
2216
+ gl.vertexAttribPointer(posAttr, 3, gl.FLOAT, false, GEOMETRY.quad.stride, 0);
2217
+ gl.enableVertexAttribArray(texCoordAttr);
2218
+ gl.vertexAttribPointer(
2219
+ texCoordAttr,
2220
+ 2,
2221
+ gl.FLOAT,
2222
+ false,
2223
+ GEOMETRY.quad.stride,
2224
+ 3 * 4
2225
+ );
2226
+ console.log("✓ Vertex attributes configured");
2227
+ texture.bind(0);
2228
+ gl.uniform1i(textureUniform, 0);
2229
+ console.log("✓ Texture bound to unit 0");
2230
+ const renderingContext = gdevice.getRenderingContext();
2231
+ const canvas = renderingContext.canvas;
2232
+ if (canvas) {
2233
+ canvas.style.display = "block";
2234
+ canvas.style.margin = "0 auto";
2235
+ canvas.style.border = "2px solid #333";
2236
+ canvas.style.backgroundColor = "#1a1a1a";
2237
+ }
2238
+ document.body.style.margin = "0";
2239
+ document.body.style.padding = "20px";
2240
+ document.body.style.backgroundColor = "#0a0a0a";
2241
+ document.body.style.fontFamily = "monospace";
2242
+ document.body.style.color = "#aaa";
2243
+ const title = document.createElement("h1");
2244
+ title.textContent = "🩸 Resource Loader Demo";
2245
+ title.style.textAlign = "center";
2246
+ title.style.color = "#fff";
2247
+ if (canvas && canvas.parentNode) {
2248
+ canvas.parentNode.insertBefore(title, canvas);
2249
+ } else {
2250
+ document.body.insertBefore(title, document.body.firstChild);
2251
+ }
2252
+ const info = document.createElement("div");
2253
+ info.style.textAlign = "center";
2254
+ info.style.marginTop = "10px";
2255
+ info.style.fontSize = "12px";
2256
+ info.innerHTML = `
2257
+ <div>Environment: <strong>${env}</strong></div>
2258
+ <div>Shaders loaded: <strong>${shaders.length}</strong></div>
2259
+ <div>Cached resources: <strong>${cachedSize}</strong></div>
2260
+ `;
2261
+ document.body.appendChild(info);
2262
+ let frameCount = 0;
2263
+ const startTime = Date.now();
2264
+ function render() {
2265
+ const now = Date.now();
2266
+ const elapsedSeconds = (now - startTime) / 1e3;
2267
+ gdevice.clear({ r: 0.1, g: 0.1, b: 0.1, a: 1 });
2268
+ const quads = [
2269
+ { x: -0.3, y: 0.3, color: [1, 0.2, 0.2], glow: 1.5 },
2270
+ { x: 0.3, y: 0.3, color: [0.2, 1, 0.2], glow: 1.8 },
2271
+ { x: -0.3, y: -0.3, color: [0.2, 0.5, 1], glow: 2 },
2272
+ { x: 0.3, y: -0.3, color: [1, 1, 0.2], glow: 1.6 }
2273
+ ];
2274
+ for (const quad of quads) {
2275
+ const matrix = createIdentityMatrix();
2276
+ translateMatrix(matrix, quad.x, quad.y, 0);
2277
+ scaleMatrix(matrix, 0.4, 0.4, 1);
2278
+ if (matrixUniform) {
2279
+ gl.uniformMatrix4fv(matrixUniform, false, matrix);
2280
+ }
2281
+ if (colorUniform) {
2282
+ gl.uniform3f(
2283
+ colorUniform,
2284
+ quad.color[0],
2285
+ quad.color[1],
2286
+ quad.color[2]
2287
+ );
2288
+ }
2289
+ if (glowIntensityUniform) {
2290
+ const pulse = quad.glow + Math.sin(elapsedSeconds * 2) * 0.3;
2291
+ gl.uniform1f(glowIntensityUniform, pulse);
2292
+ }
2293
+ gl.drawArrays(gl.TRIANGLES, 0, quadBuffer.getVertexCount());
2294
+ }
2295
+ gdevice.present();
2296
+ frameCount++;
2297
+ const totalTime = (now - startTime) / 1e3;
2298
+ const fps = frameCount / totalTime;
2299
+ info.innerHTML = `
2300
+ <div>FPS: <strong>${fps.toFixed(1)}</strong> | Frame: <strong>${frameCount}</strong> | Elapsed: <strong>${totalTime.toFixed(2)}s</strong></div>
2301
+ <div>Environment: <strong>${env}</strong> | Shaders loaded: <strong>${shaders.length}</strong> | Cached: <strong>${cachedSize}</strong></div>
2302
+ `;
2303
+ requestAnimationFrame(render);
2304
+ }
2305
+ console.log("\n✓ Demo started! Rendering animation...");
2306
+ render();
2307
+ }
2308
+ function createIdentityMatrix() {
2309
+ return new Float32Array([
2310
+ 1,
2311
+ 0,
2312
+ 0,
2313
+ 0,
2314
+ 0,
2315
+ 1,
2316
+ 0,
2317
+ 0,
2318
+ 0,
2319
+ 0,
2320
+ 1,
2321
+ 0,
2322
+ 0,
2323
+ 0,
2324
+ 0,
2325
+ 1
2326
+ ]);
2327
+ }
2328
+ function translateMatrix(mat, x, y, z) {
2329
+ mat[12] += x;
2330
+ mat[13] += y;
2331
+ mat[14] += z;
2332
+ }
2333
+ function scaleMatrix(mat, x, y, z) {
2334
+ mat[0] *= x;
2335
+ mat[5] *= y;
2336
+ mat[10] *= z;
2337
+ }
2338
+ if (typeof window !== "undefined") {
2339
+ runBrowserResourceLoaderDemo().catch((error) => {
2340
+ console.error("❌ Demo failed:", error);
2341
+ });
2342
+ }
2343
+ export {
2344
+ BatchRenderer,
2345
+ BrowserRenderingContext,
2346
+ BrowserResourceLoader,
2347
+ Camera,
2348
+ Environment,
2349
+ GraphicsDevice,
2350
+ IndexBuffer,
2351
+ Matrix4,
2352
+ NodeRenderingContext,
2353
+ NodeResourceLoader,
2354
+ RenderingContextFactory,
2355
+ ResourceLoaderFactory,
2356
+ ResourcePipeline,
2357
+ Shader,
2358
+ SpriteBatchRenderer,
2359
+ Texture,
2360
+ VertexBuffer,
2361
+ createResourceLoader,
2362
+ createResourcePipeline,
2363
+ runBrowserResourceLoaderDemo
2364
+ };