spryte 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -2
  3. package/dist/backend.d.ts +69 -0
  4. package/dist/backend.d.ts.map +1 -0
  5. package/dist/backend.js +2 -0
  6. package/dist/backend.js.map +1 -0
  7. package/dist/bitmap-font.d.ts +55 -0
  8. package/dist/bitmap-font.d.ts.map +1 -0
  9. package/dist/bitmap-font.js +103 -0
  10. package/dist/bitmap-font.js.map +1 -0
  11. package/dist/canvas2d.d.ts +45 -0
  12. package/dist/canvas2d.d.ts.map +1 -0
  13. package/dist/canvas2d.js +210 -0
  14. package/dist/canvas2d.js.map +1 -0
  15. package/dist/color.d.ts +7 -0
  16. package/dist/color.d.ts.map +1 -0
  17. package/dist/color.js +49 -0
  18. package/dist/color.js.map +1 -0
  19. package/dist/copies.d.ts +45 -0
  20. package/dist/copies.d.ts.map +1 -0
  21. package/dist/copies.js +43 -0
  22. package/dist/copies.js.map +1 -0
  23. package/dist/draw.d.ts +188 -0
  24. package/dist/draw.d.ts.map +1 -0
  25. package/dist/draw.js +603 -0
  26. package/dist/draw.js.map +1 -0
  27. package/dist/index.d.ts +9 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +7 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/renderer.d.ts +17 -0
  32. package/dist/renderer.d.ts.map +1 -0
  33. package/dist/renderer.js +61 -0
  34. package/dist/renderer.js.map +1 -0
  35. package/dist/shader.d.ts +34 -0
  36. package/dist/shader.d.ts.map +1 -0
  37. package/dist/shader.js +40 -0
  38. package/dist/shader.js.map +1 -0
  39. package/dist/target.d.ts +13 -0
  40. package/dist/target.d.ts.map +1 -0
  41. package/dist/target.js +18 -0
  42. package/dist/target.js.map +1 -0
  43. package/dist/webgl2.d.ts +90 -0
  44. package/dist/webgl2.d.ts.map +1 -0
  45. package/dist/webgl2.js +868 -0
  46. package/dist/webgl2.js.map +1 -0
  47. package/package.json +54 -4
package/dist/webgl2.js ADDED
@@ -0,0 +1,868 @@
1
+ import { copyAt } from "./copies.js";
2
+ import { RenderTarget } from "./target.js";
3
+ /** Texture units sampled per batch. 8 is guaranteed on every WebGL2 implementation's fragment stage (min 16). */
4
+ const UNITS = 8;
5
+ const VERT = `#version 300 es
6
+ layout(location=0) in vec2 a_corner;
7
+ layout(location=1) in vec3 a_m0; // a, c, e
8
+ layout(location=2) in vec3 a_m1; // b, d, f
9
+ layout(location=3) in vec4 a_uv; // u0, v0, u1, v1 (normalised)
10
+ layout(location=4) in vec2 a_extra; // flash, texture unit
11
+ layout(location=5) in vec4 a_color; // normalised RGBA
12
+ uniform vec2 u_view;
13
+ out highp vec2 v_uv;
14
+ out vec4 v_color;
15
+ out float v_flash;
16
+ flat out int v_unit;
17
+ void main() {
18
+ vec2 p = vec2(dot(a_m0, vec3(a_corner, 1.0)), dot(a_m1, vec3(a_corner, 1.0)));
19
+ vec2 clip = p / u_view * 2.0 - 1.0;
20
+ gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
21
+ v_uv = mix(a_uv.xy, a_uv.zw, a_corner);
22
+ v_color = vec4(a_color.rgb * a_color.a, a_color.a);
23
+ v_flash = a_extra.x;
24
+ v_unit = int(a_extra.y);
25
+ }`;
26
+ /** Copies of one quad that differ only in position (`draw.sprites`): 8 bytes per copy instead of 52. */
27
+ const COPY_VERT = `#version 300 es
28
+ layout(location=0) in vec2 a_corner;
29
+ layout(location=1) in vec2 a_pos;
30
+ uniform vec2 u_view;
31
+ uniform vec4 u_quad; // a, b, c, d: the unit square onto the sprite
32
+ uniform vec2 u_origin; // e, f for a copy at (0, 0)
33
+ uniform vec4 u_linear; // the current transform's linear part, moving each copy
34
+ uniform vec4 u_uv;
35
+ uniform float u_snap;
36
+ out highp vec2 v_uv;
37
+ void main() {
38
+ vec2 e = u_origin + mat2(u_linear.xy, u_linear.zw) * a_pos;
39
+ // Same rule as draw.sprite() for unrotated pixel art: whole pixels (Math.round == floor(x + 0.5)).
40
+ if (u_snap > 0.5) e = floor(e + 0.5);
41
+ vec2 p = mat2(u_quad.xy, u_quad.zw) * a_corner + e;
42
+ vec2 clip = p / u_view * 2.0 - 1.0;
43
+ gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
44
+ v_uv = mix(u_uv.xy, u_uv.zw, a_corner);
45
+ }`;
46
+ const COPY_FRAG = `#version 300 es
47
+ precision mediump float;
48
+ in highp vec2 v_uv;
49
+ uniform sampler2D u_tex;
50
+ uniform vec4 u_color;
51
+ uniform float u_flash;
52
+ out vec4 outColor;
53
+ void main() {
54
+ vec4 t = texture(u_tex, v_uv) * vec4(u_color.rgb * u_color.a, u_color.a);
55
+ outColor = vec4(mix(t.rgb, vec3(t.a), u_flash), t.a);
56
+ }`;
57
+ /**
58
+ * Copies with per-copy scale, rotation and colour (`draw.sprites` with `each`). The same algebra as
59
+ * `draw.sprite`, run per vertex; absent attributes read constant defaults (scale 1, rotation 0, white).
60
+ */
61
+ const EACH_VERT = `#version 300 es
62
+ layout(location=0) in vec2 a_corner;
63
+ layout(location=1) in vec2 a_pos;
64
+ layout(location=2) in float a_scale;
65
+ layout(location=3) in float a_rot;
66
+ layout(location=4) in uint a_color;
67
+ uniform vec2 u_view;
68
+ uniform vec4 u_linear; // current transform, linear part
69
+ uniform vec2 u_mt; // current transform, translation
70
+ uniform vec2 u_size; // frame size
71
+ uniform vec2 u_offset; // -origin × size
72
+ uniform vec2 u_scale;
73
+ uniform float u_rot;
74
+ uniform vec4 u_uv;
75
+ uniform vec4 u_color; // shared tint × alpha, straight
76
+ uniform float u_snap;
77
+ out highp vec2 v_uv;
78
+ out vec4 v_color;
79
+ void main() {
80
+ float r = u_rot + a_rot;
81
+ float cs = cos(r);
82
+ float sn = sin(r);
83
+ vec2 s = u_scale * a_scale;
84
+ vec2 ab = vec2(cs * s.x, sn * s.x);
85
+ vec2 cd = vec2(-sn * s.y, cs * s.y);
86
+ vec2 l = ab * u_offset.x + cd * u_offset.y + a_pos;
87
+ mat2 M = mat2(u_linear.xy, u_linear.zw);
88
+ vec2 e = M * l + u_mt;
89
+ // Like sprite(): unrotated copies land on whole pixels, decided per copy.
90
+ if (u_snap > 0.5 && r == 0.0) e = floor(e + 0.5);
91
+ vec2 p = (M * ab) * (u_size.x * a_corner.x) + (M * cd) * (u_size.y * a_corner.y) + e;
92
+ vec2 clip = p / u_view * 2.0 - 1.0;
93
+ gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
94
+ v_uv = mix(u_uv.xy, u_uv.zw, a_corner);
95
+ vec4 each = vec4(float(a_color >> 24u), float((a_color >> 16u) & 255u), float((a_color >> 8u) & 255u), float(a_color & 255u)) / 255.0;
96
+ vec4 c = u_color * each;
97
+ v_color = vec4(c.rgb * c.a, c.a);
98
+ }`;
99
+ const EACH_FRAG = `#version 300 es
100
+ precision mediump float;
101
+ in highp vec2 v_uv;
102
+ in vec4 v_color;
103
+ uniform sampler2D u_tex;
104
+ uniform float u_flash;
105
+ out vec4 outColor;
106
+ void main() {
107
+ vec4 t = texture(u_tex, v_uv) * v_color;
108
+ outColor = vec4(mix(t.rgb, vec3(t.a), u_flash), t.a);
109
+ }`;
110
+ const FRAG_HEADER = `#version 300 es
111
+ precision mediump float;
112
+ // highp: mediump is fp16 on many mobile GPUs, too coarse to address texels in atlases past ~1024 px.
113
+ in highp vec2 v_uv;
114
+ in vec4 v_color;
115
+ in float v_flash;
116
+ flat in int v_unit;
117
+ uniform sampler2D u_tex[${UNITS}];
118
+ out vec4 outColor;
119
+ vec4 source(vec2 uv) {
120
+ ${Array.from({ length: UNITS }, (_, i) => `if (v_unit == ${i}) return texture(u_tex[${i}], uv);`).join("\n ")}
121
+ return vec4(1.0);
122
+ }
123
+ `;
124
+ // Flash: push colour toward white while keeping alpha (premultiplied).
125
+ const FRAG_MAIN = `
126
+ void main() {
127
+ vec4 t = effect(v_color, v_uv);
128
+ outColor = vec4(mix(t.rgb, vec3(t.a), v_flash), t.a);
129
+ }`;
130
+ const FRAG = `${FRAG_HEADER}vec4 effect(vec4 color, vec2 uv) { return source(uv) * color; }${FRAG_MAIN}`;
131
+ /** Floats per instance before the 4-byte colour. */
132
+ const FLOATS = 12;
133
+ const STRIDE = FLOATS * 4 + 4;
134
+ /** Instanced, multi-texture WebGL2 sprite batcher behind the `draw` API. */
135
+ export class WebGL2Backend {
136
+ kind = "webgl2";
137
+ raw;
138
+ gl;
139
+ capacity = 16_384;
140
+ data = new ArrayBuffer(this.capacity * STRIDE);
141
+ f32 = new Float32Array(this.data);
142
+ u32 = new Uint32Array(this.data);
143
+ // GPU objects are recreated after a context loss, so they are not readonly.
144
+ instanceBuffer;
145
+ textures = new Map();
146
+ white;
147
+ /** Filtering lives in sampler objects, so one upload serves smooth and pixelated draws. */
148
+ nearest;
149
+ linear;
150
+ main;
151
+ /** The program sprite batches draw with: `main`, or a custom shader's inside `withShader`. */
152
+ active;
153
+ shader = null;
154
+ customs = new Map();
155
+ vao;
156
+ quadBuffer;
157
+ copyPipe;
158
+ each;
159
+ stagingScale = new Float32Array(0);
160
+ stagingRot = new Float32Array(0);
161
+ staging = new Float32Array(0);
162
+ targets = new Map();
163
+ /** Where drawing goes: a render target, or null for the canvas. */
164
+ target = null;
165
+ /** Height in pixels of the current destination, for flipping scissor rectangles. */
166
+ outH = 1;
167
+ viewW = 1;
168
+ viewH = 1;
169
+ /** True between webglcontextlost and webglcontextrestored: drawing is skipped. */
170
+ lost = false;
171
+ onLost = (e) => {
172
+ // preventDefault tells the browser we will handle restoration.
173
+ e.preventDefault();
174
+ this.lost = true;
175
+ };
176
+ onRestored = () => {
177
+ // Every GPU object died with the context. Sources are still in memory, so textures re-upload lazily.
178
+ // Render targets come back blank. Custom shaders recompile on next use.
179
+ const shader = this.shader;
180
+ this.customs.clear();
181
+ this.textures.clear();
182
+ this.targets.clear();
183
+ this.target = null;
184
+ this.lastSrc = null;
185
+ this.last = undefined;
186
+ this.count = 0;
187
+ this.unitCount = 0;
188
+ this.setup();
189
+ this.lost = false;
190
+ if (shader)
191
+ this.setShader(shader);
192
+ };
193
+ units = new Array(UNITS).fill(null);
194
+ unitSmooth = new Array(UNITS).fill(false);
195
+ unitCount = 0;
196
+ lastUnit = 0;
197
+ lastSmooth = false;
198
+ lastSrc = null;
199
+ last;
200
+ count = 0;
201
+ blend = "normal";
202
+ submitted = 0;
203
+ /** Draw calls this frame, counting the batch still pending (read before `end()` flushes it). */
204
+ get drawCalls() {
205
+ return this.submitted + (this.count > 0 ? 1 : 0);
206
+ }
207
+ constructor(gl) {
208
+ this.gl = gl;
209
+ this.raw = { kind: "webgl2", gl };
210
+ this.setup();
211
+ gl.canvas.addEventListener("webglcontextlost", this.onLost);
212
+ gl.canvas.addEventListener("webglcontextrestored", this.onRestored);
213
+ }
214
+ /** True while the GPU context is lost (e.g. a mobile tab was backgrounded or the driver reset). */
215
+ get contextLost() {
216
+ return this.lost;
217
+ }
218
+ /** Create program, buffers and the white texture. Runs at start and after a context restore. */
219
+ setup() {
220
+ const gl = this.gl;
221
+ this.outH = gl.drawingBufferHeight;
222
+ const program = link(gl, VERT, FRAG);
223
+ this.main = { program, view: gl.getUniformLocation(program, "u_view"), w: -1, h: -1 };
224
+ this.active = this.main;
225
+ this.shader = null;
226
+ gl.useProgram(program);
227
+ gl.uniform1iv(gl.getUniformLocation(program, "u_tex"), Array.from({ length: UNITS }, (_, i) => i));
228
+ this.vao = gl.createVertexArray();
229
+ gl.bindVertexArray(this.vao);
230
+ const quad = gl.createBuffer();
231
+ this.quadBuffer = quad;
232
+ gl.bindBuffer(gl.ARRAY_BUFFER, quad);
233
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), gl.STATIC_DRAW);
234
+ gl.enableVertexAttribArray(0);
235
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
236
+ const instances = gl.createBuffer();
237
+ if (!instances)
238
+ throw new Error("WebGL2: could not create instance buffer");
239
+ this.instanceBuffer = instances;
240
+ gl.bindBuffer(gl.ARRAY_BUFFER, instances);
241
+ gl.bufferData(gl.ARRAY_BUFFER, this.data.byteLength, gl.DYNAMIC_DRAW);
242
+ const attr = (loc, size, type, normalise, offset) => {
243
+ gl.enableVertexAttribArray(loc);
244
+ gl.vertexAttribPointer(loc, size, type, normalise, STRIDE, offset);
245
+ gl.vertexAttribDivisor(loc, 1);
246
+ };
247
+ attr(1, 3, gl.FLOAT, false, 0);
248
+ attr(2, 3, gl.FLOAT, false, 12);
249
+ attr(3, 4, gl.FLOAT, false, 24);
250
+ attr(4, 2, gl.FLOAT, false, 40);
251
+ attr(5, 4, gl.UNSIGNED_BYTE, true, 48);
252
+ gl.enable(gl.BLEND);
253
+ this.applyBlend();
254
+ const pixel = new Uint8Array([255, 255, 255, 255]);
255
+ const whiteTex = gl.createTexture();
256
+ if (!whiteTex)
257
+ throw new Error("WebGL2: could not create texture");
258
+ gl.bindTexture(gl.TEXTURE_2D, whiteTex);
259
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
260
+ this.white = { tex: whiteTex, iw: 1, ih: 1, v0: 0 };
261
+ this.wrap();
262
+ this.nearest = sampler(gl, gl.NEAREST);
263
+ this.linear = sampler(gl, gl.LINEAR);
264
+ this.copyPipe = this.copyPipeline(quad);
265
+ this.each = this.eachPipeline(quad);
266
+ gl.useProgram(program);
267
+ gl.bindVertexArray(this.vao);
268
+ }
269
+ begin(clear, width, height) {
270
+ if (this.lost)
271
+ return;
272
+ const gl = this.gl;
273
+ if (this.target)
274
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
275
+ this.target = null;
276
+ this.outH = gl.drawingBufferHeight;
277
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
278
+ if (this.active !== this.main)
279
+ this.setShader(null);
280
+ this.viewW = width;
281
+ this.viewH = height;
282
+ gl.clearColor((clear >>> 24) / 255, ((clear >>> 16) & 0xff) / 255, ((clear >>> 8) & 0xff) / 255, (clear & 0xff) / 255);
283
+ gl.clear(gl.COLOR_BUFFER_BIT);
284
+ this.submitted = 0;
285
+ }
286
+ quad(m, src, sx, sy, sw, sh, color, flash, smooth) {
287
+ if (this.lost)
288
+ return;
289
+ // Fast path: consecutive draws from the same source skip the texture lookup.
290
+ if (src !== null && src === this.target)
291
+ throw new Error("spryte: can't draw a render target into itself");
292
+ const t = src === null ? this.white : src === this.lastSrc && this.last ? this.last : this.texture(src);
293
+ const sameAsLast = t === this.last;
294
+ this.lastSrc = src;
295
+ this.last = t;
296
+ if (this.count === this.capacity)
297
+ this.flush();
298
+ // Same texture and filter as the previous quad in this batch: same unit.
299
+ let unit = sameAsLast && smooth === this.lastSmooth && this.lastUnit < this.unitCount && this.units[this.lastUnit] === t.tex
300
+ ? this.lastUnit
301
+ : -1;
302
+ if (unit < 0)
303
+ for (let i = 0; i < this.unitCount; i++) {
304
+ if (this.units[i] === t.tex && this.unitSmooth[i] === smooth) {
305
+ unit = i;
306
+ break;
307
+ }
308
+ }
309
+ if (unit < 0) {
310
+ if (this.unitCount === UNITS)
311
+ this.flush();
312
+ unit = this.unitCount++;
313
+ this.units[unit] = t.tex;
314
+ this.unitSmooth[unit] = smooth;
315
+ }
316
+ this.lastUnit = unit;
317
+ this.lastSmooth = smooth;
318
+ const f = this.count * (STRIDE / 4);
319
+ const { f32 } = this;
320
+ f32[f] = m[0];
321
+ f32[f + 1] = m[2];
322
+ f32[f + 2] = m[4];
323
+ f32[f + 3] = m[1];
324
+ f32[f + 4] = m[3];
325
+ f32[f + 5] = m[5];
326
+ f32[f + 6] = sx * t.iw;
327
+ f32[f + 7] = t.v0 + sy * t.ih;
328
+ f32[f + 8] = (sx + sw) * t.iw;
329
+ f32[f + 9] = t.v0 + (sy + sh) * t.ih;
330
+ f32[f + 10] = flash;
331
+ f32[f + 11] = unit;
332
+ // Bytes in memory must read R, G, B, A; Uint32 is little-endian.
333
+ this.u32[f + 12] =
334
+ color === 0xffffffff
335
+ ? color
336
+ : ((color & 0xff) << 24) | (((color >>> 8) & 0xff) << 16) | (((color >>> 16) & 0xff) << 8) | (color >>> 24);
337
+ this.count++;
338
+ }
339
+ quads(t, m, positions, count, src, sx, sy, sw, sh, color, flash, smooth, snap) {
340
+ if (this.lost || count <= 0)
341
+ return;
342
+ if (src === this.target)
343
+ throw new Error("spryte: can't draw a render target into itself");
344
+ if (this.shader) {
345
+ // The copy pipeline has no custom effects: draw each copy as a quad through the active shader.
346
+ const q = [t[0], t[1], t[2], t[3], 0, 0];
347
+ for (let i = 0; i < count; i++) {
348
+ const x = positions[i * 2];
349
+ const y = positions[i * 2 + 1];
350
+ q[4] = t[4] + m[0] * x + m[2] * y;
351
+ q[5] = t[5] + m[1] * x + m[3] * y;
352
+ if (snap) {
353
+ q[4] = Math.round(q[4]);
354
+ q[5] = Math.round(q[5]);
355
+ }
356
+ this.quad(q, src, sx, sy, sw, sh, color, flash, smooth);
357
+ }
358
+ return;
359
+ }
360
+ // Draw order: everything queued so far goes first.
361
+ this.flush();
362
+ const gl = this.gl;
363
+ const tex = src === this.lastSrc && this.last ? this.last : this.texture(src);
364
+ this.lastSrc = src;
365
+ this.last = tex;
366
+ const c = this.copyPipe;
367
+ gl.useProgram(c.program);
368
+ gl.bindVertexArray(c.vao);
369
+ this.syncView(c);
370
+ gl.uniform4f(c.quad, t[0], t[1], t[2], t[3]);
371
+ gl.uniform2f(c.origin, t[4], t[5]);
372
+ gl.uniform4f(c.linear, m[0], m[1], m[2], m[3]);
373
+ gl.uniform4f(c.uv, sx * tex.iw, tex.v0 + sy * tex.ih, (sx + sw) * tex.iw, tex.v0 + (sy + sh) * tex.ih);
374
+ gl.uniform4f(c.color, (color >>> 24) / 255, ((color >>> 16) & 0xff) / 255, ((color >>> 8) & 0xff) / 255, (color & 0xff) / 255);
375
+ gl.uniform1f(c.flash, flash);
376
+ gl.uniform1f(c.snap, snap ? 1 : 0);
377
+ gl.activeTexture(gl.TEXTURE0);
378
+ gl.bindTexture(gl.TEXTURE_2D, tex.tex);
379
+ gl.bindSampler(0, smooth ? this.linear : this.nearest);
380
+ gl.bindBuffer(gl.ARRAY_BUFFER, c.buffer);
381
+ // A Float32Array goes to the GPU as is: no per-copy work on the CPU at all.
382
+ let data;
383
+ if (positions instanceof Float32Array)
384
+ data = positions;
385
+ else {
386
+ if (this.staging.length < count * 2)
387
+ this.staging = new Float32Array(count * 2);
388
+ data = this.staging;
389
+ for (let i = 0; i < count * 2; i++)
390
+ data[i] = positions[i];
391
+ }
392
+ gl.bufferData(gl.ARRAY_BUFFER, data, gl.STREAM_DRAW, 0, count * 2);
393
+ gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, count);
394
+ this.submitted++;
395
+ gl.useProgram(this.active.program);
396
+ gl.bindVertexArray(this.vao);
397
+ }
398
+ copies(c) {
399
+ if (this.lost || c.count <= 0)
400
+ return;
401
+ if (c.src === this.target)
402
+ throw new Error("spryte: can't draw a render target into itself");
403
+ if (this.shader) {
404
+ // Custom effects run in the sprite program: one quad per copy.
405
+ const q = [1, 0, 0, 1, 0, 0];
406
+ for (let i = 0; i < c.count; i++) {
407
+ const color = copyAt(c, i, q);
408
+ this.quad(q, c.src, c.sx, c.sy, c.sw, c.sh, color, c.flash, c.smooth);
409
+ }
410
+ return;
411
+ }
412
+ this.flush();
413
+ const gl = this.gl;
414
+ const tex = c.src === this.lastSrc && this.last ? this.last : this.texture(c.src);
415
+ this.lastSrc = c.src;
416
+ this.last = tex;
417
+ const p = this.each;
418
+ const at = p.at;
419
+ gl.useProgram(p.program);
420
+ gl.bindVertexArray(p.vao);
421
+ this.syncView(p);
422
+ const m = c.m;
423
+ gl.uniform4f(at.linear, m[0], m[1], m[2], m[3]);
424
+ gl.uniform2f(at.mt, m[4], m[5]);
425
+ gl.uniform2f(at.size, c.sw, c.sh);
426
+ gl.uniform2f(at.offset, c.ox, c.oy);
427
+ gl.uniform2f(at.scale, c.scaleX, c.scaleY);
428
+ gl.uniform1f(at.rot, c.rotation);
429
+ gl.uniform4f(at.uv, c.sx * tex.iw, tex.v0 + c.sy * tex.ih, (c.sx + c.sw) * tex.iw, tex.v0 + (c.sy + c.sh) * tex.ih);
430
+ const col = c.color;
431
+ gl.uniform4f(at.color, (col >>> 24) / 255, ((col >>> 16) & 0xff) / 255, ((col >>> 8) & 0xff) / 255, (col & 0xff) / 255);
432
+ gl.uniform1f(at.flash, c.flash);
433
+ gl.uniform1f(at.snap, c.snap ? 1 : 0);
434
+ gl.activeTexture(gl.TEXTURE0);
435
+ gl.bindTexture(gl.TEXTURE_2D, tex.tex);
436
+ gl.bindSampler(0, c.smooth ? this.linear : this.nearest);
437
+ const n = c.count;
438
+ this.attribute(1, p.buffers[0], c.positions, n * 2, "staging");
439
+ if (c.each.scale)
440
+ this.attribute(2, p.buffers[1], c.each.scale, n, "stagingScale");
441
+ else {
442
+ gl.disableVertexAttribArray(2);
443
+ gl.vertexAttrib1f(2, 1);
444
+ }
445
+ if (c.each.rotation)
446
+ this.attribute(3, p.buffers[2], c.each.rotation, n, "stagingRot");
447
+ else {
448
+ gl.disableVertexAttribArray(3);
449
+ gl.vertexAttrib1f(3, 0);
450
+ }
451
+ if (c.each.color) {
452
+ gl.bindBuffer(gl.ARRAY_BUFFER, p.buffers[3]);
453
+ gl.bufferData(gl.ARRAY_BUFFER, c.each.color, gl.STREAM_DRAW, 0, n);
454
+ gl.enableVertexAttribArray(4);
455
+ }
456
+ else {
457
+ gl.disableVertexAttribArray(4);
458
+ gl.vertexAttribI4ui(4, 0xffffffff, 0, 0, 0);
459
+ }
460
+ gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, n);
461
+ this.submitted++;
462
+ gl.useProgram(this.active.program);
463
+ gl.bindVertexArray(this.vao);
464
+ }
465
+ /** Upload one float attribute: a Float32Array as is, anything else through a reused staging array. */
466
+ attribute(loc, buffer, data, length, staging) {
467
+ const gl = this.gl;
468
+ let out;
469
+ if (data instanceof Float32Array)
470
+ out = data;
471
+ else {
472
+ if (this[staging].length < length)
473
+ this[staging] = new Float32Array(length);
474
+ out = this[staging];
475
+ for (let i = 0; i < length; i++)
476
+ out[i] = data[i];
477
+ }
478
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
479
+ gl.bufferData(gl.ARRAY_BUFFER, out, gl.STREAM_DRAW, 0, length);
480
+ gl.enableVertexAttribArray(loc);
481
+ }
482
+ setBlend(mode) {
483
+ if (mode === this.blend || this.lost)
484
+ return;
485
+ this.flush();
486
+ this.blend = mode;
487
+ this.applyBlend();
488
+ }
489
+ setClip(rect) {
490
+ if (this.lost)
491
+ return;
492
+ this.flush();
493
+ const gl = this.gl;
494
+ if (!rect) {
495
+ gl.disable(gl.SCISSOR_TEST);
496
+ return;
497
+ }
498
+ gl.enable(gl.SCISSOR_TEST);
499
+ // WebGL's scissor origin is bottom-left.
500
+ gl.scissor(rect.x, this.outH - rect.y - rect.h, rect.w, rect.h);
501
+ }
502
+ end() {
503
+ this.flush();
504
+ this.gl.disable(this.gl.SCISSOR_TEST);
505
+ }
506
+ resize() {
507
+ // The viewport is set from drawingBufferWidth/Height at the start of each frame.
508
+ }
509
+ invalidate(src) {
510
+ // A target's texture is its contents: there is nothing to re-upload.
511
+ if (src instanceof RenderTarget)
512
+ return;
513
+ const cached = this.textures.get(src);
514
+ if (!cached)
515
+ return;
516
+ this.flush();
517
+ this.gl.deleteTexture(cached.tex);
518
+ this.textures.delete(src);
519
+ if (this.lastSrc === src)
520
+ this.lastSrc = null;
521
+ }
522
+ /** Release every GPU object this backend created, so renderers can be created and destroyed repeatedly. */
523
+ dispose() {
524
+ const gl = this.gl;
525
+ gl.canvas.removeEventListener("webglcontextlost", this.onLost);
526
+ gl.canvas.removeEventListener("webglcontextrestored", this.onRestored);
527
+ if (!this.lost) {
528
+ for (const { tex } of this.textures.values())
529
+ gl.deleteTexture(tex);
530
+ gl.deleteTexture(this.white.tex);
531
+ gl.deleteSampler(this.nearest);
532
+ gl.deleteSampler(this.linear);
533
+ gl.deleteBuffer(this.instanceBuffer);
534
+ gl.deleteBuffer(this.quadBuffer);
535
+ gl.deleteVertexArray(this.vao);
536
+ gl.deleteProgram(this.main.program);
537
+ for (const custom of this.customs.values())
538
+ gl.deleteProgram(custom.program);
539
+ for (const { fbo } of this.targets.values())
540
+ gl.deleteFramebuffer(fbo);
541
+ gl.deleteProgram(this.copyPipe.program);
542
+ gl.deleteVertexArray(this.copyPipe.vao);
543
+ gl.deleteBuffer(this.copyPipe.buffer);
544
+ gl.deleteProgram(this.each.program);
545
+ gl.deleteVertexArray(this.each.vao);
546
+ for (const b of this.each.buffers)
547
+ gl.deleteBuffer(b);
548
+ }
549
+ this.textures.clear();
550
+ this.targets.clear();
551
+ this.customs.clear();
552
+ this.lastSrc = null;
553
+ this.last = undefined;
554
+ }
555
+ setShader(shader) {
556
+ if (shader === this.shader)
557
+ return;
558
+ this.flush();
559
+ if (this.shader)
560
+ this.shader.onChange = undefined;
561
+ this.shader = shader;
562
+ if (this.lost)
563
+ return;
564
+ const next = shader ? this.custom(shader) : this.main;
565
+ this.active = next;
566
+ this.gl.useProgram(next.program);
567
+ // Uniform changes flush the batch drawn with the old values first.
568
+ if (shader)
569
+ shader.onChange = () => this.flush();
570
+ }
571
+ setTarget(target, clear, width, height) {
572
+ if (this.lost) {
573
+ this.target = target;
574
+ return;
575
+ }
576
+ this.flush();
577
+ const gl = this.gl;
578
+ if (target) {
579
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.targetStore(target).fbo);
580
+ gl.viewport(0, 0, target.width, target.height);
581
+ this.outH = target.height;
582
+ }
583
+ else {
584
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
585
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
586
+ this.outH = gl.drawingBufferHeight;
587
+ }
588
+ this.target = target;
589
+ this.viewW = width;
590
+ this.viewH = height;
591
+ if (clear !== null) {
592
+ gl.clearColor((clear >>> 24) / 255, ((clear >>> 16) & 0xff) / 255, ((clear >>> 8) & 0xff) / 255, (clear & 0xff) / 255);
593
+ gl.clear(gl.COLOR_BUFFER_BIT);
594
+ }
595
+ }
596
+ disposeTarget(target) {
597
+ if (target === this.target)
598
+ throw new Error("spryte: can't dispose a render target while drawing into it");
599
+ const store = this.targets.get(target);
600
+ if (!store)
601
+ return;
602
+ this.flush();
603
+ if (!this.lost) {
604
+ this.gl.deleteFramebuffer(store.fbo);
605
+ this.gl.deleteTexture(store.cached.tex);
606
+ }
607
+ this.targets.delete(target);
608
+ this.textures.delete(target);
609
+ if (this.lastSrc === target) {
610
+ this.lastSrc = null;
611
+ this.last = undefined;
612
+ }
613
+ }
614
+ /** A target's framebuffer and texture, created on first use. */
615
+ targetStore(target) {
616
+ let store = this.targets.get(target);
617
+ if (store)
618
+ return store;
619
+ const gl = this.gl;
620
+ const tex = gl.createTexture();
621
+ const fbo = gl.createFramebuffer();
622
+ if (!tex || !fbo)
623
+ throw new Error("WebGL2: could not create a render target");
624
+ gl.activeTexture(gl.TEXTURE0);
625
+ gl.bindTexture(gl.TEXTURE_2D, tex);
626
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, target.width, target.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
627
+ this.wrap();
628
+ const previous = this.target ? (this.targets.get(this.target)?.fbo ?? null) : null;
629
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
630
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
631
+ // Start transparent, not with whatever the driver left in that memory.
632
+ const scissor = gl.isEnabled(gl.SCISSOR_TEST);
633
+ if (scissor)
634
+ gl.disable(gl.SCISSOR_TEST);
635
+ gl.clearColor(0, 0, 0, 0);
636
+ gl.clear(gl.COLOR_BUFFER_BIT);
637
+ if (scissor)
638
+ gl.enable(gl.SCISSOR_TEST);
639
+ gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
640
+ // Rendered contents are already premultiplied, and stored bottom row first.
641
+ store = { fbo, cached: { tex, iw: 1 / target.width, ih: -1 / target.height, v0: 1 } };
642
+ this.targets.set(target, store);
643
+ this.textures.set(target, store.cached);
644
+ return store;
645
+ }
646
+ eachPipeline(quad) {
647
+ const gl = this.gl;
648
+ const program = link(gl, EACH_VERT, EACH_FRAG);
649
+ gl.useProgram(program);
650
+ gl.uniform1i(gl.getUniformLocation(program, "u_tex"), 0);
651
+ const vao = gl.createVertexArray();
652
+ gl.bindVertexArray(vao);
653
+ gl.bindBuffer(gl.ARRAY_BUFFER, quad);
654
+ gl.enableVertexAttribArray(0);
655
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
656
+ const buffers = [];
657
+ for (const [loc, size] of [
658
+ [1, 2],
659
+ [2, 1],
660
+ [3, 1],
661
+ [4, 1],
662
+ ]) {
663
+ const b = gl.createBuffer();
664
+ if (!b)
665
+ throw new Error("WebGL2: could not create buffer");
666
+ buffers.push(b);
667
+ gl.bindBuffer(gl.ARRAY_BUFFER, b);
668
+ if (loc === 4)
669
+ gl.vertexAttribIPointer(loc, size, gl.UNSIGNED_INT, 0, 0);
670
+ else
671
+ gl.vertexAttribPointer(loc, size, gl.FLOAT, false, 0, 0);
672
+ gl.vertexAttribDivisor(loc, 1);
673
+ }
674
+ const u = (name) => gl.getUniformLocation(program, name);
675
+ return {
676
+ program,
677
+ vao,
678
+ buffers,
679
+ view: u("u_view"),
680
+ w: -1,
681
+ h: -1,
682
+ at: {
683
+ linear: u("u_linear"),
684
+ mt: u("u_mt"),
685
+ size: u("u_size"),
686
+ offset: u("u_offset"),
687
+ scale: u("u_scale"),
688
+ rot: u("u_rot"),
689
+ uv: u("u_uv"),
690
+ color: u("u_color"),
691
+ flash: u("u_flash"),
692
+ snap: u("u_snap"),
693
+ },
694
+ };
695
+ }
696
+ copyPipeline(quad) {
697
+ const gl = this.gl;
698
+ const program = link(gl, COPY_VERT, COPY_FRAG);
699
+ gl.useProgram(program);
700
+ gl.uniform1i(gl.getUniformLocation(program, "u_tex"), 0);
701
+ const vao = gl.createVertexArray();
702
+ gl.bindVertexArray(vao);
703
+ gl.bindBuffer(gl.ARRAY_BUFFER, quad);
704
+ gl.enableVertexAttribArray(0);
705
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
706
+ const buffer = gl.createBuffer();
707
+ if (!buffer)
708
+ throw new Error("WebGL2: could not create buffer");
709
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
710
+ gl.enableVertexAttribArray(1);
711
+ gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0);
712
+ gl.vertexAttribDivisor(1, 1);
713
+ const at = (name) => gl.getUniformLocation(program, name);
714
+ return {
715
+ program,
716
+ vao,
717
+ buffer,
718
+ view: at("u_view"),
719
+ w: -1,
720
+ h: -1,
721
+ quad: at("u_quad"),
722
+ origin: at("u_origin"),
723
+ linear: at("u_linear"),
724
+ uv: at("u_uv"),
725
+ color: at("u_color"),
726
+ flash: at("u_flash"),
727
+ snap: at("u_snap"),
728
+ };
729
+ }
730
+ flush() {
731
+ if (this.lost) {
732
+ this.count = 0;
733
+ this.unitCount = 0;
734
+ return;
735
+ }
736
+ if (this.count === 0) {
737
+ this.unitCount = 0;
738
+ return;
739
+ }
740
+ const gl = this.gl;
741
+ for (let i = 0; i < this.unitCount; i++) {
742
+ gl.activeTexture(gl.TEXTURE0 + i);
743
+ gl.bindTexture(gl.TEXTURE_2D, this.units[i] ?? null);
744
+ gl.bindSampler(i, this.unitSmooth[i] ? this.linear : this.nearest);
745
+ }
746
+ this.syncView(this.active);
747
+ if (this.shader)
748
+ this.syncUniforms(this.shader, this.active);
749
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer);
750
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.f32, 0, this.count * (STRIDE / 4));
751
+ gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, this.count);
752
+ this.submitted++;
753
+ this.count = 0;
754
+ this.unitCount = 0;
755
+ }
756
+ /** Upload `u_view` to a program if the destination size changed since it last drew. The program must be bound. */
757
+ syncView(p) {
758
+ if (p.w === this.viewW && p.h === this.viewH)
759
+ return;
760
+ this.gl.uniform2f(p.view, this.viewW, this.viewH);
761
+ p.w = this.viewW;
762
+ p.h = this.viewH;
763
+ }
764
+ syncUniforms(shader, p) {
765
+ const gl = this.gl;
766
+ for (const [name, value] of shader.uniforms) {
767
+ let loc = p.locations.get(name);
768
+ if (loc === undefined) {
769
+ loc = gl.getUniformLocation(p.program, name);
770
+ p.locations.set(name, loc);
771
+ }
772
+ if (loc === null)
773
+ continue; // unused uniforms are optimised away by the compiler
774
+ if (typeof value === "number")
775
+ gl.uniform1f(loc, value);
776
+ else if (value.length === 2)
777
+ gl.uniform2f(loc, value[0], value[1]);
778
+ else if (value.length === 3)
779
+ gl.uniform3f(loc, value[0], value[1], value[2]);
780
+ else
781
+ gl.uniform4f(loc, value[0], value[1], value[2], value[3]);
782
+ }
783
+ }
784
+ custom(shader) {
785
+ let p = this.customs.get(shader);
786
+ if (p)
787
+ return p;
788
+ const gl = this.gl;
789
+ // #line makes compile errors point at lines in the user's source.
790
+ const program = link(gl, VERT, `${FRAG_HEADER}#line 1\n${shader.source}\n${FRAG_MAIN}`, "custom shader");
791
+ gl.useProgram(program);
792
+ gl.uniform1iv(gl.getUniformLocation(program, "u_tex"), Array.from({ length: UNITS }, (_, i) => i));
793
+ p = { program, view: gl.getUniformLocation(program, "u_view"), w: -1, h: -1, locations: new Map() };
794
+ this.customs.set(shader, p);
795
+ return p;
796
+ }
797
+ texture(src) {
798
+ const hit = this.textures.get(src);
799
+ if (hit)
800
+ return hit;
801
+ if (src instanceof RenderTarget)
802
+ return this.targetStore(src).cached;
803
+ const gl = this.gl;
804
+ const tex = gl.createTexture();
805
+ if (!tex)
806
+ throw new Error("WebGL2: could not create texture");
807
+ gl.activeTexture(gl.TEXTURE0);
808
+ gl.bindTexture(gl.TEXTURE_2D, tex);
809
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
810
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src);
811
+ this.wrap();
812
+ const w = "naturalWidth" in src ? src.naturalWidth : src.width;
813
+ const h = "naturalHeight" in src ? src.naturalHeight : src.height;
814
+ const cached = { tex, iw: 1 / w, ih: 1 / h, v0: 0 };
815
+ this.textures.set(src, cached);
816
+ // Unit 0 is now bound to this texture; the next flush rebinds whatever the batch needs.
817
+ return cached;
818
+ }
819
+ /** Texture parameters for the bound texture. Filtering comes from the samplers; this keeps the texture complete without them. */
820
+ wrap() {
821
+ const gl = this.gl;
822
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
823
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
824
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
825
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
826
+ }
827
+ applyBlend() {
828
+ const gl = this.gl;
829
+ if (this.blend === "add")
830
+ gl.blendFunc(gl.ONE, gl.ONE);
831
+ else
832
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
833
+ }
834
+ }
835
+ function sampler(gl, filter) {
836
+ const s = gl.createSampler();
837
+ if (!s)
838
+ throw new Error("WebGL2: could not create sampler");
839
+ gl.samplerParameteri(s, gl.TEXTURE_MIN_FILTER, filter);
840
+ gl.samplerParameteri(s, gl.TEXTURE_MAG_FILTER, filter);
841
+ gl.samplerParameteri(s, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
842
+ gl.samplerParameteri(s, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
843
+ return s;
844
+ }
845
+ function link(gl, vs, fs, label = "WebGL2") {
846
+ const compile = (type, source) => {
847
+ const shader = gl.createShader(type);
848
+ if (!shader)
849
+ throw new Error("WebGL2: could not create shader");
850
+ gl.shaderSource(shader, source);
851
+ gl.compileShader(shader);
852
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
853
+ throw new Error(`spryte: ${label} failed to compile:\n${gl.getShaderInfoLog(shader)}`);
854
+ return shader;
855
+ };
856
+ const program = gl.createProgram();
857
+ const shaders = [compile(gl.VERTEX_SHADER, vs), compile(gl.FRAGMENT_SHADER, fs)];
858
+ for (const shader of shaders)
859
+ gl.attachShader(program, shader);
860
+ gl.linkProgram(program);
861
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS))
862
+ throw new Error(`WebGL2 program: ${gl.getProgramInfoLog(program)}`);
863
+ // The linked program keeps the compiled code; the shader objects can go.
864
+ for (const shader of shaders)
865
+ gl.deleteShader(shader);
866
+ return program;
867
+ }
868
+ //# sourceMappingURL=webgl2.js.map