@weasel-js/renderer 0.0.0-stage → 1.9.4

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.
package/dist/index.js ADDED
@@ -0,0 +1,4086 @@
1
+ export { createTilePattern } from './chunk-2VOBNWBN.js';
2
+ import { BATCH_ATTRIBUTE_LOCATIONS, BATCH_TEXTURE_SLOTS, registerProgram, BATCH_VERT_SRC, BATCH_FRAG_SRC, BATCH_FILL_UNIFORMS, BATCH_FILL_ATTRIBUTES, GLImageCache, DrawBatch, getProgramSource, commandFill, cachedMarkerCommands, PAINT_MODE_PLAIN, PAINT_MODE_RADIAL, PAINT_MODE_CONIC, getMesh, quantizeStrokeScale, strokeMesh, axisAlignedClipRect, intersectClipRects, getPaintDrawKind, outlineMesh, outlineStrokeMesh, quantizeEmWidth } from './chunk-VHQJ56XQ.js';
3
+ export { commandFill, commandStroke, fillInPoseFrame, fillToBoundsFrame, getImageBitmap, getPaintDrawKind, getProgramSource, imageStatus, isPaintKindKnown, isPatternSpec, isVectorImageSrc, paintDrawKindRegistry, programSourceRegistry, registerPaintDrawKind, registerPaintDrawKindLoader, registerProgram, releaseImage, releaseImageSource, resolveFillPattern, resolvePatternSpec, retryImage, subscribeImageReady, warmPaintKinds } from './chunk-VHQJ56XQ.js';
4
+ import { textureRegistry, getTexture } from './chunk-ORBXHIRB.js';
5
+ export { registerTexture, textureRegistry, unregisterTexture } from './chunk-ORBXHIRB.js';
6
+ import { DEFAULT_BAKE_BUDGET, markAllFontsNotUploaded, resetBakeBudget, GLYPH_COVERAGE_GLSL, syncDynamicPageTexture, ensureFontTexture, dynamicPageTextureId, textureCacheKey, GLYPH_MODE_R8, GLYPH_MODE_MSDF, glyphFieldScale } from '@weasel-js/font';
7
+ export { canQueryLocalFonts, cssFontFamily, cssFontFamilyLoading, enableLocalFontOutlines, fontOutlineRegistry, fontRegistry, hasFontOutlines, isCanvasFont, listFontOutlines, loadFontOutlines, outlineStatus, registerCanvasFont, registerFont, registerFontOutlines, subscribeGlyphReady, unregisterCanvasFont, unregisterFontOutlines } from '@weasel-js/font';
8
+ import { resolveGradientStops, sampleResolvedStops, resolveScreenLength, resolveColor } from '@weasel-js/paint';
9
+ import { mat3 } from '@weasel-js/geom';
10
+ export { mat3, viewToMat3 } from '@weasel-js/geom';
11
+ import { tessellate } from '@weasel-js/geom/tessellate';
12
+ export { tessellate } from '@weasel-js/geom/tessellate';
13
+ import { resolveStrokeWidth, markerReach, metricKeepsRects, strokeSpaceOf, resolveMarkerSize } from '@weasel-js/stroke';
14
+ export { tessellateStroke } from '@weasel-js/stroke';
15
+ import { resolveTextStyle, resolveRuns, textPoseLayoutInput, cachedLayoutRuns, verticalAlignOffset, SYNTHETIC_ITALIC_RADIANS } from '@weasel-js/text';
16
+
17
+ // src/paintResources.ts
18
+ function createPaintResources() {
19
+ let releases = [];
20
+ return {
21
+ onRelease(release) {
22
+ if (releases) releases.push(release);
23
+ else release();
24
+ },
25
+ release() {
26
+ const pending = releases;
27
+ releases = null;
28
+ for (const release of pending ?? []) release();
29
+ }
30
+ };
31
+ }
32
+
33
+ // src/shaders/ShaderProgram.ts
34
+ var ShaderCompileError = class extends Error {
35
+ constructor(stage, log) {
36
+ super(`shader ${stage} failure: ${log}`);
37
+ this.stage = stage;
38
+ this.log = log;
39
+ this.name = "ShaderCompileError";
40
+ }
41
+ stage;
42
+ log;
43
+ };
44
+ var POSITION_ATTRIBUTE_LOCATION = 0;
45
+ var ShaderProgram = class {
46
+ constructor(gl, vertSrc, fragSrc2) {
47
+ this.gl = gl;
48
+ const vs = this.compile(gl.VERTEX_SHADER, vertSrc, "vertex");
49
+ let fs;
50
+ try {
51
+ fs = this.compile(gl.FRAGMENT_SHADER, fragSrc2, "fragment");
52
+ } catch (e) {
53
+ gl.deleteShader(vs);
54
+ throw e;
55
+ }
56
+ const program = gl.createProgram();
57
+ if (!program) {
58
+ gl.deleteShader(vs);
59
+ gl.deleteShader(fs);
60
+ throw new Error("createProgram returned null");
61
+ }
62
+ gl.attachShader(program, vs);
63
+ gl.attachShader(program, fs);
64
+ gl.bindAttribLocation(program, POSITION_ATTRIBUTE_LOCATION, "a_position");
65
+ gl.linkProgram(program);
66
+ gl.deleteShader(vs);
67
+ gl.deleteShader(fs);
68
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
69
+ const log = gl.getProgramInfoLog(program) ?? "";
70
+ gl.deleteProgram(program);
71
+ throw new ShaderCompileError("link", log);
72
+ }
73
+ this.handle = program;
74
+ }
75
+ gl;
76
+ handle;
77
+ uniforms = /* @__PURE__ */ new Map();
78
+ arrays = /* @__PURE__ */ new Map();
79
+ attributes = /* @__PURE__ */ new Map();
80
+ compile(type, source, stage) {
81
+ const gl = this.gl;
82
+ const shader = gl.createShader(type);
83
+ if (!shader) throw new Error("createShader returned null");
84
+ gl.shaderSource(shader, source);
85
+ gl.compileShader(shader);
86
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
87
+ const log = gl.getShaderInfoLog(shader) ?? "";
88
+ gl.deleteShader(shader);
89
+ throw new ShaderCompileError(stage, log);
90
+ }
91
+ return shader;
92
+ }
93
+ lookupUniforms(names) {
94
+ for (const name of names) {
95
+ const loc = this.gl.getUniformLocation(this.handle, name);
96
+ if (loc !== null) this.uniforms.set(name, loc);
97
+ }
98
+ }
99
+ /** Record each array's slot-0 location under its bare name. An array the
100
+ * driver optimized away has no location and is left out. */
101
+ lookupUniformArrays(decls) {
102
+ for (const [name, { type, size }] of decls) {
103
+ const base = `${name}[0]`;
104
+ const location = this.uniforms.get(base) ?? this.gl.getUniformLocation(this.handle, base);
105
+ if (location !== null) this.arrays.set(name, { type, size, location });
106
+ }
107
+ }
108
+ lookupAttributes(names) {
109
+ for (const name of names) {
110
+ const loc = this.gl.getAttribLocation(this.handle, name);
111
+ if (loc >= 0) this.attributes.set(name, loc);
112
+ }
113
+ }
114
+ uniform(name) {
115
+ return this.uniforms.get(name);
116
+ }
117
+ uniformArray(name) {
118
+ return this.arrays.get(name);
119
+ }
120
+ attribute(name) {
121
+ return this.attributes.get(name);
122
+ }
123
+ };
124
+
125
+ // src/shaders/pathFill.ts
126
+ var VERT_SRC = (
127
+ /* glsl */
128
+ `#version 300 es
129
+ in vec2 a_position;
130
+ uniform mat3 u_proj;
131
+ uniform mat3 u_model;
132
+ uniform vec2 u_origin;
133
+ void main() {
134
+ vec3 screen = u_model * vec3(a_position - u_origin, 1.0);
135
+ vec3 clip = u_proj * vec3(screen.xy, 1.0);
136
+ gl_Position = vec4(clip.xy, 0.0, 1.0);
137
+ }
138
+ `
139
+ );
140
+ var FRAG_SRC = (
141
+ /* glsl */
142
+ `#version 300 es
143
+ precision highp float;
144
+ uniform vec4 u_color;
145
+ uniform float u_alpha;
146
+ uniform mat4 u_colorMatrix;
147
+ uniform vec4 u_colorBias;
148
+ out vec4 outColor;
149
+ void main() {
150
+ vec4 mapped = clamp(u_colorMatrix * u_color + u_colorBias, 0.0, 1.0);
151
+ float a = mapped.a * u_alpha;
152
+ outColor = vec4(mapped.rgb * a, a);
153
+ }
154
+ `
155
+ );
156
+ var VCOLOR_VERT_SRC = (
157
+ /* glsl */
158
+ `#version 300 es
159
+ in vec2 a_position;
160
+ in vec4 a_vertexColor;
161
+ uniform mat3 u_proj;
162
+ uniform mat3 u_model;
163
+ uniform vec2 u_origin;
164
+ out vec4 v_vertexColor;
165
+ void main() {
166
+ vec3 screen = u_model * vec3(a_position - u_origin, 1.0);
167
+ vec3 clip = u_proj * vec3(screen.xy, 1.0);
168
+ gl_Position = vec4(clip.xy, 0.0, 1.0);
169
+ v_vertexColor = a_vertexColor;
170
+ }
171
+ `
172
+ );
173
+ var VCOLOR_FRAG_SRC = (
174
+ /* glsl */
175
+ `#version 300 es
176
+ precision highp float;
177
+ in vec4 v_vertexColor;
178
+ uniform vec4 u_color;
179
+ uniform float u_alpha;
180
+ uniform mat4 u_colorMatrix;
181
+ uniform vec4 u_colorBias;
182
+ out vec4 outColor;
183
+ void main() {
184
+ vec4 src = u_color * v_vertexColor;
185
+ vec4 mapped = clamp(u_colorMatrix * src + u_colorBias, 0.0, 1.0);
186
+ float a = mapped.a * u_alpha;
187
+ outColor = vec4(mapped.rgb * a, a);
188
+ }
189
+ `
190
+ );
191
+ var PATH_FILL_UNIFORMS = [
192
+ "u_proj",
193
+ "u_model",
194
+ "u_origin",
195
+ "u_color",
196
+ "u_alpha",
197
+ "u_colorMatrix",
198
+ "u_colorBias"
199
+ ];
200
+ var PATH_FILL_ATTRIBUTES = ["a_position"];
201
+ var PATH_FILL_VCOLOR_ATTRIBUTES = ["a_position", "a_vertexColor"];
202
+
203
+ // src/shaders/gradFill.ts
204
+ var GRAD_VERT_SRC = (
205
+ /* glsl */
206
+ `#version 300 es
207
+ in vec2 a_position;
208
+ uniform mat3 u_proj;
209
+ uniform mat3 u_model;
210
+ uniform vec2 u_origin;
211
+ uniform mat3 u_worldInv;
212
+ out vec2 v_world;
213
+ void main() {
214
+ vec3 screen = u_model * vec3(a_position - u_origin, 1.0);
215
+ vec3 clip = u_proj * vec3(screen.xy, 1.0);
216
+ gl_Position = vec4(clip.xy, 0.0, 1.0);
217
+ vec3 world = u_worldInv * vec3(screen.xy, 1.0);
218
+ v_world = world.xy;
219
+ }
220
+ `
221
+ );
222
+ var GRAD_PAINT_GLSL = (
223
+ /* glsl */
224
+ `
225
+ uniform sampler2D u_ramp;
226
+ uniform float u_rampV;
227
+ uniform float u_alpha;
228
+ uniform mat4 u_colorMatrix;
229
+ uniform vec4 u_colorBias;
230
+ uniform float u_opacity;
231
+ uniform int u_gradKind;
232
+ uniform vec2 u_gradP0;
233
+ uniform vec2 u_gradDir;
234
+ uniform float u_gradLen;
235
+ uniform float u_gradRadius;
236
+ uniform float u_gradAngle;
237
+
238
+ const float PI = 3.14159265358979323846;
239
+
240
+ vec4 shadePaint(vec2 world) {
241
+ float t;
242
+ if (u_gradKind == 0) {
243
+ vec2 d = world - u_gradP0;
244
+ t = dot(d, u_gradDir) / max(u_gradLen, 0.0001);
245
+ } else if (u_gradKind == 1) {
246
+ t = length(world - u_gradP0) / max(u_gradRadius, 0.0001);
247
+ } else {
248
+ float a = atan(world.y - u_gradP0.y, world.x - u_gradP0.x) - u_gradAngle;
249
+ t = fract(a / (2.0 * PI));
250
+ }
251
+ t = clamp(t, 0.0, 1.0);
252
+ vec4 rampColor = texture(u_ramp, vec2(t, u_rampV));
253
+ // Fill opacity into the alpha channel before the matrix, which is where the
254
+ // batch program's vertex alpha sits. The two have to agree, since a linear
255
+ // gradient goes through that one and a radial through this.
256
+ vec4 src = vec4(rampColor.rgb, rampColor.a * u_opacity);
257
+ vec4 mapped = clamp(u_colorMatrix * src + u_colorBias, 0.0, 1.0);
258
+ float a = mapped.a * u_alpha;
259
+ return vec4(mapped.rgb * a, a);
260
+ }
261
+ `
262
+ );
263
+ var GRAD_FRAG_SRC = (
264
+ /* glsl */
265
+ `#version 300 es
266
+ precision highp float;
267
+ in vec2 v_world;
268
+ ${GRAD_PAINT_GLSL}
269
+ out vec4 outColor;
270
+ void main() {
271
+ outColor = shadePaint(v_world);
272
+ }
273
+ `
274
+ );
275
+ var GRAD_FILL_UNIFORMS = [
276
+ "u_proj",
277
+ "u_model",
278
+ "u_origin",
279
+ "u_worldInv",
280
+ "u_ramp",
281
+ "u_rampV",
282
+ "u_alpha",
283
+ "u_opacity",
284
+ "u_colorMatrix",
285
+ "u_colorBias",
286
+ "u_gradKind",
287
+ "u_gradP0",
288
+ "u_gradDir",
289
+ "u_gradLen",
290
+ "u_gradRadius",
291
+ "u_gradAngle"
292
+ ];
293
+ var GRAD_FILL_ATTRIBUTES = ["a_position"];
294
+
295
+ // src/shaders/patternFill.ts
296
+ var PATTERN_VERT_SRC = (
297
+ /* glsl */
298
+ `#version 300 es
299
+ in vec2 a_position;
300
+ uniform mat3 u_proj;
301
+ uniform mat3 u_model;
302
+ uniform vec2 u_origin;
303
+ uniform mat3 u_worldInv;
304
+ out vec2 v_world;
305
+ void main() {
306
+ vec3 screen = u_model * vec3(a_position - u_origin, 1.0);
307
+ vec3 clip = u_proj * vec3(screen.xy, 1.0);
308
+ gl_Position = vec4(clip.xy, 0.0, 1.0);
309
+ vec3 world = u_worldInv * vec3(screen.xy, 1.0);
310
+ v_world = world.xy;
311
+ }
312
+ `
313
+ );
314
+ var PATTERN_PAINT_GLSL = (
315
+ /* glsl */
316
+ `
317
+ uniform sampler2D u_sampler;
318
+ uniform vec2 u_tileSize;
319
+ uniform float u_opacity;
320
+ uniform float u_alpha;
321
+ uniform mat4 u_colorMatrix;
322
+ uniform vec4 u_colorBias;
323
+ vec4 shadePaint(vec2 world) {
324
+ vec2 uv = world / max(u_tileSize, vec2(0.0001));
325
+ vec4 texel = texture(u_sampler, uv);
326
+ vec4 mapped = clamp(u_colorMatrix * texel + u_colorBias, 0.0, 1.0);
327
+ float a = mapped.a * u_opacity * u_alpha;
328
+ return vec4(mapped.rgb * a, a);
329
+ }
330
+ `
331
+ );
332
+ var PATTERN_FRAG_SRC = (
333
+ /* glsl */
334
+ `#version 300 es
335
+ precision highp float;
336
+ in vec2 v_world;
337
+ ${PATTERN_PAINT_GLSL}
338
+ out vec4 outColor;
339
+ void main() {
340
+ outColor = shadePaint(v_world);
341
+ }
342
+ `
343
+ );
344
+ var PATTERN_FILL_UNIFORMS = [
345
+ "u_proj",
346
+ "u_model",
347
+ "u_origin",
348
+ "u_worldInv",
349
+ "u_sampler",
350
+ "u_tileSize",
351
+ "u_opacity",
352
+ "u_alpha",
353
+ "u_colorMatrix",
354
+ "u_colorBias"
355
+ ];
356
+ var PATTERN_FILL_ATTRIBUTES = ["a_position"];
357
+ var L = BATCH_ATTRIBUTE_LOCATIONS;
358
+ var GLYPH_PAINT_VERT_SRC = (
359
+ /* glsl */
360
+ `#version 300 es
361
+ layout(location = ${L.a_position}) in vec2 a_position;
362
+ layout(location = ${L.a_uv}) in vec2 a_uv;
363
+ layout(location = ${L.a_texSlot}) in float a_texSlot;
364
+ uniform mat3 u_proj;
365
+ uniform mat3 u_model;
366
+ uniform mat3 u_worldInv;
367
+ out vec2 v_world;
368
+ out vec2 v_uv;
369
+ flat out float v_glyphMode;
370
+ void main() {
371
+ vec3 screen = u_model * vec3(a_position, 1.0);
372
+ vec3 clip = u_proj * vec3(screen.xy, 1.0);
373
+ gl_Position = vec4(clip.xy, 0.0, 1.0);
374
+ v_world = (u_worldInv * vec3(screen.xy, 1.0)).xy;
375
+ v_uv = a_uv;
376
+ v_glyphMode = float(int(a_texSlot + 0.5) / ${BATCH_TEXTURE_SLOTS});
377
+ }
378
+ `
379
+ );
380
+ function fragSrc(paintGlsl) {
381
+ return (
382
+ /* glsl */
383
+ `#version 300 es
384
+ precision highp float;
385
+ in vec2 v_world;
386
+ in vec2 v_uv;
387
+ flat in float v_glyphMode;
388
+ uniform sampler2D u_atlas;
389
+ uniform float u_synthBold;
390
+ uniform vec2 u_fieldScale;
391
+ ${paintGlsl}
392
+ ${GLYPH_COVERAGE_GLSL}
393
+ out vec4 outColor;
394
+ void main() {
395
+ float coverage = glyphCoverage(texture(u_atlas, v_uv), v_glyphMode, u_synthBold, v_uv, u_fieldScale);
396
+ outColor = shadePaint(v_world) * coverage;
397
+ }
398
+ `
399
+ );
400
+ }
401
+ var TEXTURE_PAINT_GLSL = (
402
+ /* glsl */
403
+ `
404
+ uniform sampler2D u_sampler;
405
+ vec4 shadePaint(vec2 uv) {
406
+ return texture(u_sampler, uv);
407
+ }
408
+ `
409
+ );
410
+ var GLYPH_PATTERN_FRAG_SRC = fragSrc(PATTERN_PAINT_GLSL);
411
+ var GLYPH_GRAD_FRAG_SRC = fragSrc(GRAD_PAINT_GLSL);
412
+ var GLYPH_TEXTURE_FRAG_SRC = fragSrc(TEXTURE_PAINT_GLSL);
413
+ var GLYPH_UNIFORMS = ["u_atlas", "u_synthBold", "u_fieldScale"];
414
+ var notOrigin = (name) => name !== "u_origin";
415
+ var GLYPH_PATTERN_UNIFORMS = [
416
+ ...PATTERN_FILL_UNIFORMS.filter(notOrigin),
417
+ ...GLYPH_UNIFORMS
418
+ ];
419
+ var GLYPH_GRAD_UNIFORMS = [
420
+ ...GRAD_FILL_UNIFORMS.filter(notOrigin),
421
+ ...GLYPH_UNIFORMS
422
+ ];
423
+ var GLYPH_TEXTURE_UNIFORMS = [
424
+ "u_proj",
425
+ "u_model",
426
+ "u_worldInv",
427
+ "u_sampler",
428
+ ...GLYPH_UNIFORMS
429
+ ];
430
+ var GLYPH_PAINT_ATTRIBUTES = ["a_position", "a_uv", "a_texSlot"];
431
+
432
+ // src/paintScratch.ts
433
+ var MIN_SIDE = 64;
434
+ var PaintScratch = class {
435
+ gl;
436
+ target = null;
437
+ constructor(gl) {
438
+ this.gl = gl;
439
+ }
440
+ /** A buffer at least `width` × `height` device pixels. Its contents are
441
+ * whatever the last user left; the caller clears the region it uses. */
442
+ acquire(width, height) {
443
+ const have = this.target;
444
+ if (have && have.width >= width && have.height >= height) return have;
445
+ const w = Math.max(have?.width ?? 0, pow2(width));
446
+ const h = Math.max(have?.height ?? 0, pow2(height));
447
+ this.release();
448
+ this.target = this.create(w, h);
449
+ return this.target;
450
+ }
451
+ /** Drop the buffer. Called on dispose and on context restore, where the
452
+ * handles are already dead and only the bookkeeping has to go. */
453
+ release() {
454
+ if (!this.target) return;
455
+ this.gl.deleteFramebuffer(this.target.fbo);
456
+ this.gl.deleteTexture(this.target.texture);
457
+ this.target = null;
458
+ }
459
+ create(width, height) {
460
+ const gl = this.gl;
461
+ const texture = gl.createTexture();
462
+ gl.bindTexture(gl.TEXTURE_2D, texture);
463
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
464
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
465
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
466
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
467
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
468
+ gl.bindTexture(gl.TEXTURE_2D, null);
469
+ const fbo = gl.createFramebuffer();
470
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
471
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
472
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
473
+ return { fbo, texture, width, height };
474
+ }
475
+ };
476
+ function pow2(n) {
477
+ let side = MIN_SIDE;
478
+ while (side < n) side *= 2;
479
+ return side;
480
+ }
481
+
482
+ // src/cache/GLMeshCache.ts
483
+ var GLMeshCache = class {
484
+ constructor(gl) {
485
+ this.gl = gl;
486
+ this.finalizer = new FinalizationRegistry((resources) => {
487
+ if (!this.persistent.delete(resources)) return;
488
+ this.pendingDeletes.push(resources);
489
+ });
490
+ }
491
+ gl;
492
+ map = /* @__PURE__ */ new WeakMap();
493
+ /** Meshes this context has drawn at least once; see `uploadRecurring`. */
494
+ seen = /* @__PURE__ */ new WeakSet();
495
+ finalizer;
496
+ /** Every persistent upload not yet freed, so `dispose` can reach them. */
497
+ persistent = /* @__PURE__ */ new Set();
498
+ pendingDeletes = [];
499
+ disposed = false;
500
+ /** Transient resources allocated this frame; freed at end of render(). */
501
+ transientThisFrame = [];
502
+ handleFor(mesh) {
503
+ const cached = this.map.get(mesh);
504
+ if (cached) return cached;
505
+ const { handle, vbo, ibo } = this.upload(mesh);
506
+ this.map.set(mesh, handle);
507
+ const resources = { vao: handle.vao, vbo, ibo };
508
+ this.persistent.add(resources);
509
+ this.finalizer.register(mesh, resources, mesh);
510
+ return handle;
511
+ }
512
+ /**
513
+ * Upload a Mesh that the caller knows is single-use (e.g. the per-frame
514
+ * stroke ribbon from `tessellateStroke`). Returns a handle backed by fresh
515
+ * GL resources that the renderer will free deterministically at the end
516
+ * of the current frame via `freeTransient()`. Bypasses the WeakMap cache
517
+ * and the FinalizationRegistry, so transient meshes never wait on GC.
518
+ */
519
+ uploadTransient(mesh) {
520
+ const { handle, vbo, ibo } = this.upload(mesh);
521
+ this.transientThisFrame.push({ vao: handle.vao, vbo, ibo });
522
+ return handle;
523
+ }
524
+ /**
525
+ * Upload a Mesh that may or may not recur across frames. Its first sight in
526
+ * *this* context takes `uploadTransient`; every later one takes the
527
+ * persistent handle. One transient upload to find out is cheaper than
528
+ * stranding a persistent VAO on a mesh that never returns, whose release
529
+ * would wait on GC. The transient upload does not populate the persistent
530
+ * map, so steady-state reuse begins on the third frame.
531
+ */
532
+ uploadRecurring(mesh) {
533
+ if (this.seen.has(mesh)) return this.handleFor(mesh);
534
+ this.seen.add(mesh);
535
+ return this.uploadTransient(mesh);
536
+ }
537
+ /**
538
+ * Free all transient resources allocated since the last call. Called by
539
+ * the renderer at the end of each `render()`. Safe under context loss.
540
+ */
541
+ freeTransient() {
542
+ if (this.transientThisFrame.length === 0) return;
543
+ const gl = this.gl;
544
+ if (gl.isContextLost()) {
545
+ this.transientThisFrame.length = 0;
546
+ return;
547
+ }
548
+ for (const r of this.transientThisFrame) {
549
+ gl.deleteVertexArray(r.vao);
550
+ gl.deleteBuffer(r.vbo);
551
+ gl.deleteBuffer(r.ibo);
552
+ }
553
+ this.transientThisFrame.length = 0;
554
+ }
555
+ /** @internal — for tests asserting the transient-list size. */
556
+ _transientCount() {
557
+ return this.transientThisFrame.length;
558
+ }
559
+ /**
560
+ * Free GL resources whose Mesh has been GC'd since the last drain.
561
+ * Called by the renderer at the top of each `render()`, before any draws.
562
+ */
563
+ drainPendingDeletes() {
564
+ if (this.pendingDeletes.length === 0) return;
565
+ const gl = this.gl;
566
+ if (gl.isContextLost()) {
567
+ this.pendingDeletes.length = 0;
568
+ return;
569
+ }
570
+ for (const r of this.pendingDeletes) {
571
+ gl.deleteVertexArray(r.vao);
572
+ gl.deleteBuffer(r.vbo);
573
+ gl.deleteBuffer(r.ibo);
574
+ }
575
+ this.pendingDeletes.length = 0;
576
+ }
577
+ /**
578
+ * Free every GL resource this cache holds — persistent, pending and
579
+ * transient — and ignore finalizers from here on. Idempotent. Unlike the
580
+ * other paths this does not wait on GC, which is what lets a renderer on a
581
+ * context it does not own give back everything it uploaded.
582
+ */
583
+ dispose() {
584
+ if (this.disposed) return;
585
+ this.disposed = true;
586
+ this.freeTransient();
587
+ for (const r of this.persistent) this.pendingDeletes.push(r);
588
+ this.persistent.clear();
589
+ this.drainPendingDeletes();
590
+ }
591
+ /** @internal — for tests asserting the queue size. */
592
+ _pendingDeleteCount() {
593
+ return this.pendingDeletes.length;
594
+ }
595
+ /** @internal — for tests that need to simulate the finalizer firing. */
596
+ _enqueueDeleteForTest(resources) {
597
+ this.pendingDeletes.push(resources);
598
+ }
599
+ upload(mesh) {
600
+ const gl = this.gl;
601
+ const vao = gl.createVertexArray();
602
+ if (!vao) throw new Error("createVertexArray returned null");
603
+ gl.bindVertexArray(vao);
604
+ const vbo = gl.createBuffer();
605
+ if (!vbo) throw new Error("createBuffer (VBO) returned null");
606
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
607
+ gl.bufferData(gl.ARRAY_BUFFER, mesh.vertices, gl.STATIC_DRAW);
608
+ gl.enableVertexAttribArray(POSITION_ATTRIBUTE_LOCATION);
609
+ gl.vertexAttribPointer(POSITION_ATTRIBUTE_LOCATION, 2, gl.FLOAT, false, 0, 0);
610
+ const ibo = gl.createBuffer();
611
+ if (!ibo) throw new Error("createBuffer (IBO) returned null");
612
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
613
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, mesh.indices, gl.STATIC_DRAW);
614
+ gl.bindVertexArray(null);
615
+ return {
616
+ handle: {
617
+ vao,
618
+ indexCount: mesh.indices.length,
619
+ requiresStencil: mesh.requiresStencil ?? false,
620
+ anchorA: mesh.anchorA,
621
+ anchorB: mesh.anchorB,
622
+ anchorT: mesh.anchorT
623
+ },
624
+ vbo,
625
+ ibo
626
+ };
627
+ }
628
+ };
629
+
630
+ // src/cache/GLTextureCache.ts
631
+ var GLTextureCache = class {
632
+ constructor(gl) {
633
+ this.gl = gl;
634
+ }
635
+ gl;
636
+ map = /* @__PURE__ */ new Map();
637
+ wraps = /* @__PURE__ */ new Map();
638
+ /** The source each registry-backed id was last uploaded from. */
639
+ registered = /* @__PURE__ */ new Map();
640
+ sweptVersion = -1;
641
+ has(id) {
642
+ return this.map.has(id);
643
+ }
644
+ /** Uploads the pixels once per id. `wrap` is re-applied whenever it differs
645
+ * from what the id was last uploaded under: the same registered image can
646
+ * be a clamped shader texture in one draw and a repeating pattern tile in
647
+ * the next, and first-upload-wins gave the second one the first one's mode. */
648
+ upload(id, source, wrap = "clamp") {
649
+ const cached = this.map.get(id);
650
+ if (cached) {
651
+ if (this.wraps.get(id) !== wrap) this.applyWrap(id, cached, wrap);
652
+ return id;
653
+ }
654
+ const gl = this.gl;
655
+ const tex = gl.createTexture();
656
+ if (!tex) throw new Error(`GLTextureCache: createTexture failed for id="${id}"`);
657
+ const wrapMode = wrap === "repeat" ? gl.REPEAT : gl.CLAMP_TO_EDGE;
658
+ gl.bindTexture(gl.TEXTURE_2D, tex);
659
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
660
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
661
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
662
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapMode);
663
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapMode);
664
+ gl.bindTexture(gl.TEXTURE_2D, null);
665
+ this.map.set(id, tex);
666
+ this.wraps.set(id, wrap);
667
+ return id;
668
+ }
669
+ /** `upload` for an id from the texture registry, whose image can be replaced
670
+ * under the same id: a changed source sends its pixels again. */
671
+ uploadRegistered(id, source, wrap = "clamp") {
672
+ const cached = this.map.get(id);
673
+ if (cached && this.registered.get(id) !== source) {
674
+ const gl = this.gl;
675
+ gl.bindTexture(gl.TEXTURE_2D, cached);
676
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
677
+ gl.bindTexture(gl.TEXTURE_2D, null);
678
+ }
679
+ this.registered.set(id, source);
680
+ return this.upload(id, source, wrap);
681
+ }
682
+ /**
683
+ * Delete the texture of every registry-backed id the registry has dropped.
684
+ * The renderer calls this at the top of `render()`, before any draw, so a
685
+ * texture is never deleted under a draw that has it bound.
686
+ */
687
+ sweep(registry) {
688
+ const version = registry.getVersion();
689
+ if (version === this.sweptVersion) return;
690
+ this.sweptVersion = version;
691
+ for (const id of this.registered.keys()) {
692
+ if (registry.has(id)) continue;
693
+ this.gl.deleteTexture(this.map.get(id) ?? null);
694
+ this.map.delete(id);
695
+ this.wraps.delete(id);
696
+ this.registered.delete(id);
697
+ }
698
+ }
699
+ applyWrap(id, tex, wrap) {
700
+ const gl = this.gl;
701
+ const mode = wrap === "repeat" ? gl.REPEAT : gl.CLAMP_TO_EDGE;
702
+ gl.bindTexture(gl.TEXTURE_2D, tex);
703
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, mode);
704
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, mode);
705
+ gl.bindTexture(gl.TEXTURE_2D, null);
706
+ this.wraps.set(id, wrap);
707
+ }
708
+ /** Create a single-channel R8 texture from raw bytes (full upload).
709
+ * No-op if `id` already exists. Same LINEAR/CLAMP params as `upload`;
710
+ * UNPACK_ALIGNMENT dropped to 1 for non-4-aligned row widths. */
711
+ uploadR8(id, width, height, data) {
712
+ if (this.map.has(id)) return;
713
+ const gl = this.gl;
714
+ const tex = gl.createTexture();
715
+ if (!tex) throw new Error(`GLTextureCache: createTexture failed for id="${id}"`);
716
+ gl.bindTexture(gl.TEXTURE_2D, tex);
717
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
718
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8, width, height, 0, gl.RED, gl.UNSIGNED_BYTE, data);
719
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
720
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
721
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
722
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
723
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
724
+ gl.bindTexture(gl.TEXTURE_2D, null);
725
+ this.map.set(id, tex);
726
+ this.wraps.set(id, "clamp");
727
+ }
728
+ /** Patch a rect of an existing R8 texture with tightly-packed w×h bytes. */
729
+ subImageR8(id, x, y, w, h, data) {
730
+ const tex = this.map.get(id);
731
+ if (!tex) throw new Error(`GLTextureCache: texture "${id}" not uploaded`);
732
+ const gl = this.gl;
733
+ gl.bindTexture(gl.TEXTURE_2D, tex);
734
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
735
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, w, h, gl.RED, gl.UNSIGNED_BYTE, data);
736
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
737
+ gl.bindTexture(gl.TEXTURE_2D, null);
738
+ }
739
+ bind(id, unit) {
740
+ const tex = this.map.get(id);
741
+ if (!tex) throw new Error(`GLTextureCache: texture "${id}" not uploaded`);
742
+ const gl = this.gl;
743
+ gl.activeTexture(gl.TEXTURE0 + unit);
744
+ gl.bindTexture(gl.TEXTURE_2D, tex);
745
+ }
746
+ /**
747
+ * Delete every uploaded GL texture and clear the map. Called by
748
+ * `WeaselRenderer.dispose()`. The cache is unusable but refillable
749
+ * afterward — a subsequent `upload()` for a previously-seen id re-creates
750
+ * the texture rather than restoring the deleted one.
751
+ */
752
+ free() {
753
+ const gl = this.gl;
754
+ for (const tex of this.map.values()) {
755
+ gl.deleteTexture(tex);
756
+ }
757
+ this.map.clear();
758
+ this.wraps.clear();
759
+ this.registered.clear();
760
+ }
761
+ };
762
+ var RAMP_WIDTH = 256;
763
+ var INITIAL_ROWS = 16;
764
+ var RAMP_ATLAS_MAX_ROWS = 1024;
765
+ function buildGradientRamp(stops, space = "rgb") {
766
+ const data = new Uint8ClampedArray(RAMP_WIDTH * 4);
767
+ if (stops.length === 0) return data;
768
+ const resolved = resolveGradientStops(stops);
769
+ for (let i = 0; i < RAMP_WIDTH; i++) {
770
+ const [r, g, b, a] = sampleResolvedStops(resolved, i / (RAMP_WIDTH - 1), space);
771
+ data[i * 4] = r * 255;
772
+ data[i * 4 + 1] = g * 255;
773
+ data[i * 4 + 2] = b * 255;
774
+ data[i * 4 + 3] = a * 255;
775
+ }
776
+ return data;
777
+ }
778
+ function rampKey(stops, space) {
779
+ return `${space}|${JSON.stringify(stops)}`;
780
+ }
781
+ var GradientRampAtlas = class {
782
+ constructor(gl) {
783
+ this.gl = gl;
784
+ }
785
+ gl;
786
+ /** Row per stop list, in least-recently-used order: a hit re-inserts, so the
787
+ * first entry is the row a full atlas recycles. */
788
+ rowByKey = /* @__PURE__ */ new Map();
789
+ texture = null;
790
+ rows = 0;
791
+ /** Mirror of the texture, so growth can respecify it without re-baking every
792
+ * ramp it already holds. */
793
+ pixels = new Uint8ClampedArray(0);
794
+ totalQueries = 0;
795
+ cacheHits = 0;
796
+ /** Rows the atlas currently holds — its texture height. */
797
+ get height() {
798
+ return this.rows;
799
+ }
800
+ /**
801
+ * Bake `stops` into a row and return it, reusing the row an identical stop
802
+ * list already holds.
803
+ *
804
+ * **A returned row outlives the frame only while the atlas has room.** Past
805
+ * `RAMP_ATLAS_MAX_ROWS` an upload recycles the least recently used row, which
806
+ * rewrites texels a row handed out earlier still names. Nothing that defers
807
+ * its draw past this call may hold a row across another `upload` without
808
+ * arranging to be flushed first.
809
+ */
810
+ upload(stops, space = "rgb") {
811
+ const key = rampKey(stops, space);
812
+ this.totalQueries++;
813
+ const existing = this.rowByKey.get(key);
814
+ if (existing !== void 0) {
815
+ this.cacheHits++;
816
+ this.rowByKey.delete(key);
817
+ this.rowByKey.set(key, existing);
818
+ return existing;
819
+ }
820
+ const row = this.claimRow();
821
+ this.writeRow(row, buildGradientRamp(stops, space));
822
+ this.rowByKey.set(key, row);
823
+ return row;
824
+ }
825
+ /**
826
+ * Whether uploading `stops` would move where existing rows sit — by growing
827
+ * the atlas, which changes every row's `v`, or by recycling one, which
828
+ * rewrites its texels.
829
+ *
830
+ * Anything holding a row past this call asks first and gets itself out of
831
+ * the way, because neither can be undone once it has happened.
832
+ */
833
+ wouldReshape(stops, space = "rgb") {
834
+ if (this.rowByKey.size === 0) return false;
835
+ return !this.rowByKey.has(rampKey(stops, space)) && this.rowByKey.size >= this.rows;
836
+ }
837
+ /**
838
+ * The `v` a row is sampled at — its center.
839
+ *
840
+ * The center is load-bearing: the atlas filters LINEAR, so a `v` anywhere
841
+ * else blends the ramp beside it into this one. At the center the neighbor's
842
+ * weight is exactly zero.
843
+ */
844
+ rowV(row) {
845
+ return (row + 0.5) / (this.rows || 1);
846
+ }
847
+ bind(unit) {
848
+ const gl = this.gl;
849
+ gl.activeTexture(gl.TEXTURE0 + unit);
850
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
851
+ }
852
+ hitRate() {
853
+ if (this.totalQueries === 0) return 0;
854
+ return this.cacheHits / this.totalQueries;
855
+ }
856
+ resetStats() {
857
+ this.totalQueries = 0;
858
+ this.cacheHits = 0;
859
+ }
860
+ /**
861
+ * Delete the atlas texture and forget every row in it. Called by
862
+ * `WeaselRenderer.dispose()`. The CPU mirror goes with it, so the atlas is
863
+ * unusable but refillable afterward (stats are left as-is; call
864
+ * `resetStats()` separately if desired).
865
+ */
866
+ free() {
867
+ if (this.texture) this.gl.deleteTexture(this.texture);
868
+ this.texture = null;
869
+ this.rows = 0;
870
+ this.pixels = new Uint8ClampedArray(0);
871
+ this.rowByKey.clear();
872
+ }
873
+ /** The row the next ramp is written to: a free one, a taller atlas, or the
874
+ * least recently used row of a full one. */
875
+ claimRow() {
876
+ if (this.rowByKey.size < this.rows) return this.rowByKey.size;
877
+ if (this.rows < RAMP_ATLAS_MAX_ROWS) {
878
+ this.resize(this.rows === 0 ? INITIAL_ROWS : this.rows * 2);
879
+ return this.rowByKey.size;
880
+ }
881
+ const [lruKey, lruRow] = this.rowByKey.entries().next().value;
882
+ this.rowByKey.delete(lruKey);
883
+ return lruRow;
884
+ }
885
+ /** Grow to `rows`, keeping every row at the index it already had — a row
886
+ * index is a stable name, and callers hold them. */
887
+ resize(rows) {
888
+ const gl = this.gl;
889
+ const next = new Uint8ClampedArray(RAMP_WIDTH * rows * 4);
890
+ next.set(this.pixels);
891
+ this.pixels = next;
892
+ this.rows = rows;
893
+ if (!this.texture) {
894
+ const tex = gl.createTexture();
895
+ if (!tex) throw new Error("GradientRampAtlas: createTexture failed");
896
+ this.texture = tex;
897
+ gl.bindTexture(gl.TEXTURE_2D, tex);
898
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
899
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
900
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
901
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
902
+ } else {
903
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
904
+ }
905
+ gl.texImage2D(
906
+ gl.TEXTURE_2D,
907
+ 0,
908
+ gl.RGBA,
909
+ RAMP_WIDTH,
910
+ rows,
911
+ 0,
912
+ gl.RGBA,
913
+ gl.UNSIGNED_BYTE,
914
+ this.pixels
915
+ );
916
+ gl.bindTexture(gl.TEXTURE_2D, null);
917
+ }
918
+ writeRow(row, ramp2) {
919
+ const gl = this.gl;
920
+ this.pixels.set(ramp2, row * RAMP_WIDTH * 4);
921
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
922
+ gl.texSubImage2D(
923
+ gl.TEXTURE_2D,
924
+ 0,
925
+ 0,
926
+ row,
927
+ RAMP_WIDTH,
928
+ 1,
929
+ gl.RGBA,
930
+ gl.UNSIGNED_BYTE,
931
+ ramp2
932
+ );
933
+ gl.bindTexture(gl.TEXTURE_2D, null);
934
+ }
935
+ };
936
+ var IDENTITY_COLOR_MATRIX = new Float32Array([
937
+ 1,
938
+ 0,
939
+ 0,
940
+ 0,
941
+ 0,
942
+ 0,
943
+ 1,
944
+ 0,
945
+ 0,
946
+ 0,
947
+ 0,
948
+ 0,
949
+ 1,
950
+ 0,
951
+ 0,
952
+ 0,
953
+ 0,
954
+ 0,
955
+ 1,
956
+ 0
957
+ ]);
958
+ function compose4x5(outer, inner) {
959
+ const result = new Float32Array(20);
960
+ for (let row = 0; row < 4; row++) {
961
+ for (let col = 0; col < 4; col++) {
962
+ let sum = 0;
963
+ for (let k = 0; k < 4; k++) {
964
+ sum += outer[row * 5 + k] * inner[k * 5 + col];
965
+ }
966
+ result[row * 5 + col] = sum;
967
+ }
968
+ let biasSum = outer[row * 5 + 4];
969
+ for (let k = 0; k < 4; k++) {
970
+ biasSum += outer[row * 5 + k] * inner[k * 5 + 4];
971
+ }
972
+ result[row * 5 + 4] = biasSum;
973
+ }
974
+ return result;
975
+ }
976
+ var GroupState = class {
977
+ transformStack = [mat3.identity()];
978
+ alphaStack = [1];
979
+ colorMatrixStack = [IDENTITY_COLOR_MATRIX];
980
+ get transform() {
981
+ return this.transformStack[this.transformStack.length - 1];
982
+ }
983
+ get alpha() {
984
+ return this.alphaStack[this.alphaStack.length - 1];
985
+ }
986
+ get colorMatrix() {
987
+ return this.colorMatrixStack[this.colorMatrixStack.length - 1];
988
+ }
989
+ push(frame) {
990
+ const current = this.transform;
991
+ const nextTransform = frame.transform ? mat3.multiply(current, frame.transform) : current;
992
+ const nextAlpha = frame.alpha !== void 0 ? this.alpha * frame.alpha : this.alpha;
993
+ const nextCM = frame.colorMatrix ? compose4x5(
994
+ this.colorMatrix,
995
+ frame.colorMatrix instanceof Float32Array ? frame.colorMatrix : new Float32Array(frame.colorMatrix)
996
+ ) : this.colorMatrix;
997
+ this.transformStack.push(nextTransform);
998
+ this.alphaStack.push(nextAlpha);
999
+ this.colorMatrixStack.push(nextCM);
1000
+ }
1001
+ /**
1002
+ * Push a frame whose children paint into a surface of their own.
1003
+ *
1004
+ * The transform still accumulates — the children draw where they would have
1005
+ * drawn. Alpha and colour do not: they describe how the finished surface
1006
+ * joins the frame, and applying them on the way in as well would fade the
1007
+ * pixels an effect is about to read, then fade them again on the way out.
1008
+ *
1009
+ * Returns the pair the caller must apply at composite time — what `push`
1010
+ * would have left on the stack — so the composition rules live here and not
1011
+ * in the dispatcher.
1012
+ */
1013
+ pushIsolated(frame) {
1014
+ this.push(frame);
1015
+ const composited = { alpha: this.alpha, colorMatrix: this.colorMatrix };
1016
+ this.pop();
1017
+ const current = this.transform;
1018
+ this.transformStack.push(frame.transform ? mat3.multiply(current, frame.transform) : current);
1019
+ this.alphaStack.push(1);
1020
+ this.colorMatrixStack.push(IDENTITY_COLOR_MATRIX);
1021
+ return composited;
1022
+ }
1023
+ /** Drop every pushed frame, leaving the root. A frame that throws part-way
1024
+ * down the tree never pops, and the next frame would draw under leftovers. */
1025
+ reset() {
1026
+ this.transformStack.length = 1;
1027
+ this.alphaStack.length = 1;
1028
+ this.colorMatrixStack.length = 1;
1029
+ }
1030
+ pop() {
1031
+ if (this.transformStack.length <= 1) {
1032
+ throw new Error("GroupState.pop: cannot pop root frame");
1033
+ }
1034
+ this.transformStack.pop();
1035
+ this.alphaStack.pop();
1036
+ this.colorMatrixStack.pop();
1037
+ }
1038
+ };
1039
+
1040
+ // src/effects/effectPrelude.ts
1041
+ var EFFECT_VERT_SRC = (
1042
+ /* glsl */
1043
+ `#version 300 es
1044
+ in vec2 a_position;
1045
+ in vec2 a_uv;
1046
+ out vec2 v_uv;
1047
+
1048
+ void main() {
1049
+ v_uv = a_uv;
1050
+ gl_Position = vec4(a_position, 0.0, 1.0);
1051
+ }
1052
+ `
1053
+ );
1054
+ var EFFECT_KIT_UNIFORMS = ["u_source", "u_resolution", "u_texel"];
1055
+ var COMPOSITE_FRAG_SRC = (
1056
+ /* glsl */
1057
+ `#version 300 es
1058
+ precision highp float;
1059
+ in vec2 v_uv;
1060
+ uniform sampler2D u_source;
1061
+ uniform float u_alpha;
1062
+ uniform mat4 u_colorMatrix;
1063
+ uniform vec4 u_colorBias;
1064
+ out vec4 outColor;
1065
+
1066
+ void main() {
1067
+ vec4 src = texture(u_source, v_uv);
1068
+ // Undo premultiplication before the colour matrix, which is defined on
1069
+ // straight alpha, then put it back for the blend.
1070
+ float a = src.a;
1071
+ vec3 rgb = a > 0.0 ? src.rgb / a : vec3(0.0);
1072
+ vec4 c = u_colorMatrix * vec4(rgb, a) + u_colorBias;
1073
+ c.a *= u_alpha;
1074
+ outColor = vec4(clamp(c.rgb, 0.0, 1.0) * c.a, c.a);
1075
+ }
1076
+ `
1077
+ );
1078
+
1079
+ // src/effects/composite.ts
1080
+ var COMPOSITE_PROGRAM_ID = "weasel:effect-composite";
1081
+ function compositeProgramId() {
1082
+ if (!getProgramSource(COMPOSITE_PROGRAM_ID)) {
1083
+ registerProgram(COMPOSITE_PROGRAM_ID, EFFECT_VERT_SRC, COMPOSITE_FRAG_SRC);
1084
+ }
1085
+ return COMPOSITE_PROGRAM_ID;
1086
+ }
1087
+
1088
+ // src/draw/drawTriangles.ts
1089
+ function drawTriangles(ctx, count, type) {
1090
+ ctx.gl.drawElements(ctx.gl.TRIANGLES, count, type, 0);
1091
+ if (ctx.stats) ctx.stats.drawCalls++;
1092
+ }
1093
+
1094
+ // src/draw/stencil.ts
1095
+ function applyClipTest(ctx, depth = ctx.clipDepth) {
1096
+ const gl = ctx.gl;
1097
+ if (depth === 0) {
1098
+ gl.disable(gl.STENCIL_TEST);
1099
+ return;
1100
+ }
1101
+ const mask = ancestorMask(depth);
1102
+ gl.enable(gl.STENCIL_TEST);
1103
+ gl.stencilFunc(gl.EQUAL, mask, mask);
1104
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
1105
+ }
1106
+ function ancestorMask(depth) {
1107
+ return depth === 0 ? 0 : (1 << depth + 1) - 1 & 254;
1108
+ }
1109
+ function beginParityMask(ctx, indexCount) {
1110
+ const gl = ctx.gl;
1111
+ gl.enable(gl.STENCIL_TEST);
1112
+ gl.colorMask(false, false, false, false);
1113
+ gl.stencilMask(1);
1114
+ gl.stencilFunc(gl.ALWAYS, 0, 1);
1115
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.INVERT);
1116
+ drawTriangles(ctx, indexCount, ctx.gl.UNSIGNED_INT);
1117
+ const clipMask = ancestorMask(ctx.clipDepth);
1118
+ gl.colorMask(true, true, true, true);
1119
+ gl.stencilFunc(gl.EQUAL, clipMask | 1, clipMask | 1);
1120
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
1121
+ }
1122
+ function endParityMask(ctx) {
1123
+ const gl = ctx.gl;
1124
+ gl.stencilMask(1);
1125
+ gl.clear(gl.STENCIL_BUFFER_BIT);
1126
+ gl.disable(gl.STENCIL_TEST);
1127
+ }
1128
+
1129
+ // src/draw/customShader.ts
1130
+ var warnedUniforms = /* @__PURE__ */ new Set();
1131
+ function warnOnceUniform(programId, name) {
1132
+ const key = `${programId}:${name}`;
1133
+ if (warnedUniforms.has(key)) return;
1134
+ warnedUniforms.add(key);
1135
+ const isDev = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
1136
+ if (isDev) {
1137
+ console.warn(`weasel drawShader: uniform "${name}" not found in program "${programId}". Check spelling, ensure it's used in the shader (unused uniforms are optimized away by the driver).`);
1138
+ }
1139
+ }
1140
+ function setUniform(gl, loc, value, textureCache, nextTexUnit) {
1141
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Float32Array) && "id" in value && typeof value.id === "string") {
1142
+ const handle = value;
1143
+ const entry = getTexture(handle.id);
1144
+ if (!entry) {
1145
+ const isDev = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
1146
+ if (isDev) console.warn(`weasel setUniform: TextureHandle "${handle.id}" not registered`);
1147
+ return;
1148
+ }
1149
+ const unit = nextTexUnit.value++;
1150
+ gl.activeTexture(gl.TEXTURE0 + unit);
1151
+ textureCache.uploadRegistered(handle.id, entry.source);
1152
+ textureCache.bind(handle.id, unit);
1153
+ gl.uniform1i(loc, unit);
1154
+ return;
1155
+ }
1156
+ if (value instanceof Float64Array) value = Float32Array.from(value);
1157
+ if (value instanceof Float32Array) {
1158
+ if (value.length === 9) {
1159
+ gl.uniformMatrix3fv(loc, false, value);
1160
+ } else if (value.length === 16) {
1161
+ gl.uniformMatrix4fv(loc, false, value);
1162
+ } else {
1163
+ const isDev = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
1164
+ if (isDev) throw new TypeError(`weasel setUniform: Float32Array must be length 9 (mat3) or 16 (mat4), got ${value.length}`);
1165
+ }
1166
+ return;
1167
+ }
1168
+ if (Array.isArray(value)) {
1169
+ const arr = value;
1170
+ switch (arr.length) {
1171
+ case 2:
1172
+ gl.uniform2fv(loc, arr);
1173
+ break;
1174
+ case 3:
1175
+ gl.uniform3fv(loc, arr);
1176
+ break;
1177
+ case 4:
1178
+ gl.uniform4fv(loc, arr);
1179
+ break;
1180
+ default: {
1181
+ const isDev = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
1182
+ if (isDev) throw new TypeError(`weasel setUniform: array length ${arr.length} not supported`);
1183
+ }
1184
+ }
1185
+ return;
1186
+ }
1187
+ if (typeof value === "number") {
1188
+ gl.uniform1f(loc, value);
1189
+ return;
1190
+ }
1191
+ }
1192
+ var ARRAY_UPLOADS = {
1193
+ float: { fn: "uniform1fv", n: 1 },
1194
+ vec2: { fn: "uniform2fv", n: 2 },
1195
+ vec3: { fn: "uniform3fv", n: 3 },
1196
+ vec4: { fn: "uniform4fv", n: 4 },
1197
+ int: { fn: "uniform1iv", n: 1 },
1198
+ ivec2: { fn: "uniform2iv", n: 2 },
1199
+ ivec3: { fn: "uniform3iv", n: 3 },
1200
+ ivec4: { fn: "uniform4iv", n: 4 },
1201
+ bool: { fn: "uniform1iv", n: 1 },
1202
+ bvec2: { fn: "uniform2iv", n: 2 },
1203
+ bvec3: { fn: "uniform3iv", n: 3 },
1204
+ bvec4: { fn: "uniform4iv", n: 4 },
1205
+ uint: { fn: "uniform1uiv", n: 1 },
1206
+ uvec2: { fn: "uniform2uiv", n: 2 },
1207
+ uvec3: { fn: "uniform3uiv", n: 3 },
1208
+ uvec4: { fn: "uniform4uiv", n: 4 },
1209
+ mat2: { fn: "uniformMatrix2fv", n: 4, matrix: true },
1210
+ mat3: { fn: "uniformMatrix3fv", n: 9, matrix: true },
1211
+ mat4: { fn: "uniformMatrix4fv", n: 16, matrix: true },
1212
+ mat2x2: { fn: "uniformMatrix2fv", n: 4, matrix: true },
1213
+ mat3x3: { fn: "uniformMatrix3fv", n: 9, matrix: true },
1214
+ mat4x4: { fn: "uniformMatrix4fv", n: 16, matrix: true },
1215
+ mat2x3: { fn: "uniformMatrix2x3fv", n: 6, matrix: true },
1216
+ mat2x4: { fn: "uniformMatrix2x4fv", n: 8, matrix: true },
1217
+ mat3x2: { fn: "uniformMatrix3x2fv", n: 6, matrix: true },
1218
+ mat3x4: { fn: "uniformMatrix3x4fv", n: 12, matrix: true },
1219
+ mat4x2: { fn: "uniformMatrix4x2fv", n: 8, matrix: true },
1220
+ mat4x3: { fn: "uniformMatrix4x3fv", n: 12, matrix: true }
1221
+ };
1222
+ function isArrayValue(value) {
1223
+ return Array.isArray(value) || value instanceof Float32Array || value instanceof Int32Array || value instanceof Uint32Array;
1224
+ }
1225
+ function setUniformArray(gl, name, arr, value) {
1226
+ const isDev = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
1227
+ const upload = ARRAY_UPLOADS[arr.type];
1228
+ if (!upload) {
1229
+ if (isDev) console.warn(`weasel setUniform: array uniform "${name}" has type ${arr.type}, which cannot be written as a whole`);
1230
+ return;
1231
+ }
1232
+ if (value.length % upload.n !== 0 || value.length > upload.n * arr.size) {
1233
+ if (isDev) {
1234
+ throw new TypeError(
1235
+ `weasel setUniform: "${name}" is ${arr.type}[${arr.size}], so its value must hold a multiple of ${upload.n} numbers, at most ${upload.n * arr.size}; got ${value.length}`
1236
+ );
1237
+ }
1238
+ return;
1239
+ }
1240
+ if (value.length === 0) return;
1241
+ const fn = gl[upload.fn];
1242
+ if (upload.matrix) fn.call(gl, arr.location, false, value);
1243
+ else fn.call(gl, arr.location, value);
1244
+ }
1245
+ function setUniforms(gl, program, programId, uniforms, textureCache, nextTexUnit) {
1246
+ for (const [name, value] of Object.entries(uniforms)) {
1247
+ const arr = program.uniformArray(name);
1248
+ if (arr && isArrayValue(value)) {
1249
+ setUniformArray(gl, name, arr, value);
1250
+ continue;
1251
+ }
1252
+ const loc = program.uniform(name);
1253
+ if (loc === void 0) {
1254
+ warnOnceUniform(programId, name);
1255
+ continue;
1256
+ }
1257
+ setUniform(gl, loc, value, textureCache, nextTexUnit);
1258
+ }
1259
+ }
1260
+ function drawShader(ctx, cmd) {
1261
+ const { gl, programRegistry, quadVbo, quadIbo, textureCache } = ctx;
1262
+ const program = programRegistry.get(cmd.program.id);
1263
+ if (!program) {
1264
+ console.warn(
1265
+ `weasel drawShader: program "${cmd.program.id}" not compiled on this renderer. Call renderer.registerProgram(handle) after the module-level registerProgram().`
1266
+ );
1267
+ return;
1268
+ }
1269
+ if (!quadVbo || !quadIbo) {
1270
+ console.warn("weasel drawShader: quad geometry not initialized");
1271
+ return;
1272
+ }
1273
+ gl.useProgram(program.handle);
1274
+ const aPosLoc = program.attribute("a_position");
1275
+ const aUvLoc = program.attribute("a_uv");
1276
+ gl.bindBuffer(gl.ARRAY_BUFFER, quadVbo);
1277
+ if (aPosLoc !== void 0) {
1278
+ gl.enableVertexAttribArray(aPosLoc);
1279
+ gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, 16, 0);
1280
+ }
1281
+ if (aUvLoc !== void 0) {
1282
+ gl.enableVertexAttribArray(aUvLoc);
1283
+ gl.vertexAttribPointer(aUvLoc, 2, gl.FLOAT, false, 16, 8);
1284
+ }
1285
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, quadIbo);
1286
+ const proj = mat3.screenToClip(ctx.widthCss, ctx.heightCss);
1287
+ const uProj = program.uniform("u_proj");
1288
+ if (uProj !== void 0) gl.uniformMatrix3fv(uProj, false, mat3.toUniform(proj));
1289
+ const uBounds = program.uniform("u_bounds");
1290
+ if (uBounds !== void 0) {
1291
+ gl.uniform4f(uBounds, cmd.bounds.x, cmd.bounds.y, cmd.bounds.w, cmd.bounds.h);
1292
+ }
1293
+ const uView = program.uniform("u_view");
1294
+ if (uView !== void 0) gl.uniformMatrix3fv(uView, false, mat3.toUniform(ctx.state.transform));
1295
+ setUniforms(gl, program, cmd.program.id, cmd.uniforms, textureCache, { value: 1 });
1296
+ applyClipTest(ctx);
1297
+ drawTriangles(ctx, 6, ctx.gl.UNSIGNED_SHORT);
1298
+ if (aPosLoc !== void 0) gl.disableVertexAttribArray(aPosLoc);
1299
+ if (aUvLoc !== void 0) gl.disableVertexAttribArray(aUvLoc);
1300
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
1301
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
1302
+ }
1303
+ var FRAME_UPLOADS = /* @__PURE__ */ new WeakMap();
1304
+ var FRAME_PROJ = /* @__PURE__ */ new WeakMap();
1305
+ function uploadedFor(ctx, prog) {
1306
+ let byProgram = FRAME_UPLOADS.get(ctx);
1307
+ if (!byProgram) {
1308
+ byProgram = /* @__PURE__ */ new WeakMap();
1309
+ FRAME_UPLOADS.set(ctx, byProgram);
1310
+ }
1311
+ let uploaded = byProgram.get(prog);
1312
+ if (!uploaded) {
1313
+ uploaded = {};
1314
+ byProgram.set(prog, uploaded);
1315
+ }
1316
+ return uploaded;
1317
+ }
1318
+ function sameValues(prev, next) {
1319
+ if (prev === void 0 || prev.length !== next.length) return false;
1320
+ for (let i = 0; i < prev.length; i++) if (prev[i] !== next[i]) return false;
1321
+ return true;
1322
+ }
1323
+ function setColorUniform(ctx, prog, r, g, b, a) {
1324
+ const loc = prog.uniform("u_color");
1325
+ if (loc === void 0) return;
1326
+ const uploaded = uploadedFor(ctx, prog);
1327
+ const prev = uploaded.color;
1328
+ if (prev !== void 0 && prev[0] === r && prev[1] === g && prev[2] === b && prev[3] === a) {
1329
+ return;
1330
+ }
1331
+ ctx.gl.uniform4f(loc, r, g, b, a);
1332
+ uploaded.color = [r, g, b, a];
1333
+ }
1334
+ function setAlphaUniform(ctx, prog, alpha) {
1335
+ const loc = prog.uniform("u_alpha");
1336
+ if (loc === void 0) return;
1337
+ const uploaded = uploadedFor(ctx, prog);
1338
+ if (uploaded.alpha === alpha) return;
1339
+ ctx.gl.uniform1f(loc, alpha);
1340
+ uploaded.alpha = alpha;
1341
+ }
1342
+ function projFor(ctx) {
1343
+ let proj = FRAME_PROJ.get(ctx);
1344
+ if (!proj) {
1345
+ proj = mat3.toUniform(mat3.screenToClip(ctx.widthCss, ctx.heightCss));
1346
+ FRAME_PROJ.set(ctx, proj);
1347
+ }
1348
+ return proj;
1349
+ }
1350
+ var COLOR_MATRIX_SCRATCH = new Float32Array(16);
1351
+ function setColorMatrixUniforms(ctx, prog, cm = ctx.state.colorMatrix) {
1352
+ const gl = ctx.gl;
1353
+ const uploaded = uploadedFor(ctx, prog);
1354
+ const mLoc = prog.uniform("u_colorMatrix");
1355
+ if (mLoc !== void 0) {
1356
+ for (let row = 0; row < 4; row++) {
1357
+ for (let col = 0; col < 4; col++) {
1358
+ COLOR_MATRIX_SCRATCH[col * 4 + row] = cm[row * 5 + col];
1359
+ }
1360
+ }
1361
+ if (!sameValues(uploaded.colorMatrix, COLOR_MATRIX_SCRATCH)) {
1362
+ gl.uniformMatrix4fv(mLoc, false, COLOR_MATRIX_SCRATCH);
1363
+ uploaded.colorMatrix = Float32Array.from(COLOR_MATRIX_SCRATCH);
1364
+ }
1365
+ }
1366
+ const bLoc = prog.uniform("u_colorBias");
1367
+ if (bLoc !== void 0) {
1368
+ const bias = [cm[4], cm[9], cm[14], cm[19]];
1369
+ if (!sameValues(uploaded.colorBias, bias)) {
1370
+ gl.uniform4f(bLoc, bias[0], bias[1], bias[2], bias[3]);
1371
+ uploaded.colorBias = Float32Array.from(bias);
1372
+ }
1373
+ }
1374
+ }
1375
+ var REBASE_TRANSLATION_PX = 65536;
1376
+ var MODEL_SCRATCH = new Float32Array(9);
1377
+ var PROJ_SCRATCH = new Float32Array(9);
1378
+ function setProjAndModel(ctx, prog, model = ctx.state.transform, proj = projFor(ctx)) {
1379
+ const gl = ctx.gl;
1380
+ const uploaded = uploadedFor(ctx, prog);
1381
+ PROJ_SCRATCH.set(proj);
1382
+ if (!sameValues(uploaded.proj, PROJ_SCRATCH)) {
1383
+ gl.uniformMatrix3fv(prog.uniform("u_proj"), false, PROJ_SCRATCH);
1384
+ uploaded.proj = Float32Array.from(PROJ_SCRATCH);
1385
+ }
1386
+ MODEL_SCRATCH.set(model);
1387
+ const originLoc = prog.uniform("u_origin");
1388
+ if (originLoc !== void 0) {
1389
+ let ox = 0;
1390
+ let oy = 0;
1391
+ const tx = model[6], ty = model[7];
1392
+ if (Math.abs(tx) >= REBASE_TRANSLATION_PX || Math.abs(ty) >= REBASE_TRANSLATION_PX) {
1393
+ const a = model[0], b = model[1], c = model[3], d = model[4];
1394
+ const det = a * d - b * c;
1395
+ const lx = Math.fround((c * ty - d * tx) / det);
1396
+ const ly = Math.fround((b * tx - a * ty) / det);
1397
+ if (Number.isFinite(lx) && Number.isFinite(ly)) {
1398
+ ox = lx;
1399
+ oy = ly;
1400
+ MODEL_SCRATCH[6] = a * ox + c * oy + tx;
1401
+ MODEL_SCRATCH[7] = b * ox + d * oy + ty;
1402
+ }
1403
+ }
1404
+ if (uploaded.originX !== ox || uploaded.originY !== oy) {
1405
+ gl.uniform2f(originLoc, ox, oy);
1406
+ uploaded.originX = ox;
1407
+ uploaded.originY = oy;
1408
+ }
1409
+ }
1410
+ if (!sameValues(uploaded.model, MODEL_SCRATCH)) {
1411
+ gl.uniformMatrix3fv(prog.uniform("u_model"), false, MODEL_SCRATCH);
1412
+ uploaded.model = Float32Array.from(MODEL_SCRATCH);
1413
+ }
1414
+ }
1415
+ function setSolidPaintUniforms(ctx, prog, color, opacity) {
1416
+ const [r, g, b, a] = resolveColor(color);
1417
+ setColorUniform(ctx, prog, r, g, b, a * (opacity ?? 1));
1418
+ setAlphaUniform(ctx, prog, ctx.state.alpha);
1419
+ }
1420
+
1421
+ // src/draw/effects.ts
1422
+ function returnToRenderTarget(ctx, width, height, scissorWasOn) {
1423
+ const gl = ctx.gl;
1424
+ const target = ctx.renderTarget ?? null;
1425
+ gl.bindFramebuffer(gl.FRAMEBUFFER, target ? target.fbo : null);
1426
+ if (target) {
1427
+ gl.viewport(0, 0, width, height);
1428
+ } else {
1429
+ ctx.restoreTargetRect?.();
1430
+ if (scissorWasOn) gl.enable(gl.SCISSOR_TEST);
1431
+ }
1432
+ }
1433
+ function drawFullscreenQuad(ctx, program, texture, width, height, uniforms, composite, programId) {
1434
+ const gl = ctx.gl;
1435
+ gl.useProgram(program.handle);
1436
+ const aPos = program.attribute("a_position");
1437
+ const aUv = program.attribute("a_uv");
1438
+ gl.bindBuffer(gl.ARRAY_BUFFER, ctx.quadVbo);
1439
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ctx.quadIbo);
1440
+ if (aPos !== void 0 && aPos >= 0) {
1441
+ gl.enableVertexAttribArray(aPos);
1442
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 16, 0);
1443
+ }
1444
+ if (aUv !== void 0 && aUv >= 0) {
1445
+ gl.enableVertexAttribArray(aUv);
1446
+ gl.vertexAttribPointer(aUv, 2, gl.FLOAT, false, 16, 8);
1447
+ }
1448
+ gl.activeTexture(gl.TEXTURE0);
1449
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1450
+ const source = program.uniform("u_source");
1451
+ if (source !== void 0) gl.uniform1i(source, 0);
1452
+ const resolution = program.uniform("u_resolution");
1453
+ if (resolution !== void 0) gl.uniform2f(resolution, width, height);
1454
+ const texel = program.uniform("u_texel");
1455
+ if (texel !== void 0) gl.uniform2f(texel, 1 / width, 1 / height);
1456
+ if (composite) {
1457
+ const alpha = program.uniform("u_alpha");
1458
+ if (alpha !== void 0) gl.uniform1f(alpha, composite.alpha);
1459
+ setColorMatrixUniforms(ctx, program, composite.colorMatrix);
1460
+ }
1461
+ if (uniforms) setUniforms(gl, program, programId ?? "effect", uniforms, ctx.textureCache, { value: 1 });
1462
+ drawTriangles(ctx, 6, ctx.gl.UNSIGNED_SHORT);
1463
+ if (aPos !== void 0 && aPos >= 0) gl.disableVertexAttribArray(aPos);
1464
+ if (aUv !== void 0 && aUv >= 0) gl.disableVertexAttribArray(aUv);
1465
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
1466
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
1467
+ }
1468
+ function fillMesh(ctx, path) {
1469
+ if (ctx.flattenTolerance !== void 0) {
1470
+ return tessellate(path, { flattenTolerance: ctx.flattenTolerance });
1471
+ }
1472
+ return getMesh(path);
1473
+ }
1474
+ function meshHandle(ctx, mesh) {
1475
+ if (ctx.flattenTolerance !== void 0) return ctx.meshCache.uploadTransient(mesh);
1476
+ return ctx.meshCache.handleFor(mesh);
1477
+ }
1478
+ function fillMeshHandle(ctx, path) {
1479
+ return meshHandle(ctx, fillMesh(ctx, path));
1480
+ }
1481
+ var BATCH_MODEL = mat3.identity();
1482
+ var SAMPLER_UNITS = new Int32Array(
1483
+ Array.from({ length: BATCH_TEXTURE_SLOTS }, (_, i) => i)
1484
+ );
1485
+ var FIELD_SCALES = new Float32Array(2 * BATCH_TEXTURE_SLOTS);
1486
+ function flushBatch(ctx) {
1487
+ const batch = ctx.drawBatch;
1488
+ const staged = ctx.batchState;
1489
+ if (batch.length === 0 || staged === void 0) {
1490
+ ctx.batchState = void 0;
1491
+ return;
1492
+ }
1493
+ const gl = ctx.gl;
1494
+ const prog = ctx.batchFill;
1495
+ gl.useProgram(prog.handle);
1496
+ const indexCount = batch.uploadAndBind();
1497
+ setProjAndModel(ctx, prog, BATCH_MODEL);
1498
+ setColorUniform(ctx, prog, 1, 1, 1, 1);
1499
+ setAlphaUniform(ctx, prog, staged.alpha);
1500
+ gl.uniform1f(prog.uniform("u_synthBold"), staged.synthBold);
1501
+ setColorMatrixUniforms(ctx, prog, staged.colorMatrix);
1502
+ gl.activeTexture(gl.TEXTURE0);
1503
+ gl.bindTexture(gl.TEXTURE_2D, ctx.whiteTexture);
1504
+ for (let i = 0; i < staged.textures.length; i++) {
1505
+ const entry = staged.textures[i];
1506
+ if (entry.kind === "atlas") {
1507
+ ctx.textureCache.bind(entry.id, i + 1);
1508
+ FIELD_SCALES[2 * (i + 1)] = entry.fieldScale[0];
1509
+ FIELD_SCALES[2 * (i + 1) + 1] = entry.fieldScale[1];
1510
+ continue;
1511
+ }
1512
+ if (entry.kind === "ramps") {
1513
+ ctx.gradRamps.bind(i + 1);
1514
+ continue;
1515
+ }
1516
+ ctx.imageCache.bind(entry.image, i + 1);
1517
+ ctx.imageCache.setMagFilter(
1518
+ entry.image,
1519
+ entry.sampling === "nearest" ? gl.NEAREST : gl.LINEAR
1520
+ );
1521
+ }
1522
+ gl.uniform1iv(prog.uniform("u_samplers"), SAMPLER_UNITS);
1523
+ gl.uniform2fv(prog.uniform("u_fieldScale"), FIELD_SCALES);
1524
+ applyClipTest(ctx, staged.clipDepth);
1525
+ drawTriangles(ctx, indexCount, ctx.gl.UNSIGNED_INT);
1526
+ gl.activeTexture(gl.TEXTURE0);
1527
+ gl.bindVertexArray(null);
1528
+ batch.reset();
1529
+ ctx.batchState = void 0;
1530
+ }
1531
+
1532
+ // src/draw/clip.ts
1533
+ function enterGroupClip(ctx, clip) {
1534
+ const pending = ctx.clipStack.reduce((n, e) => n + (e.depth === 0 ? 1 : 0), 0);
1535
+ if (ctx.clipDepth + pending + 1 > 7) {
1536
+ throw new Error(
1537
+ "weasel: clip nesting depth exceeded (max 7). You can't nest more than 7 levels of clipped containers in a single draw tree. Flatten the hierarchy or compose poses outside the scene graph."
1538
+ );
1539
+ }
1540
+ enterClip(ctx, clip);
1541
+ }
1542
+ function enterClip(ctx, path) {
1543
+ const transform = ctx.state.transform;
1544
+ const rect = axisAlignedClipRect(path, transform);
1545
+ if (rect === null) {
1546
+ materializeClips(ctx);
1547
+ const depth = ctx.clipDepth + 1;
1548
+ pushClip(ctx, path, depth);
1549
+ ctx.clipDepth = depth;
1550
+ ctx.clipStack.push({ path, transform, rect: null, depth });
1551
+ } else {
1552
+ ctx.clipStack.push({ path, transform, rect, depth: 0 });
1553
+ }
1554
+ syncBatchClip(ctx);
1555
+ }
1556
+ function leaveClip(ctx) {
1557
+ const entry = ctx.clipStack.pop();
1558
+ if (entry.depth > 0) {
1559
+ popClip(ctx, entry.path, entry.depth - 1, entry.transform);
1560
+ ctx.clipDepth = entry.depth - 1;
1561
+ }
1562
+ syncBatchClip(ctx);
1563
+ }
1564
+ function materializeClips(ctx) {
1565
+ for (const entry of ctx.clipStack) {
1566
+ if (entry.depth !== 0) continue;
1567
+ const depth = ctx.clipDepth + 1;
1568
+ pushClip(ctx, entry.path, depth, entry.transform);
1569
+ entry.depth = depth;
1570
+ ctx.clipDepth = depth;
1571
+ }
1572
+ }
1573
+ function syncBatchClip(ctx) {
1574
+ let rect = null;
1575
+ for (const entry of ctx.clipStack) {
1576
+ if (entry.rect) rect = rect ? intersectClipRects(rect, entry.rect) : entry.rect;
1577
+ }
1578
+ ctx.drawBatch.clip = rect;
1579
+ }
1580
+ function runClipDepth(ctx) {
1581
+ const stack = ctx.clipStack;
1582
+ if (stack.length === 0 || ctx.uncut) return ctx.clipDepth;
1583
+ for (let i = stack.length - 1; i >= 0; i--) {
1584
+ if (stack[i].rect === null) return stack[i].depth;
1585
+ }
1586
+ for (const entry of stack) if (entry.depth > 0) return entry.depth - 1;
1587
+ return ctx.clipDepth;
1588
+ }
1589
+ function enterUncut(ctx) {
1590
+ materializeClips(ctx);
1591
+ const cut = ctx.drawBatch.clip;
1592
+ ctx.drawBatch.clip = null;
1593
+ ctx.uncut = true;
1594
+ return cut;
1595
+ }
1596
+ function leaveUncut(ctx, cut) {
1597
+ ctx.drawBatch.clip = cut;
1598
+ ctx.uncut = false;
1599
+ }
1600
+ function retryUncut(ctx, a, b, fn) {
1601
+ const cut = enterUncut(ctx);
1602
+ fn(ctx, a, b);
1603
+ leaveUncut(ctx, cut);
1604
+ }
1605
+ function flushForOwnDraw(ctx) {
1606
+ flushBatch(ctx);
1607
+ if (ctx.clipStack.length > 0) materializeClips(ctx);
1608
+ }
1609
+ function rasterizePathToStencil(ctx, path, model) {
1610
+ const gl = ctx.gl;
1611
+ const handle = fillMeshHandle(ctx, path);
1612
+ gl.useProgram(ctx.pathFill.handle);
1613
+ gl.bindVertexArray(handle.vao);
1614
+ setProjAndModel(ctx, ctx.pathFill, model);
1615
+ drawTriangles(ctx, handle.indexCount, ctx.gl.UNSIGNED_INT);
1616
+ gl.bindVertexArray(null);
1617
+ }
1618
+ function pushClip(ctx, path, newDepth, model = ctx.state.transform) {
1619
+ flushBatch(ctx);
1620
+ const gl = ctx.gl;
1621
+ const ancestors = ancestorMask(newDepth - 1);
1622
+ const newBit = 1 << newDepth;
1623
+ const ref = ancestors | newBit;
1624
+ gl.enable(gl.STENCIL_TEST);
1625
+ gl.colorMask(false, false, false, false);
1626
+ if (fillMeshHandle(ctx, path).requiresStencil) {
1627
+ gl.stencilMask(1);
1628
+ gl.stencilFunc(gl.ALWAYS, 0, 1);
1629
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.INVERT);
1630
+ rasterizePathToStencil(ctx, path, model);
1631
+ gl.stencilMask(newBit);
1632
+ gl.stencilFunc(gl.EQUAL, ref | 1, ancestors | 1);
1633
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
1634
+ rasterizePathToStencil(ctx, path, model);
1635
+ gl.stencilMask(1);
1636
+ gl.stencilFunc(gl.ALWAYS, 0, 1);
1637
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.INVERT);
1638
+ rasterizePathToStencil(ctx, path, model);
1639
+ } else {
1640
+ gl.stencilMask(newBit);
1641
+ gl.stencilFunc(gl.EQUAL, ref, ancestors);
1642
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
1643
+ rasterizePathToStencil(ctx, path, model);
1644
+ }
1645
+ gl.colorMask(true, true, true, true);
1646
+ gl.stencilMask(255);
1647
+ }
1648
+ function popClip(ctx, path, oldDepth, model = ctx.state.transform) {
1649
+ if (ctx.batchState !== void 0 && ctx.batchState.clipDepth > oldDepth) flushBatch(ctx);
1650
+ const gl = ctx.gl;
1651
+ gl.enable(gl.STENCIL_TEST);
1652
+ const oldBit = 1 << oldDepth + 1;
1653
+ const ref = ancestorMask(oldDepth) | oldBit;
1654
+ gl.colorMask(false, false, false, false);
1655
+ gl.stencilMask(oldBit);
1656
+ gl.stencilFunc(gl.EQUAL, ref, ref);
1657
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.ZERO);
1658
+ rasterizePathToStencil(ctx, path, model);
1659
+ gl.colorMask(true, true, true, true);
1660
+ gl.stencilMask(255);
1661
+ }
1662
+ var WORLD = {};
1663
+ function resolveStrokeLengths(transform, cmd) {
1664
+ const stroke = cmd.stroke;
1665
+ const { markerStart, markerMid, markerEnd } = stroke;
1666
+ if (typeof stroke.width !== "object" && !isPxMarker(markerStart) && !isPxMarker(markerMid) && !isPxMarker(markerEnd)) {
1667
+ return { cmd, spaces: WORLD };
1668
+ }
1669
+ const resolved = { ...stroke };
1670
+ const { scale, metric } = strokeSpaceOf(transform[0], transform[1], transform[3], transform[4]);
1671
+ const screen = (pxPerUnit) => metric === null ? void 0 : { metric, pxPerUnit };
1672
+ let ribbon;
1673
+ if (typeof stroke.width === "object") {
1674
+ const pxPerUnit = quantizeStrokeScale(stroke.width.px, scale);
1675
+ resolved.width = resolveStrokeWidth(stroke.width, pxPerUnit);
1676
+ if (stroke.dash && pxPerUnit > 0) resolved.dash = stroke.dash.map((d) => d / pxPerUnit);
1677
+ ribbon = screen(pxPerUnit);
1678
+ }
1679
+ const head = (ref) => {
1680
+ if (ref === void 0 || typeof ref === "string" || ref.size === void 0) return ribbon;
1681
+ if (typeof ref.size === "number") return void 0;
1682
+ return screen(quantizeStrokeScale(ref.size.px, scale));
1683
+ };
1684
+ const spaces = { ribbon, start: head(markerStart), mid: head(markerMid), end: head(markerEnd) };
1685
+ if (isPxMarker(markerStart)) resolved.markerStart = resolvedMarker(markerStart, scale);
1686
+ if (isPxMarker(markerMid)) resolved.markerMid = resolvedMarker(markerMid, scale);
1687
+ if (isPxMarker(markerEnd)) resolved.markerEnd = resolvedMarker(markerEnd, scale);
1688
+ return { cmd: { ...cmd, stroke: resolved }, spaces };
1689
+ }
1690
+ function isPxMarker(ref) {
1691
+ return typeof ref === "object" && typeof ref.size === "object";
1692
+ }
1693
+ function resolvedMarker(ref, scale) {
1694
+ return { ...ref, size: resolveMarkerSize(ref, 0, quantizeStrokeScale(ref.size.px, scale)) };
1695
+ }
1696
+ function batchPaint(ctx, fill, hasVColors) {
1697
+ if (hasVColors) return void 0;
1698
+ const kind = fill.fill ?? "solid";
1699
+ if (kind === "solid") {
1700
+ const solid = fill;
1701
+ return { kind: "solid", color: solid.color, opacity: solid.opacity };
1702
+ }
1703
+ if (kind !== "linear-gradient" && kind !== "radial-gradient" && kind !== "conic-gradient") {
1704
+ return void 0;
1705
+ }
1706
+ const grad = fill;
1707
+ const g = gradientMap(ctx, grad.units);
1708
+ if (!g) return null;
1709
+ if (grad.fill === "linear-gradient") return ramp(grad, PAINT_MODE_PLAIN, linearRampUV(g, grad));
1710
+ if (grad.fill === "radial-gradient") return ramp(grad, PAINT_MODE_RADIAL, radialRampUV(g, grad));
1711
+ return ramp(grad, PAINT_MODE_CONIC, conicRampUV(g, grad));
1712
+ }
1713
+ function ramp(fill, mode, uv) {
1714
+ return {
1715
+ kind: "ramp",
1716
+ stops: fill.stops,
1717
+ space: fill.interpolate ?? "rgb",
1718
+ opacity: fill.opacity,
1719
+ mode,
1720
+ uv
1721
+ };
1722
+ }
1723
+ function gradientMap(ctx, units) {
1724
+ const inverse = gradientSpaceInverse(ctx, units);
1725
+ return inverse && mat3.multiply(inverse, ctx.state.transform);
1726
+ }
1727
+ function linearRampUV(g, fill) {
1728
+ const dx = fill.to.x - fill.from.x;
1729
+ const dy = fill.to.y - fill.from.y;
1730
+ const len = Math.hypot(dx, dy) || 1;
1731
+ const k = 1 / (len * len);
1732
+ return {
1733
+ ux: (g[0] * dx + g[1] * dy) * k,
1734
+ uy: (g[3] * dx + g[4] * dy) * k,
1735
+ u0: ((g[6] - fill.from.x) * dx + (g[7] - fill.from.y) * dy) * k,
1736
+ vx: 0,
1737
+ vy: 0,
1738
+ v0: 0
1739
+ };
1740
+ }
1741
+ function radialRampUV(g, fill) {
1742
+ const k = 1 / Math.max(fill.radius, 1e-4);
1743
+ return {
1744
+ ux: g[0] * k,
1745
+ uy: g[3] * k,
1746
+ u0: (g[6] - fill.center.x) * k,
1747
+ vx: g[1] * k,
1748
+ vy: g[4] * k,
1749
+ v0: (g[7] - fill.center.y) * k
1750
+ };
1751
+ }
1752
+ function conicRampUV(g, fill) {
1753
+ const cos = Math.cos(fill.angle);
1754
+ const sin = Math.sin(fill.angle);
1755
+ const dx0 = g[6] - fill.center.x;
1756
+ const dy0 = g[7] - fill.center.y;
1757
+ return {
1758
+ ux: cos * g[0] + sin * g[1],
1759
+ uy: cos * g[3] + sin * g[4],
1760
+ u0: cos * dx0 + sin * dy0,
1761
+ vx: cos * g[1] - sin * g[0],
1762
+ vy: cos * g[4] - sin * g[3],
1763
+ v0: cos * dy0 - sin * dx0
1764
+ };
1765
+ }
1766
+ function rampVertices(paint, rowV) {
1767
+ return paint.mode === PAINT_MODE_PLAIN ? { uv: { ...paint.uv, v0: rowV }, post: 1 } : { uv: paint.uv, post: rowV };
1768
+ }
1769
+ function gradientSpaceInverse(ctx, units) {
1770
+ if (units === "local") return mat3.invert(ctx.state.transform);
1771
+ if (units === "world" && ctx.viewMatrix) return mat3.invert(ctx.viewMatrix);
1772
+ return mat3.identity();
1773
+ }
1774
+ function spaceToDevice(ctx, units) {
1775
+ const inverse = gradientSpaceInverse(ctx, units);
1776
+ const toScreen = inverse && mat3.invert(inverse);
1777
+ if (!toScreen) return null;
1778
+ const ratio = ctx.deviceWidth && ctx.widthCss > 0 ? ctx.deviceWidth / ctx.widthCss : 1;
1779
+ return mat3.multiply(mat3.scaled(mat3.identity(), ratio, ratio), toScreen);
1780
+ }
1781
+
1782
+ // src/draw/batch.ts
1783
+ function isIdentityColorMatrix(cm) {
1784
+ return cm === IDENTITY_COLOR_MATRIX || sameValues(IDENTITY_COLOR_MATRIX, cm);
1785
+ }
1786
+ function stagedStateIsLive(ctx, staged, image, sampling, synthBold = 0) {
1787
+ if (staged.clipDepth !== (ctx.clipStack.length === 0 ? ctx.clipDepth : runClipDepth(ctx))) return false;
1788
+ if (staged.synthBold !== synthBold) return false;
1789
+ if (!staged.foldsAlpha && staged.alpha !== ctx.state.alpha) return false;
1790
+ if (image !== void 0 && slotForBitmap(staged, image, sampling) < 0) return false;
1791
+ const colorMatrix = ctx.state.colorMatrix;
1792
+ return staged.colorMatrix === colorMatrix || sameValues(staged.colorMatrix, colorMatrix);
1793
+ }
1794
+ function slotForBitmap(staged, image, sampling) {
1795
+ for (let i = 0; i < staged.textures.length; i++) {
1796
+ const entry = staged.textures[i];
1797
+ if (entry.kind !== "bitmap" || entry.image !== image) continue;
1798
+ return entry.sampling === sampling ? i + 1 : -1;
1799
+ }
1800
+ return nextFreeSlot(staged);
1801
+ }
1802
+ function slotForRamps(staged) {
1803
+ for (let i = 0; i < staged.textures.length; i++) {
1804
+ if (staged.textures[i].kind === "ramps") return i + 1;
1805
+ }
1806
+ return nextFreeSlot(staged);
1807
+ }
1808
+ function slotForAtlas(staged, id) {
1809
+ for (let i = 0; i < staged.textures.length; i++) {
1810
+ const entry = staged.textures[i];
1811
+ if (entry.kind === "atlas" && entry.id === id) return i + 1;
1812
+ }
1813
+ return nextFreeSlot(staged);
1814
+ }
1815
+ function nextFreeSlot(staged) {
1816
+ return staged.textures.length + 1 < BATCH_TEXTURE_SLOTS ? staged.textures.length + 1 : -1;
1817
+ }
1818
+ var MAX_BATCHED_MESH_VERTICES = 96;
1819
+ var MAX_BATCHED_MESH_VERTICES_IN_RUN = 1024;
1820
+ var meshBatching = {
1821
+ maxVertices: MAX_BATCHED_MESH_VERTICES,
1822
+ maxVerticesInRun: MAX_BATCHED_MESH_VERTICES_IN_RUN
1823
+ };
1824
+ function canBatchMesh(ctx, mesh) {
1825
+ if (mesh.requiresStencil) return false;
1826
+ const cap = ctx.drawBatch.length > 0 ? meshBatching.maxVerticesInRun : meshBatching.maxVertices;
1827
+ return mesh.vertices.length >> 1 <= cap;
1828
+ }
1829
+ function stageSolid(ctx, vertices) {
1830
+ if (ctx.batchState !== void 0 && !stagedStateIsLive(ctx, ctx.batchState)) flushBatch(ctx);
1831
+ if (ctx.drawBatch.wouldOverflow(vertices)) flushBatch(ctx);
1832
+ return openRun(ctx);
1833
+ }
1834
+ function openRun(ctx) {
1835
+ if (ctx.batchState === void 0) {
1836
+ const colorMatrix = ctx.state.colorMatrix;
1837
+ const foldsAlpha = isIdentityColorMatrix(colorMatrix);
1838
+ ctx.batchState = {
1839
+ alpha: foldsAlpha ? 1 : ctx.state.alpha,
1840
+ colorMatrix,
1841
+ clipDepth: runClipDepth(ctx),
1842
+ synthBold: 0,
1843
+ foldsAlpha,
1844
+ textures: []
1845
+ };
1846
+ }
1847
+ return ctx.batchState;
1848
+ }
1849
+ function stageImage(ctx, image, sampling) {
1850
+ if (ctx.batchState !== void 0 && !stagedStateIsLive(ctx, ctx.batchState, image, sampling)) {
1851
+ flushBatch(ctx);
1852
+ }
1853
+ if (ctx.drawBatch.wouldOverflow(4)) flushBatch(ctx);
1854
+ const staged = openRun(ctx);
1855
+ let slot = slotForBitmap(staged, image, sampling);
1856
+ if (slot > staged.textures.length) {
1857
+ staged.textures.push({ kind: "bitmap", image, sampling });
1858
+ slot = staged.textures.length;
1859
+ }
1860
+ return { staged, slot };
1861
+ }
1862
+ function stageGlyphs(ctx, atlasId, fieldScale, synthBold) {
1863
+ if (ctx.batchState !== void 0 && (!stagedStateIsLive(ctx, ctx.batchState, void 0, void 0, synthBold) || slotForAtlas(ctx.batchState, atlasId) < 0)) {
1864
+ flushBatch(ctx);
1865
+ }
1866
+ if (ctx.drawBatch.wouldOverflow(4)) flushBatch(ctx);
1867
+ const staged = openRun(ctx);
1868
+ staged.synthBold = synthBold;
1869
+ let slot = slotForAtlas(staged, atlasId);
1870
+ if (slot > staged.textures.length) {
1871
+ staged.textures.push({ kind: "atlas", id: atlasId, fieldScale });
1872
+ slot = staged.textures.length;
1873
+ }
1874
+ return { staged, slot };
1875
+ }
1876
+ function stageRamps(ctx, stops, space, vertices) {
1877
+ if (ctx.gradRamps.wouldReshape(stops, space)) flushBatch(ctx);
1878
+ if (ctx.batchState !== void 0 && (!stagedStateIsLive(ctx, ctx.batchState) || slotForRamps(ctx.batchState) < 0)) {
1879
+ flushBatch(ctx);
1880
+ }
1881
+ if (ctx.drawBatch.wouldOverflow(vertices)) flushBatch(ctx);
1882
+ const staged = openRun(ctx);
1883
+ let slot = slotForRamps(staged);
1884
+ if (slot > staged.textures.length) {
1885
+ staged.textures.push({ kind: "ramps" });
1886
+ slot = staged.textures.length;
1887
+ }
1888
+ return { staged, slot, rowV: ctx.gradRamps.rowV(ctx.gradRamps.upload(stops, space)) };
1889
+ }
1890
+ function stagedAlpha(ctx, staged, opacity) {
1891
+ return (opacity ?? 1) * (staged.foldsAlpha ? ctx.state.alpha : 1);
1892
+ }
1893
+ function stagedColor(ctx, staged, paint) {
1894
+ const [r, g, b, a] = resolveColor(paint.color);
1895
+ return [r, g, b, a * stagedAlpha(ctx, staged, paint.opacity)];
1896
+ }
1897
+ function pushRect(ctx, rect, fill) {
1898
+ const staged = stageSolid(ctx, 4);
1899
+ const [r, g, b, a] = stagedColor(ctx, staged, fill);
1900
+ if (!ctx.drawBatch.pushRect(
1901
+ rect.x,
1902
+ rect.y,
1903
+ rect.width,
1904
+ rect.height,
1905
+ ctx.state.transform,
1906
+ r,
1907
+ g,
1908
+ b,
1909
+ a
1910
+ )) retryUncut(ctx, rect, fill, pushRect);
1911
+ }
1912
+ function pushMesh(ctx, mesh, paint) {
1913
+ const staged = stageSolid(ctx, mesh.vertices.length >> 1);
1914
+ const [r, g, b, a] = stagedColor(ctx, staged, paint);
1915
+ if (!ctx.drawBatch.pushMesh(mesh, ctx.state.transform, r, g, b, a)) {
1916
+ retryUncut(ctx, mesh, paint, pushMesh);
1917
+ }
1918
+ }
1919
+ function pushRampRect(ctx, rect, paint) {
1920
+ const { staged, slot, rowV } = stageRamps(ctx, paint.stops, paint.space, 4);
1921
+ const { uv, post } = rampVertices(paint, rowV);
1922
+ if (!ctx.drawBatch.pushGradientRect(
1923
+ rect.x,
1924
+ rect.y,
1925
+ rect.width,
1926
+ rect.height,
1927
+ ctx.state.transform,
1928
+ uv,
1929
+ post,
1930
+ slot,
1931
+ paint.mode,
1932
+ 1,
1933
+ 1,
1934
+ 1,
1935
+ stagedAlpha(ctx, staged, paint.opacity)
1936
+ )) retryUncut(ctx, rect, paint, pushRampRect);
1937
+ }
1938
+ function pushRampMesh(ctx, mesh, paint) {
1939
+ const { staged, slot, rowV } = stageRamps(ctx, paint.stops, paint.space, mesh.vertices.length >> 1);
1940
+ const { uv, post } = rampVertices(paint, rowV);
1941
+ if (!ctx.drawBatch.pushGradientMesh(
1942
+ mesh,
1943
+ ctx.state.transform,
1944
+ uv,
1945
+ post,
1946
+ slot,
1947
+ paint.mode,
1948
+ 1,
1949
+ 1,
1950
+ 1,
1951
+ stagedAlpha(ctx, staged, paint.opacity)
1952
+ )) retryUncut(ctx, mesh, paint, pushRampMesh);
1953
+ }
1954
+ function tryStageFill(ctx, mesh, paint) {
1955
+ if (paint !== void 0 && canBatchMesh(ctx, mesh)) {
1956
+ if (paint.kind === "ramp") pushRampMesh(ctx, mesh, paint);
1957
+ else pushMesh(ctx, mesh, paint);
1958
+ return true;
1959
+ }
1960
+ flushForOwnDraw(ctx);
1961
+ return false;
1962
+ }
1963
+ function patternTileSpace(spaceInverse, origin, transform) {
1964
+ const [a, b, c, d] = transform ?? [1, 0, 0, 1];
1965
+ const placement = mat3.fromAffine([a, b, c, d, origin.x, origin.y]);
1966
+ const unplace = mat3.invert(placement);
1967
+ return unplace && mat3.multiply(unplace, spaceInverse);
1968
+ }
1969
+
1970
+ // src/draw/paintBind.ts
1971
+ function paintBindContext(ctx) {
1972
+ const cached = BIND_CONTEXTS.get(ctx);
1973
+ if (cached) return cached;
1974
+ const made = {
1975
+ gl: ctx.gl,
1976
+ get alpha() {
1977
+ return ctx.state.alpha;
1978
+ },
1979
+ program: (id) => ctx.ensureProgram?.(id) ?? ctx.programRegistry.get(id) ?? null,
1980
+ setProjAndModel: (prog) => setProjAndModel(ctx, prog),
1981
+ spaceInverse: (units) => gradientSpaceInverse(ctx, units),
1982
+ spaceToDevice: (units) => spaceToDevice(ctx, units),
1983
+ get maxTextureSize() {
1984
+ return ctx.imageCache.maxTextureSide;
1985
+ },
1986
+ resources: ctx.paintResources ?? unownedPaintResources(ctx.gl),
1987
+ bindRamp: (stops, unit, space) => {
1988
+ const row = ctx.gradRamps.upload(stops, space);
1989
+ ctx.gradRamps.bind(unit);
1990
+ return ctx.gradRamps.rowV(row);
1991
+ }
1992
+ };
1993
+ BIND_CONTEXTS.set(ctx, made);
1994
+ return made;
1995
+ }
1996
+ var BIND_CONTEXTS = /* @__PURE__ */ new WeakMap();
1997
+ var UNOWNED_RESOURCES = /* @__PURE__ */ new WeakMap();
1998
+ function unownedPaintResources(gl) {
1999
+ let made = UNOWNED_RESOURCES.get(gl);
2000
+ if (!made) {
2001
+ made = createPaintResources();
2002
+ UNOWNED_RESOURCES.set(gl, made);
2003
+ }
2004
+ return made;
2005
+ }
2006
+ function bindPattern(ctx, prog, fill, model) {
2007
+ const tex = fill.pattern;
2008
+ const entry = getTexture(tex.id);
2009
+ if (!entry) {
2010
+ const isDev = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
2011
+ if (isDev) console.warn(`weasel: pattern TextureHandle "${tex.id}" not registered`);
2012
+ return null;
2013
+ }
2014
+ const inverse = gradientSpaceInverse(ctx, fill.units);
2015
+ if (!inverse) return null;
2016
+ const tileSpace = patternTileSpace(inverse, fill.origin ?? { x: 0, y: 0 }, fill.transform);
2017
+ if (!tileSpace) return null;
2018
+ ctx.textureCache.uploadRegistered(tex.id, entry.source, "repeat");
2019
+ const gl = ctx.gl;
2020
+ gl.useProgram(prog.handle);
2021
+ setProjAndModel(ctx, prog, model);
2022
+ setColorMatrixUniforms(ctx, prog);
2023
+ gl.uniformMatrix3fv(prog.uniform("u_worldInv"), false, mat3.toUniform(tileSpace));
2024
+ const [tw, th] = textureSize(entry.source);
2025
+ gl.uniform2f(prog.uniform("u_tileSize"), tw, th);
2026
+ ctx.textureCache.bind(tex.id, 0);
2027
+ gl.uniform1i(prog.uniform("u_sampler"), 0);
2028
+ gl.uniform1f(prog.uniform("u_opacity"), fill.opacity ?? 1);
2029
+ setAlphaUniform(ctx, prog, ctx.state.alpha);
2030
+ return prog;
2031
+ }
2032
+ function textureSize(source) {
2033
+ const w = "naturalWidth" in source ? source.naturalWidth : source.width;
2034
+ const h = "naturalHeight" in source ? source.naturalHeight : source.height;
2035
+ return [w || 1, h || 1];
2036
+ }
2037
+ function bindGradient(ctx, prog, fill, model) {
2038
+ const inverse = gradientSpaceInverse(ctx, fill.units);
2039
+ if (!inverse) return null;
2040
+ const gl = ctx.gl;
2041
+ const row = ctx.gradRamps.upload(fill.stops, fill.interpolate ?? "rgb");
2042
+ gl.useProgram(prog.handle);
2043
+ setProjAndModel(ctx, prog, model);
2044
+ gl.uniformMatrix3fv(prog.uniform("u_worldInv"), false, mat3.toUniform(inverse));
2045
+ setColorMatrixUniforms(ctx, prog);
2046
+ ctx.gradRamps.bind(0);
2047
+ gl.uniform1i(prog.uniform("u_ramp"), 0);
2048
+ gl.uniform1f(prog.uniform("u_rampV"), ctx.gradRamps.rowV(row));
2049
+ setAlphaUniform(ctx, prog, ctx.state.alpha);
2050
+ gl.uniform1f(prog.uniform("u_opacity"), fill.opacity ?? 1);
2051
+ if (fill.fill === "linear-gradient") {
2052
+ gl.uniform1i(prog.uniform("u_gradKind"), 0);
2053
+ const dx = fill.to.x - fill.from.x;
2054
+ const dy = fill.to.y - fill.from.y;
2055
+ const len = Math.hypot(dx, dy) || 1;
2056
+ gl.uniform2f(prog.uniform("u_gradP0"), fill.from.x, fill.from.y);
2057
+ gl.uniform2f(prog.uniform("u_gradDir"), dx / len, dy / len);
2058
+ gl.uniform1f(prog.uniform("u_gradLen"), len);
2059
+ gl.uniform1f(prog.uniform("u_gradRadius"), 0);
2060
+ gl.uniform1f(prog.uniform("u_gradAngle"), 0);
2061
+ } else if (fill.fill === "radial-gradient") {
2062
+ gl.uniform1i(prog.uniform("u_gradKind"), 1);
2063
+ gl.uniform2f(prog.uniform("u_gradP0"), fill.center.x, fill.center.y);
2064
+ gl.uniform2f(prog.uniform("u_gradDir"), 0, 0);
2065
+ gl.uniform1f(prog.uniform("u_gradLen"), 0);
2066
+ gl.uniform1f(prog.uniform("u_gradRadius"), fill.radius);
2067
+ gl.uniform1f(prog.uniform("u_gradAngle"), 0);
2068
+ } else {
2069
+ gl.uniform1i(prog.uniform("u_gradKind"), 2);
2070
+ gl.uniform2f(prog.uniform("u_gradP0"), fill.center.x, fill.center.y);
2071
+ gl.uniform2f(prog.uniform("u_gradDir"), 0, 0);
2072
+ gl.uniform1f(prog.uniform("u_gradLen"), 0);
2073
+ gl.uniform1f(prog.uniform("u_gradRadius"), 0);
2074
+ gl.uniform1f(prog.uniform("u_gradAngle"), fill.angle);
2075
+ }
2076
+ return prog;
2077
+ }
2078
+
2079
+ // src/draw/pathPaint.ts
2080
+ function drawPath(ctx, cmd) {
2081
+ if (cmd.fill) drawPathFill(ctx, cmd);
2082
+ if (cmd.stroke?.paint) drawPathStroke(ctx, cmd);
2083
+ }
2084
+ function drawPathFill(ctx, cmd) {
2085
+ const fill = cmd.fill;
2086
+ const isSolid2 = fill.fill === void 0 || fill.fill === "solid";
2087
+ const hasVColors = !!(cmd.vertexColors && cmd.vertexColors.length > 0);
2088
+ const solid = fill;
2089
+ const paint = batchPaint(ctx, fill, hasVColors);
2090
+ if (paint === null) return;
2091
+ if (paint !== void 0 && cmd.path.kind === "rect") {
2092
+ if (paint.kind === "ramp") pushRampRect(ctx, cmd.path, paint);
2093
+ else pushRect(ctx, cmd.path, paint);
2094
+ return;
2095
+ }
2096
+ const mesh = fillMesh(ctx, cmd.path);
2097
+ if (tryStageFill(ctx, mesh, paint)) return;
2098
+ const handle = meshHandle(ctx, mesh);
2099
+ if (isSolid2 && hasVColors) {
2100
+ drawPathFillVColor(ctx, cmd, solid, handle);
2101
+ } else if (handle.requiresStencil) {
2102
+ drawPathFillStencil(ctx, fill, handle);
2103
+ } else {
2104
+ drawPathFillByKind(ctx, fill, handle);
2105
+ }
2106
+ }
2107
+ function attachVertexColors(gl, prog, perAnchor, handle) {
2108
+ const vbo = gl.createBuffer();
2109
+ if (!vbo) throw new Error("attachVertexColors: createBuffer returned null");
2110
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
2111
+ gl.bufferData(gl.ARRAY_BUFFER, expandAnchorColors(perAnchor, handle), gl.DYNAMIC_DRAW);
2112
+ const location = prog.attribute("a_vertexColor");
2113
+ if (location !== void 0) {
2114
+ gl.enableVertexAttribArray(location);
2115
+ gl.vertexAttribPointer(location, 4, gl.FLOAT, false, 0, 0);
2116
+ }
2117
+ return { vbo, location };
2118
+ }
2119
+ function detachVertexColors(gl, { vbo, location }) {
2120
+ if (location !== void 0) gl.disableVertexAttribArray(location);
2121
+ gl.deleteBuffer(vbo);
2122
+ }
2123
+ function expandAnchorColors(perAnchor, handle) {
2124
+ const aA = handle.anchorA;
2125
+ const aB = handle.anchorB;
2126
+ const aT = handle.anchorT;
2127
+ if (!aA || !aB || !aT) {
2128
+ return new Float32Array(perAnchor);
2129
+ }
2130
+ const n = aA.length;
2131
+ const out = new Float32Array(n * 4);
2132
+ for (let i = 0; i < n; i++) {
2133
+ const a4 = aA[i] * 4;
2134
+ const b4 = aB[i] * 4;
2135
+ const t = aT[i];
2136
+ out[i * 4 + 0] = perAnchor[a4 + 0] + (perAnchor[b4 + 0] - perAnchor[a4 + 0]) * t;
2137
+ out[i * 4 + 1] = perAnchor[a4 + 1] + (perAnchor[b4 + 1] - perAnchor[a4 + 1]) * t;
2138
+ out[i * 4 + 2] = perAnchor[a4 + 2] + (perAnchor[b4 + 2] - perAnchor[a4 + 2]) * t;
2139
+ out[i * 4 + 3] = perAnchor[a4 + 3] + (perAnchor[b4 + 3] - perAnchor[a4 + 3]) * t;
2140
+ }
2141
+ return out;
2142
+ }
2143
+ function drawPathFillVColor(ctx, cmd, fill, handle) {
2144
+ const gl = ctx.gl;
2145
+ const prog = ctx.pathFillVColor;
2146
+ gl.useProgram(prog.handle);
2147
+ gl.bindVertexArray(handle.vao);
2148
+ setProjAndModel(ctx, prog);
2149
+ setSolidPaintUniforms(ctx, prog, fill.color, fill.opacity);
2150
+ setColorMatrixUniforms(ctx, prog);
2151
+ const colors = attachVertexColors(gl, prog, cmd.vertexColors, handle);
2152
+ if (handle.requiresStencil) {
2153
+ beginParityMask(ctx, handle.indexCount);
2154
+ drawTriangles(ctx, handle.indexCount, ctx.gl.UNSIGNED_INT);
2155
+ endParityMask(ctx);
2156
+ } else {
2157
+ applyClipTest(ctx);
2158
+ drawTriangles(ctx, handle.indexCount, ctx.gl.UNSIGNED_INT);
2159
+ }
2160
+ detachVertexColors(gl, colors);
2161
+ gl.bindVertexArray(null);
2162
+ }
2163
+ function bindPathFillByKind(ctx, fill) {
2164
+ const kind = fill.fill ?? "solid";
2165
+ if (kind === "solid") return bindPathFillSolid(ctx, fill);
2166
+ if (kind === "pattern") return bindPathFillPattern(ctx, fill);
2167
+ if (kind === "linear-gradient" || kind === "radial-gradient" || kind === "conic-gradient") {
2168
+ return bindPathFillGradient(ctx, fill);
2169
+ }
2170
+ const bind = getPaintDrawKind(kind)?.bind;
2171
+ return bind ? bind(paintBindContext(ctx), fill) : null;
2172
+ }
2173
+ function drawPathFillByKind(ctx, fill, handle) {
2174
+ const prog = bindPathFillByKind(ctx, fill);
2175
+ if (!prog) return;
2176
+ const gl = ctx.gl;
2177
+ gl.bindVertexArray(handle.vao);
2178
+ applyClipTest(ctx);
2179
+ drawTriangles(ctx, handle.indexCount, ctx.gl.UNSIGNED_INT);
2180
+ gl.bindVertexArray(null);
2181
+ }
2182
+ function bindPathFillSolid(ctx, fill) {
2183
+ const prog = ctx.pathFill;
2184
+ ctx.gl.useProgram(prog.handle);
2185
+ setProjAndModel(ctx, prog);
2186
+ setSolidPaintUniforms(ctx, prog, fill.color, fill.opacity);
2187
+ setColorMatrixUniforms(ctx, prog);
2188
+ return prog;
2189
+ }
2190
+ function bindPathFillPattern(ctx, fill) {
2191
+ return bindPattern(ctx, ctx.patternFill, fill, ctx.state.transform);
2192
+ }
2193
+ function bindPathFillGradient(ctx, fill) {
2194
+ return bindGradient(ctx, ctx.gradFill, fill, ctx.state.transform);
2195
+ }
2196
+ function drawPathFillStencil(ctx, fill, handle) {
2197
+ const gl = ctx.gl;
2198
+ gl.useProgram(ctx.pathFill.handle);
2199
+ gl.bindVertexArray(handle.vao);
2200
+ setProjAndModel(ctx, ctx.pathFill);
2201
+ beginParityMask(ctx, handle.indexCount);
2202
+ if (bindPathFillByKind(ctx, fill)) {
2203
+ drawTriangles(ctx, handle.indexCount, ctx.gl.UNSIGNED_INT);
2204
+ }
2205
+ endParityMask(ctx);
2206
+ gl.bindVertexArray(null);
2207
+ }
2208
+ function drawPathStroke(ctx, rawCmd) {
2209
+ const { cmd, spaces } = resolveStrokeLengths(ctx.state.transform, rawCmd);
2210
+ const stroke = cmd.stroke;
2211
+ const align = stroke.align ?? "center";
2212
+ const metric = spaces.ribbon?.metric;
2213
+ const stenciled = cmd.path.kind === "polygon" || metric !== void 0 && !metricKeepsRects(metric);
2214
+ if (stenciled && align !== "center") {
2215
+ flushForOwnDraw(ctx);
2216
+ drawPathStrokeStenciled(ctx, cmd, align, spaces);
2217
+ } else {
2218
+ drawPathStrokeUnclipped(ctx, cmd, spaces);
2219
+ }
2220
+ const width = resolveStrokeWidth(stroke.width ?? 1, 1);
2221
+ const heads = cachedMarkerCommands(cmd.path, rawCmd.stroke, stroke, width, ctx.flattenTolerance, spaces);
2222
+ for (const marker of heads) drawPath(ctx, marker);
2223
+ }
2224
+ function drawPathStrokeUnclipped(ctx, cmd, spaces) {
2225
+ const stroke = cmd.stroke;
2226
+ const paint = stroke.paint;
2227
+ const isSolid2 = paint.fill === void 0 || paint.fill === "solid";
2228
+ const solid = paint;
2229
+ const mesh = strokeMesh(cmd.path, stroke, ctx.flattenTolerance, spaces);
2230
+ if (mesh.indices.length === 0) return;
2231
+ const hasVColors = !!(stroke.vertexColors && stroke.vertexColors.length > 0);
2232
+ const batched = batchPaint(ctx, paint, hasVColors);
2233
+ if (batched === null) return;
2234
+ if (tryStageFill(ctx, mesh, batched)) return;
2235
+ const handle = hasVColors ? ctx.meshCache.uploadTransient(mesh) : ctx.meshCache.uploadRecurring(mesh);
2236
+ if (!isSolid2 && !hasVColors) {
2237
+ drawPathFillByKind(ctx, paint, handle);
2238
+ return;
2239
+ }
2240
+ const gl = ctx.gl;
2241
+ if (hasVColors) {
2242
+ const prog = ctx.pathFillVColor;
2243
+ gl.useProgram(prog.handle);
2244
+ gl.bindVertexArray(handle.vao);
2245
+ setProjAndModel(ctx, prog);
2246
+ setSolidPaintUniforms(ctx, prog, isSolid2 ? solid.color : "#ffffff", paint.opacity);
2247
+ setColorMatrixUniforms(ctx, prog);
2248
+ const colors = attachVertexColors(gl, prog, stroke.vertexColors, handle);
2249
+ applyClipTest(ctx);
2250
+ drawTriangles(ctx, handle.indexCount, ctx.gl.UNSIGNED_INT);
2251
+ detachVertexColors(gl, colors);
2252
+ gl.bindVertexArray(null);
2253
+ return;
2254
+ }
2255
+ gl.useProgram(ctx.pathFill.handle);
2256
+ gl.bindVertexArray(handle.vao);
2257
+ setProjAndModel(ctx, ctx.pathFill);
2258
+ setSolidPaintUniforms(ctx, ctx.pathFill, solid.color, solid.opacity);
2259
+ setColorMatrixUniforms(ctx, ctx.pathFill);
2260
+ applyClipTest(ctx);
2261
+ drawTriangles(ctx, handle.indexCount, ctx.gl.UNSIGNED_INT);
2262
+ gl.bindVertexArray(null);
2263
+ }
2264
+ var DOUBLED_VERTEX_WIDTHS = /* @__PURE__ */ new WeakMap();
2265
+ function doubledVertexWidths(widths) {
2266
+ let doubled = DOUBLED_VERTEX_WIDTHS.get(widths);
2267
+ if (doubled === void 0) {
2268
+ doubled = widths.map((w) => w * 2);
2269
+ DOUBLED_VERTEX_WIDTHS.set(widths, doubled);
2270
+ }
2271
+ return doubled;
2272
+ }
2273
+ function drawPathStrokeStenciled(ctx, cmd, align, spaces) {
2274
+ const stroke = cmd.stroke;
2275
+ const paint = stroke.paint;
2276
+ const isSolid2 = paint.fill === void 0 || paint.fill === "solid";
2277
+ const solid = paint;
2278
+ const widerStroke = {
2279
+ ...stroke,
2280
+ width: resolveStrokeWidth(stroke.width ?? 1, 1) * 2,
2281
+ ...stroke.vertexWidths ? { vertexWidths: doubledVertexWidths(stroke.vertexWidths) } : {},
2282
+ align: "center"
2283
+ };
2284
+ const useVColor = !!(stroke.vertexColors && stroke.vertexColors.length > 0);
2285
+ const fillHandle = fillMeshHandle(ctx, cmd.path);
2286
+ const ribbonMesh = strokeMesh(cmd.path, widerStroke, ctx.flattenTolerance, spaces);
2287
+ if (ribbonMesh.indices.length === 0) return;
2288
+ const ribbonHandle = useVColor ? ctx.meshCache.uploadTransient(ribbonMesh) : ctx.meshCache.uploadRecurring(ribbonMesh);
2289
+ const gl = ctx.gl;
2290
+ const prog = useVColor ? ctx.pathFillVColor : ctx.pathFill;
2291
+ gl.useProgram(prog.handle);
2292
+ setProjAndModel(ctx, prog);
2293
+ gl.enable(gl.STENCIL_TEST);
2294
+ gl.colorMask(false, false, false, false);
2295
+ gl.stencilMask(1);
2296
+ gl.stencilFunc(gl.ALWAYS, 1, 1);
2297
+ gl.stencilOp(gl.KEEP, gl.KEEP, fillHandle.requiresStencil ? gl.INVERT : gl.REPLACE);
2298
+ gl.bindVertexArray(fillHandle.vao);
2299
+ drawTriangles(ctx, fillHandle.indexCount, ctx.gl.UNSIGNED_INT);
2300
+ const clipMask = ancestorMask(ctx.clipDepth);
2301
+ gl.colorMask(true, true, true, true);
2302
+ if (align === "inner") {
2303
+ gl.stencilFunc(gl.EQUAL, clipMask | 1, clipMask | 1);
2304
+ } else {
2305
+ gl.stencilFunc(gl.EQUAL, clipMask, clipMask | 1);
2306
+ }
2307
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
2308
+ let ribbonProg = prog;
2309
+ if (useVColor) {
2310
+ setSolidPaintUniforms(ctx, prog, isSolid2 ? solid.color : "#ffffff", paint.opacity);
2311
+ setColorMatrixUniforms(ctx, prog);
2312
+ } else {
2313
+ ribbonProg = bindPathFillByKind(ctx, paint);
2314
+ }
2315
+ if (!ribbonProg) {
2316
+ gl.stencilMask(1);
2317
+ gl.clear(gl.STENCIL_BUFFER_BIT);
2318
+ gl.disable(gl.STENCIL_TEST);
2319
+ gl.bindVertexArray(null);
2320
+ return;
2321
+ }
2322
+ gl.bindVertexArray(ribbonHandle.vao);
2323
+ const colors = useVColor ? attachVertexColors(gl, ribbonProg, stroke.vertexColors, ribbonHandle) : null;
2324
+ drawTriangles(ctx, ribbonHandle.indexCount, ctx.gl.UNSIGNED_INT);
2325
+ gl.stencilMask(1);
2326
+ gl.clear(gl.STENCIL_BUFFER_BIT);
2327
+ gl.disable(gl.STENCIL_TEST);
2328
+ if (colors) detachVertexColors(gl, colors);
2329
+ gl.bindVertexArray(null);
2330
+ }
2331
+ function outlineGroupMesh(group, dx, dy) {
2332
+ return mergeGlyphMeshes(group, dx, dy, (glyph) => outlineMesh(glyph.key, glyph.d));
2333
+ }
2334
+ function outlineGroupStrokeMesh(group, dx, dy, transform) {
2335
+ const stroke = group.stroke;
2336
+ if (!stroke) return null;
2337
+ const width = stroke.width ?? 1;
2338
+ if (typeof width === "object") {
2339
+ if (!(width.px > 0)) return null;
2340
+ const shear = shearOf(group);
2341
+ const [a, b, c, d] = [transform[0], transform[1], transform[3], transform[4]];
2342
+ return mergeGlyphMeshes(group, dx, dy, (glyph) => {
2343
+ if (!(glyph.scale > 0)) return null;
2344
+ const s = glyph.scale;
2345
+ const space = strokeSpaceOf(s * a, s * b, s * (c - a * shear), s * (d - b * shear));
2346
+ const pxPerEm = quantizeStrokeScale(width.px, space.scale);
2347
+ if (!(pxPerEm > 0)) return null;
2348
+ const dash = stroke.dash?.map((v) => v / pxPerEm);
2349
+ return outlineStrokeMesh(
2350
+ glyph.key,
2351
+ glyph.d,
2352
+ width.px / pxPerEm,
2353
+ { ...stroke, dash },
2354
+ space.metric ?? void 0
2355
+ );
2356
+ });
2357
+ }
2358
+ if (!(width > 0)) return null;
2359
+ return mergeGlyphMeshes(group, dx, dy, (glyph) => glyph.scale > 0 ? outlineStrokeMesh(
2360
+ glyph.key,
2361
+ glyph.d,
2362
+ quantizeEmWidth(width / glyph.scale),
2363
+ { ...stroke, dash: stroke.dash?.map((v) => v / glyph.scale) }
2364
+ ) : null);
2365
+ }
2366
+ var shearOf = (group) => group.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
2367
+ function mergeGlyphMeshes(group, dx, dy, meshFor) {
2368
+ const parts = [];
2369
+ let vertexFloats = 0;
2370
+ let indexCount = 0;
2371
+ for (const glyph of group.glyphs) {
2372
+ const mesh = meshFor(glyph);
2373
+ if (!mesh || mesh.indices.length === 0) continue;
2374
+ parts.push({ mesh, glyph });
2375
+ vertexFloats += mesh.vertices.length;
2376
+ indexCount += mesh.indices.length;
2377
+ }
2378
+ if (parts.length === 0) return null;
2379
+ const shear = shearOf(group);
2380
+ const vertices = new Float32Array(vertexFloats);
2381
+ const indices = new Uint32Array(indexCount);
2382
+ let vi = 0;
2383
+ let ii = 0;
2384
+ let base = 0;
2385
+ for (const { mesh, glyph } of parts) {
2386
+ const { x, baselineY, scale } = glyph;
2387
+ for (let k = 0; k < mesh.vertices.length; k += 2) {
2388
+ const ex = mesh.vertices[k];
2389
+ const ey = mesh.vertices[k + 1];
2390
+ vertices[vi++] = x + dx + (ex - ey * shear) * scale;
2391
+ vertices[vi++] = baselineY + dy + ey * scale;
2392
+ }
2393
+ for (let k = 0; k < mesh.indices.length; k++) indices[ii++] = base + mesh.indices[k];
2394
+ base += mesh.vertices.length / 2;
2395
+ }
2396
+ return { vertices, indices };
2397
+ }
2398
+
2399
+ // src/draw/scratchPass.ts
2400
+ var UNIT_QUAD = {
2401
+ vertices: new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]),
2402
+ indices: new Uint32Array([0, 1, 2, 0, 2, 3])
2403
+ };
2404
+ function renderPaintToScratch(ctx, fill, rect) {
2405
+ const scratch = ctx.paintScratch;
2406
+ const bind = getPaintDrawKind(fill.fill)?.bind;
2407
+ if (!scratch || !bind) return null;
2408
+ const ratio = ctx.deviceWidth && ctx.widthCss > 0 ? ctx.deviceWidth / ctx.widthCss : 1;
2409
+ const deviceW = ctx.deviceWidth ?? Math.round(ctx.widthCss);
2410
+ const deviceH = ctx.deviceHeight ?? Math.round(ctx.heightCss);
2411
+ const px0 = Math.max(0, Math.floor(rect.x0 * ratio));
2412
+ const py0 = Math.max(0, Math.floor(rect.y0 * ratio));
2413
+ const maxSide = ctx.imageCache.maxTextureSide;
2414
+ const pw = Math.min(maxSide, Math.min(deviceW, Math.ceil(rect.x1 * ratio)) - px0);
2415
+ const ph = Math.min(maxSide, Math.min(deviceH, Math.ceil(rect.y1 * ratio)) - py0);
2416
+ if (!(pw > 0 && ph > 0)) return null;
2417
+ const x0 = px0 / ratio, y0 = py0 / ratio, w = pw / ratio, h = ph / ratio;
2418
+ const model = new Float32Array([w, 0, 0, 0, h, 0, x0, y0, 1]);
2419
+ const proj = new Float32Array([2 / w, 0, 0, 0, 2 / h, 0, -1 - 2 * x0 / w, -1 - 2 * y0 / h, 1]);
2420
+ const base = paintBindContext(ctx);
2421
+ const bindCtx = {
2422
+ ...base,
2423
+ alpha: ctx.state.alpha,
2424
+ maxTextureSize: base.maxTextureSize,
2425
+ setProjAndModel: (prog2) => setProjAndModel(ctx, prog2, model, proj)
2426
+ };
2427
+ const gl = ctx.gl;
2428
+ const target = scratch.acquire(pw, ph);
2429
+ const scissorWasOn = gl.isEnabled(gl.SCISSOR_TEST);
2430
+ gl.activeTexture(gl.TEXTURE0);
2431
+ gl.bindTexture(gl.TEXTURE_2D, null);
2432
+ gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
2433
+ gl.viewport(0, 0, pw, ph);
2434
+ gl.disable(gl.SCISSOR_TEST);
2435
+ gl.disable(gl.STENCIL_TEST);
2436
+ gl.clear(gl.COLOR_BUFFER_BIT);
2437
+ gl.disable(gl.BLEND);
2438
+ const prog = bind(bindCtx, fill);
2439
+ if (prog) {
2440
+ gl.bindVertexArray(ctx.meshCache.uploadRecurring(UNIT_QUAD).vao);
2441
+ drawTriangles(ctx, UNIT_QUAD.indices.length, gl.UNSIGNED_INT);
2442
+ gl.bindVertexArray(null);
2443
+ }
2444
+ gl.enable(gl.BLEND);
2445
+ returnToRenderTarget(ctx, deviceW, deviceH, scissorWasOn);
2446
+ if (!prog) return null;
2447
+ return {
2448
+ texture: target.texture,
2449
+ uvFromScreen: new Float32Array([
2450
+ ratio / target.width,
2451
+ 0,
2452
+ 0,
2453
+ 0,
2454
+ ratio / target.height,
2455
+ 0,
2456
+ -px0 / target.width,
2457
+ -py0 / target.height,
2458
+ 1
2459
+ ])
2460
+ };
2461
+ }
2462
+ function bindScratchPaint(ctx, prog, painted) {
2463
+ const gl = ctx.gl;
2464
+ gl.useProgram(prog.handle);
2465
+ setProjAndModel(ctx, prog, BATCH_MODEL);
2466
+ gl.uniformMatrix3fv(prog.uniform("u_worldInv"), false, painted.uvFromScreen);
2467
+ gl.activeTexture(gl.TEXTURE0);
2468
+ gl.bindTexture(gl.TEXTURE_2D, painted.texture);
2469
+ gl.uniform1i(prog.uniform("u_sampler"), 0);
2470
+ return prog;
2471
+ }
2472
+
2473
+ // src/draw/text.ts
2474
+ var OUTLINE_MIN_SCREEN_PX = 48;
2475
+ function modelScale(m) {
2476
+ const sx = Math.hypot(m[0], m[1]);
2477
+ const sy = Math.hypot(m[3], m[4]);
2478
+ return Math.sqrt(sx * sy) || 1;
2479
+ }
2480
+ function drawText(ctx, cmd) {
2481
+ const style = resolveTextStyle(cmd.style);
2482
+ const lineHeight = style.lineHeight;
2483
+ const align = cmd.align ?? style.align;
2484
+ const direction = cmd.direction ?? style.direction;
2485
+ const maxWidth = cmd.maxWidth ?? Infinity;
2486
+ const minScreen = ctx.textOutlineMinScreenSize ?? OUTLINE_MIN_SCREEN_PX;
2487
+ const outlineMinSize = Number.isFinite(minScreen) ? minScreen / modelScale(ctx.state.transform) : void 0;
2488
+ const laid = cachedLayoutRuns(cmd.runs, {
2489
+ maxWidth,
2490
+ alignWidth: cmd.width,
2491
+ lineHeight,
2492
+ align,
2493
+ direction,
2494
+ tabSize: style.tabSize,
2495
+ justify: cmd.justify,
2496
+ outlineMinSize
2497
+ });
2498
+ if (laid.groups.length === 0 && laid.decorations.length === 0) return;
2499
+ const dx = cmd.x;
2500
+ const dy = cmd.y + verticalAlignOffset(cmd.verticalAlign, cmd.height, laid.bounds.height);
2501
+ for (const group of laid.groups) {
2502
+ if (group.source === "outline") drawTextOutlineGroup(ctx, group, dx, dy);
2503
+ else drawTextGroup(ctx, group, dx, dy);
2504
+ }
2505
+ drawTextDecorations(ctx, laid.decorations, dx, dy);
2506
+ }
2507
+ function drawTextOutlineGroup(ctx, group, dx, dy) {
2508
+ const fill = group.fill;
2509
+ if (fill !== null) {
2510
+ const mesh = outlineGroupMesh(group, dx, dy);
2511
+ if (mesh) drawOutlineMesh(ctx, fill, mesh);
2512
+ }
2513
+ const strokePaint = group.stroke?.paint;
2514
+ if (!strokePaint) return;
2515
+ const ribbon = outlineGroupStrokeMesh(group, dx, dy, ctx.state.transform);
2516
+ if (ribbon) drawOutlineMesh(ctx, strokePaint, ribbon);
2517
+ }
2518
+ function drawOutlineMesh(ctx, fill, mesh) {
2519
+ const paint = batchPaint(ctx, fill, false);
2520
+ if (paint === null) return;
2521
+ if (tryStageFill(ctx, mesh, paint)) return;
2522
+ drawPathFillByKind(ctx, fill, ctx.meshCache.uploadTransient(mesh));
2523
+ }
2524
+ function drawTextDecorations(ctx, decorations, dx, dy) {
2525
+ for (const d of decorations) {
2526
+ if (glyphPaintKindOf(d.fill) !== null) {
2527
+ drawOutlineMesh(ctx, d.fill, ruleMesh(d.x0 + dx, d.y0 + dy, d.x1 + dx, d.y1 + dy));
2528
+ continue;
2529
+ }
2530
+ const [r, g, b, a] = "color" in d.fill ? resolveColor(d.fill.color) : [0, 0, 0, 1];
2531
+ let cut;
2532
+ for (; ; ) {
2533
+ const staged = stageSolid(ctx, 4);
2534
+ if (ctx.drawBatch.pushRect(
2535
+ d.x0 + dx,
2536
+ d.y0 + dy,
2537
+ d.x1 - d.x0,
2538
+ d.y1 - d.y0,
2539
+ ctx.state.transform,
2540
+ r,
2541
+ g,
2542
+ b,
2543
+ a * (staged.foldsAlpha ? ctx.state.alpha : 1)
2544
+ )) break;
2545
+ cut = enterUncut(ctx);
2546
+ }
2547
+ if (cut !== void 0) leaveUncut(ctx, cut);
2548
+ }
2549
+ }
2550
+ function ruleMesh(x0, y0, x1, y1) {
2551
+ return {
2552
+ vertices: new Float32Array([x0, y0, x1, y0, x1, y1, x0, y1]),
2553
+ indices: new Uint32Array([0, 1, 2, 0, 2, 3])
2554
+ };
2555
+ }
2556
+ var SYNTH_BOLD_AMOUNT = 0.08;
2557
+ function drawTextGroup(ctx, group, dx, dy) {
2558
+ if (group.source === "canvas") {
2559
+ if (!syncDynamicPageTexture(ctx.textureCache, group.page)) return;
2560
+ } else {
2561
+ if (!ensureFontTexture(group.family, group.weight, group.style, ctx.textureCache)) return;
2562
+ }
2563
+ if (group.quads.length === 0) return;
2564
+ const atlasId = group.source === "canvas" ? dynamicPageTextureId(group.page) : textureCacheKey(group.family, group.weight, group.style);
2565
+ const mode = group.source === "canvas" ? GLYPH_MODE_R8 : GLYPH_MODE_MSDF;
2566
+ const fieldScale = glyphFieldScale(
2567
+ group.source === "canvas" ? "canvas" : "atlas",
2568
+ group.family,
2569
+ group.weight,
2570
+ group.style
2571
+ );
2572
+ if (!fieldScale) return;
2573
+ const bold = group.synthetic.bold ? SYNTH_BOLD_AMOUNT : 0;
2574
+ const tanItalic = group.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
2575
+ const paintKind = glyphPaintKindOf(group.fill);
2576
+ if (paintKind !== null) {
2577
+ drawPaintedGlyphs(ctx, group, dx, dy, paintKind, atlasId, mode, bold, tanItalic, fieldScale);
2578
+ return;
2579
+ }
2580
+ const color = group.fill !== null && "color" in group.fill ? resolveColor(group.fill.color) : [0, 0, 0, 1];
2581
+ const batch = ctx.drawBatch;
2582
+ const m = ctx.state.transform;
2583
+ let { staged, slot } = stageGlyphs(ctx, atlasId, fieldScale, bold);
2584
+ let alpha = color[3] * (staged.foldsAlpha ? ctx.state.alpha : 1);
2585
+ for (const q of group.quads) {
2586
+ if (batch.wouldOverflow(4)) {
2587
+ flushBatch(ctx);
2588
+ ({ staged, slot } = stageGlyphs(ctx, atlasId, fieldScale, bold));
2589
+ alpha = color[3] * (staged.foldsAlpha ? ctx.state.alpha : 1);
2590
+ }
2591
+ let cut;
2592
+ while (!batch.pushGlyph(
2593
+ q.x0 + dx,
2594
+ q.y0 + dy,
2595
+ q.x1 + dx,
2596
+ q.y1 + dy,
2597
+ q.baselineY + dy,
2598
+ tanItalic,
2599
+ m,
2600
+ q.u0,
2601
+ q.v0,
2602
+ q.u1,
2603
+ q.v1,
2604
+ color[0],
2605
+ color[1],
2606
+ color[2],
2607
+ alpha,
2608
+ slot,
2609
+ mode
2610
+ )) {
2611
+ cut = enterUncut(ctx);
2612
+ ({ staged, slot } = stageGlyphs(ctx, atlasId, fieldScale, bold));
2613
+ alpha = color[3] * (staged.foldsAlpha ? ctx.state.alpha : 1);
2614
+ }
2615
+ if (cut !== void 0) {
2616
+ leaveUncut(ctx, cut);
2617
+ ({ staged, slot } = stageGlyphs(ctx, atlasId, fieldScale, bold));
2618
+ alpha = color[3] * (staged.foldsAlpha ? ctx.state.alpha : 1);
2619
+ }
2620
+ }
2621
+ }
2622
+ function glyphPaintKindOf(fill) {
2623
+ const kind = fill?.fill ?? "solid";
2624
+ if (kind === "pattern") return "pattern";
2625
+ if (kind === "linear-gradient" || kind === "radial-gradient" || kind === "conic-gradient") {
2626
+ return "gradient";
2627
+ }
2628
+ return kind === "solid" ? null : "texture";
2629
+ }
2630
+ var GLYPH_PAINT_ATLAS_UNIT = 1;
2631
+ function drawPaintedGlyphs(ctx, group, dx, dy, kind, atlasId, mode, bold, tanItalic, fieldScale) {
2632
+ const prog = ctx.glyphPaintProgram?.(kind);
2633
+ if (!prog) return;
2634
+ flushForOwnDraw(ctx);
2635
+ const fill = group.fill;
2636
+ const m = ctx.state.transform;
2637
+ let bound;
2638
+ if (kind === "pattern") {
2639
+ bound = bindPattern(ctx, prog, fill, BATCH_MODEL);
2640
+ } else if (kind === "gradient") {
2641
+ bound = bindGradient(ctx, prog, fill, BATCH_MODEL);
2642
+ } else {
2643
+ const painted = renderPaintToScratch(ctx, fill, glyphScreenBounds(group, dx, dy, tanItalic, m));
2644
+ bound = painted && bindScratchPaint(ctx, prog, painted);
2645
+ }
2646
+ if (!bound) return;
2647
+ const gl = ctx.gl;
2648
+ ctx.textureCache.bind(atlasId, GLYPH_PAINT_ATLAS_UNIT);
2649
+ gl.uniform1i(prog.uniform("u_atlas"), GLYPH_PAINT_ATLAS_UNIT);
2650
+ gl.uniform1f(prog.uniform("u_synthBold"), bold);
2651
+ gl.uniform2f(prog.uniform("u_fieldScale"), fieldScale[0], fieldScale[1]);
2652
+ applyClipTest(ctx);
2653
+ const batch = ctx.drawBatch;
2654
+ const cut = batch.clip;
2655
+ batch.clip = null;
2656
+ for (const q of group.quads) {
2657
+ if (batch.wouldOverflow(4)) drawStagedGlyphs(ctx);
2658
+ batch.pushGlyph(
2659
+ q.x0 + dx,
2660
+ q.y0 + dy,
2661
+ q.x1 + dx,
2662
+ q.y1 + dy,
2663
+ q.baselineY + dy,
2664
+ tanItalic,
2665
+ m,
2666
+ q.u0,
2667
+ q.v0,
2668
+ q.u1,
2669
+ q.v1,
2670
+ 1,
2671
+ 1,
2672
+ 1,
2673
+ 1,
2674
+ GLYPH_PAINT_ATLAS_UNIT,
2675
+ mode
2676
+ );
2677
+ }
2678
+ drawStagedGlyphs(ctx);
2679
+ batch.clip = cut;
2680
+ gl.activeTexture(gl.TEXTURE0);
2681
+ gl.bindVertexArray(null);
2682
+ }
2683
+ function drawStagedGlyphs(ctx) {
2684
+ const batch = ctx.drawBatch;
2685
+ if (batch.length === 0) return;
2686
+ drawTriangles(ctx, batch.uploadAndBind(), ctx.gl.UNSIGNED_INT);
2687
+ batch.reset();
2688
+ }
2689
+ function glyphScreenBounds(group, dx, dy, tanItalic, m) {
2690
+ const b = { x0: Infinity, y0: Infinity, x1: -Infinity, y1: -Infinity };
2691
+ const add = (x, y) => {
2692
+ const sx = m[0] * x + m[3] * y + m[6];
2693
+ const sy = m[1] * x + m[4] * y + m[7];
2694
+ if (sx < b.x0) b.x0 = sx;
2695
+ if (sx > b.x1) b.x1 = sx;
2696
+ if (sy < b.y0) b.y0 = sy;
2697
+ if (sy > b.y1) b.y1 = sy;
2698
+ };
2699
+ for (const q of group.quads) {
2700
+ const x0 = q.x0 + dx, x1 = q.x1 + dx, y0 = q.y0 + dy, y1 = q.y1 + dy;
2701
+ const base = q.baselineY + dy;
2702
+ const top = (base - y0) * tanItalic;
2703
+ const bottom = (base - y1) * tanItalic;
2704
+ add(x0 + top, y0);
2705
+ add(x1 + top, y0);
2706
+ add(x1 + bottom, y1);
2707
+ add(x0 + bottom, y1);
2708
+ }
2709
+ return b;
2710
+ }
2711
+
2712
+ // src/DrawCommand.ts
2713
+ var SPRITE_STRIDE = 9;
2714
+
2715
+ // src/draw/image.ts
2716
+ function drawImage(ctx, cmd) {
2717
+ ctx.imageCache.upload(cmd.image, cmd.image);
2718
+ const { staged, slot } = stageImage(ctx, cmd.image, cmd.sampling ?? "linear");
2719
+ const src = cmd.source;
2720
+ let u0 = 0, v0 = 0, u1 = 1, v1 = 1;
2721
+ if (src) {
2722
+ const tw = cmd.image.width, th = cmd.image.height;
2723
+ u0 = src.x / tw;
2724
+ u1 = (src.x + src.w) / tw;
2725
+ v0 = src.y / th;
2726
+ v1 = (src.y + src.h) / th;
2727
+ }
2728
+ if (cmd.flipX) {
2729
+ const t = u0;
2730
+ u0 = u1;
2731
+ u1 = t;
2732
+ }
2733
+ if (cmd.flipY) {
2734
+ const t = v0;
2735
+ v0 = v1;
2736
+ v1 = t;
2737
+ }
2738
+ if (!ctx.drawBatch.pushQuad(
2739
+ cmd.x,
2740
+ cmd.y,
2741
+ cmd.w,
2742
+ cmd.h,
2743
+ ctx.state.transform,
2744
+ u0,
2745
+ v0,
2746
+ u1,
2747
+ v1,
2748
+ // Group alpha rides the vertices only where it rode the solid colors too:
2749
+ // with a real color matrix in the run it is `u_alpha`, and folding it here
2750
+ // as well would apply it twice.
2751
+ (cmd.opacity ?? 1) * (staged.foldsAlpha ? ctx.state.alpha : 1),
2752
+ slot
2753
+ )) retryUncut(ctx, cmd, void 0, drawImage);
2754
+ }
2755
+ function drawSprites(ctx, cmd) {
2756
+ const data = cmd.sprites;
2757
+ const count = Math.floor(data.length / SPRITE_STRIDE);
2758
+ if (count === 0) return;
2759
+ ctx.imageCache.upload(cmd.image, cmd.image);
2760
+ const sampling = cmd.sampling ?? "linear";
2761
+ let { staged, slot } = stageImage(ctx, cmd.image, sampling);
2762
+ const batch = ctx.drawBatch;
2763
+ const m = ctx.state.transform;
2764
+ let groupAlpha = staged.foldsAlpha ? ctx.state.alpha : 1;
2765
+ const tw = cmd.image.width;
2766
+ const th = cmd.image.height;
2767
+ for (let s = 0, i = 0; s < count; s++, i += SPRITE_STRIDE) {
2768
+ if (batch.wouldOverflow(4)) {
2769
+ flushBatch(ctx);
2770
+ ({ staged, slot } = stageImage(ctx, cmd.image, sampling));
2771
+ groupAlpha = staged.foldsAlpha ? ctx.state.alpha : 1;
2772
+ }
2773
+ const sx = data[i + 4], sy = data[i + 5], sw = data[i + 6], sh = data[i + 7];
2774
+ let cut;
2775
+ while (!batch.pushQuad(
2776
+ data[i],
2777
+ data[i + 1],
2778
+ data[i + 2],
2779
+ data[i + 3],
2780
+ m,
2781
+ sx / tw,
2782
+ sy / th,
2783
+ (sx + sw) / tw,
2784
+ (sy + sh) / th,
2785
+ data[i + 8] * groupAlpha,
2786
+ slot
2787
+ )) {
2788
+ cut = enterUncut(ctx);
2789
+ ({ staged, slot } = stageImage(ctx, cmd.image, sampling));
2790
+ groupAlpha = staged.foldsAlpha ? ctx.state.alpha : 1;
2791
+ }
2792
+ if (cut !== void 0) {
2793
+ leaveUncut(ctx, cut);
2794
+ ({ staged, slot } = stageImage(ctx, cmd.image, sampling));
2795
+ groupAlpha = staged.foldsAlpha ? ctx.state.alpha : 1;
2796
+ }
2797
+ }
2798
+ }
2799
+
2800
+ // src/draw.ts
2801
+ function dispatch(ctx, cmd) {
2802
+ switch (cmd.kind) {
2803
+ case "group":
2804
+ return drawGroup(ctx, cmd);
2805
+ case "path":
2806
+ return drawPath(ctx, cmd);
2807
+ case "text":
2808
+ return drawText(ctx, cmd);
2809
+ case "image":
2810
+ return drawImage(ctx, cmd);
2811
+ case "sprites":
2812
+ return drawSprites(ctx, cmd);
2813
+ case "shader":
2814
+ flushForOwnDraw(ctx);
2815
+ return drawShader(ctx, cmd);
2816
+ }
2817
+ }
2818
+ function drawGroupWithEffects(ctx, cmd, targets, width, height) {
2819
+ const gl = ctx.gl;
2820
+ flushForOwnDraw(ctx);
2821
+ const parentTarget = ctx.renderTarget ?? null;
2822
+ const parentClipDepth = ctx.clipDepth;
2823
+ const parentClipStack = ctx.clipStack;
2824
+ const parentBatchClip = ctx.drawBatch.clip;
2825
+ const scissorWasOn = gl.isEnabled(gl.SCISSOR_TEST);
2826
+ let front = targets.acquire(width, height);
2827
+ const composited = ctx.state.pushIsolated({
2828
+ transform: cmd.transform,
2829
+ alpha: cmd.alpha,
2830
+ colorMatrix: cmd.colorMatrix
2831
+ });
2832
+ ctx.clipDepth = 0;
2833
+ ctx.clipStack = [];
2834
+ ctx.drawBatch.clip = null;
2835
+ gl.bindFramebuffer(gl.FRAMEBUFFER, front.fbo);
2836
+ ctx.renderTarget = front;
2837
+ gl.viewport(0, 0, width, height);
2838
+ gl.disable(gl.SCISSOR_TEST);
2839
+ gl.stencilMask(255);
2840
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
2841
+ for (const child of cmd.children) dispatch(ctx, child);
2842
+ flushBatch(ctx);
2843
+ gl.disable(gl.BLEND);
2844
+ gl.disable(gl.STENCIL_TEST);
2845
+ for (const effect of cmd.effects) {
2846
+ const program = ctx.ensureProgram?.(effect.program.id) ?? null;
2847
+ if (!program) {
2848
+ warnOnceUniform(effect.program.id, "(program)");
2849
+ continue;
2850
+ }
2851
+ const back = targets.acquire(width, height);
2852
+ gl.bindFramebuffer(gl.FRAMEBUFFER, back.fbo);
2853
+ gl.viewport(0, 0, width, height);
2854
+ gl.clear(gl.COLOR_BUFFER_BIT);
2855
+ drawFullscreenQuad(
2856
+ ctx,
2857
+ program,
2858
+ front.texture,
2859
+ width,
2860
+ height,
2861
+ effect.uniforms,
2862
+ void 0,
2863
+ effect.program.id
2864
+ );
2865
+ targets.release(front);
2866
+ front = back;
2867
+ }
2868
+ gl.enable(gl.BLEND);
2869
+ ctx.state.pop();
2870
+ ctx.clipDepth = parentClipDepth;
2871
+ ctx.clipStack = parentClipStack;
2872
+ ctx.drawBatch.clip = parentBatchClip;
2873
+ ctx.renderTarget = parentTarget;
2874
+ returnToRenderTarget(ctx, width, height, scissorWasOn);
2875
+ const composite = ctx.ensureProgram?.(compositeProgramId()) ?? null;
2876
+ if (composite) {
2877
+ applyClipTest(ctx);
2878
+ drawFullscreenQuad(ctx, composite, front.texture, width, height, void 0, {
2879
+ alpha: composited.alpha,
2880
+ colorMatrix: composited.colorMatrix
2881
+ });
2882
+ }
2883
+ targets.release(front);
2884
+ }
2885
+ function drawGroup(ctx, cmd) {
2886
+ if (cmd.effects && cmd.effects.length > 0 && ctx.effectTargets && ctx.deviceWidth && ctx.deviceHeight) {
2887
+ if (cmd.clip) {
2888
+ ctx.state.push({ transform: cmd.transform });
2889
+ try {
2890
+ enterGroupClip(ctx, cmd.clip);
2891
+ } finally {
2892
+ ctx.state.pop();
2893
+ }
2894
+ }
2895
+ drawGroupWithEffects(ctx, cmd, ctx.effectTargets, ctx.deviceWidth, ctx.deviceHeight);
2896
+ if (cmd.clip) leaveClip(ctx);
2897
+ return;
2898
+ }
2899
+ ctx.state.push({
2900
+ transform: cmd.transform,
2901
+ alpha: cmd.alpha,
2902
+ colorMatrix: cmd.colorMatrix
2903
+ });
2904
+ if (cmd.clip) {
2905
+ try {
2906
+ enterGroupClip(ctx, cmd.clip);
2907
+ } catch (err) {
2908
+ ctx.state.pop();
2909
+ throw err;
2910
+ }
2911
+ }
2912
+ for (const child of cmd.children) dispatch(ctx, child);
2913
+ if (cmd.clip) leaveClip(ctx);
2914
+ ctx.state.pop();
2915
+ }
2916
+
2917
+ // src/shaders/customPrelude.ts
2918
+ var CUSTOM_VERT_SRC = (
2919
+ /* glsl */
2920
+ `#version 300 es
2921
+ in vec2 a_position;
2922
+ in vec2 a_uv;
2923
+ uniform vec4 u_bounds;
2924
+ uniform mat3 u_view;
2925
+ uniform mat3 u_proj;
2926
+ out vec2 v_uv;
2927
+ out vec2 v_screen;
2928
+ out vec2 v_world;
2929
+
2930
+ void main() {
2931
+ float sx = u_bounds.x + a_uv.x * u_bounds.z;
2932
+ float sy = u_bounds.y + a_uv.y * u_bounds.w;
2933
+ v_screen = vec2(sx, sy);
2934
+ v_uv = a_uv;
2935
+
2936
+ float det = u_view[0][0] * u_view[1][1] - u_view[0][1] * u_view[1][0];
2937
+ // A flat view has no inverse: every fragment reads the world origin, not a plausible wrong mapping.
2938
+ float invDet = det != 0.0 ? 1.0 / det : 0.0;
2939
+ v_world = vec2(
2940
+ ((sx - u_view[2][0]) * u_view[1][1] + (sy - u_view[2][1]) * -u_view[0][1]) * invDet,
2941
+ ((sx - u_view[2][0]) * -u_view[1][0] + (sy - u_view[2][1]) * u_view[0][0]) * invDet
2942
+ );
2943
+
2944
+ vec3 clip = u_proj * vec3(sx, sy, 1.0);
2945
+ gl_Position = vec4(clip.xy, 0.0, 1.0);
2946
+ }
2947
+ `
2948
+ );
2949
+ var CUSTOM_ATTRIBUTES = ["a_position", "a_uv"];
2950
+ var CUSTOM_KIT_UNIFORMS = ["u_bounds", "u_view", "u_proj"];
2951
+ var QUAD_VERTICES = new Float32Array([
2952
+ -1,
2953
+ -1,
2954
+ 0,
2955
+ 0,
2956
+ 1,
2957
+ -1,
2958
+ 1,
2959
+ 0,
2960
+ 1,
2961
+ 1,
2962
+ 1,
2963
+ 1,
2964
+ -1,
2965
+ 1,
2966
+ 0,
2967
+ 1
2968
+ ]);
2969
+ var QUAD_INDICES = new Uint16Array([0, 1, 2, 0, 2, 3]);
2970
+
2971
+ // src/effects/EffectTargets.ts
2972
+ var EffectTargets = class {
2973
+ gl;
2974
+ all = [];
2975
+ free = [];
2976
+ width = 0;
2977
+ height = 0;
2978
+ constructor(gl) {
2979
+ this.gl = gl;
2980
+ }
2981
+ /** True once anything has been allocated — what a test asks instead of
2982
+ * reaching for the private arrays. */
2983
+ get allocated() {
2984
+ return this.all.length > 0;
2985
+ }
2986
+ /**
2987
+ * Borrow a target sized `width` × `height` device pixels. A size change
2988
+ * drops every pooled buffer rather than resizing in place: a borrowed one
2989
+ * cannot be resized under its holder, and a resize is not a per-frame event.
2990
+ */
2991
+ acquire(width, height) {
2992
+ if (width !== this.width || height !== this.height) {
2993
+ this.free = [];
2994
+ this.releaseAll();
2995
+ this.width = width;
2996
+ this.height = height;
2997
+ }
2998
+ const pooled = this.free.pop();
2999
+ if (pooled) return pooled;
3000
+ const made = this.create(width, height);
3001
+ this.all.push(made);
3002
+ return made;
3003
+ }
3004
+ /** Hand one back. Order does not matter — every target is interchangeable. */
3005
+ release(target) {
3006
+ this.free.push(target);
3007
+ }
3008
+ /** Take back every target still out on loan. A frame that threw inside a
3009
+ * group with effects never released the one it was drawing into. */
3010
+ reclaim() {
3011
+ if (this.free.length !== this.all.length) this.free = [...this.all];
3012
+ }
3013
+ /** Drop every buffer. Called on dispose and on context loss, where the
3014
+ * handles are already invalid and only the bookkeeping has to go. */
3015
+ releaseAll() {
3016
+ const gl = this.gl;
3017
+ for (const t of this.all) {
3018
+ gl.deleteFramebuffer(t.fbo);
3019
+ gl.deleteTexture(t.texture);
3020
+ gl.deleteRenderbuffer(t.stencil);
3021
+ }
3022
+ this.all.length = 0;
3023
+ this.free = [];
3024
+ this.width = 0;
3025
+ this.height = 0;
3026
+ }
3027
+ create(width, height) {
3028
+ const gl = this.gl;
3029
+ const texture = gl.createTexture();
3030
+ gl.bindTexture(gl.TEXTURE_2D, texture);
3031
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
3032
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
3033
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
3034
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
3035
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
3036
+ gl.bindTexture(gl.TEXTURE_2D, null);
3037
+ const stencil = gl.createRenderbuffer();
3038
+ gl.bindRenderbuffer(gl.RENDERBUFFER, stencil);
3039
+ gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, width, height);
3040
+ gl.bindRenderbuffer(gl.RENDERBUFFER, null);
3041
+ const fbo = gl.createFramebuffer();
3042
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
3043
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
3044
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, stencil);
3045
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
3046
+ return { fbo, texture, stencil };
3047
+ }
3048
+ };
3049
+
3050
+ // src/shaders/extractUniformNames.ts
3051
+ var QUALIFIERS = /* @__PURE__ */ new Set([
3052
+ "lowp",
3053
+ "mediump",
3054
+ "highp",
3055
+ "precise",
3056
+ "invariant",
3057
+ "centroid",
3058
+ "flat",
3059
+ "smooth",
3060
+ "noperspective"
3061
+ ]);
3062
+ var IDENT = /^[A-Za-z_]\w*$/;
3063
+ var MAX_DEPTH = 16;
3064
+ function extractUniformNames(glsl) {
3065
+ return [...scanUniforms(glsl).names];
3066
+ }
3067
+ function extractUniformArrays(glsl) {
3068
+ return scanUniforms(glsl).arrays;
3069
+ }
3070
+ function scanUniforms(glsl) {
3071
+ const { code, defines } = preprocess(glsl);
3072
+ const toks = tokenize(code);
3073
+ const consts = /* @__PURE__ */ new Map();
3074
+ const structs = /* @__PURE__ */ new Map();
3075
+ const out = /* @__PURE__ */ new Set();
3076
+ const arrays = /* @__PURE__ */ new Map();
3077
+ const resolveName = (name, depth2) => {
3078
+ if (depth2 > MAX_DEPTH) return null;
3079
+ let best = null;
3080
+ for (const body of defines.get(name) ?? []) {
3081
+ const v = evalInt(tokenize(body), (n) => resolveName(n, depth2 + 1));
3082
+ if (v !== null && (best === null || v > best)) best = v;
3083
+ }
3084
+ return best ?? consts.get(name) ?? null;
3085
+ };
3086
+ const evalSize = (from, to) => evalInt(toks.slice(from, to), (n) => resolveName(n, 0));
3087
+ const closer = (open) => {
3088
+ let depth2 = 0;
3089
+ for (let k = open; k < toks.length; k++) {
3090
+ if (toks[k] === "(" || toks[k] === "[" || toks[k] === "{") depth2++;
3091
+ else if (toks[k] === ")" || toks[k] === "]" || toks[k] === "}") {
3092
+ if (--depth2 === 0) return k;
3093
+ }
3094
+ }
3095
+ return toks.length;
3096
+ };
3097
+ const arraySuffix = (j) => {
3098
+ if (toks[j] !== "[") return [void 0, j];
3099
+ const end = closer(j);
3100
+ return [evalSize(j + 1, end), end + 1];
3101
+ };
3102
+ const declarators = (j, typeSize) => {
3103
+ const found = [];
3104
+ while (j < toks.length && toks[j] !== ";" && toks[j] !== "}") {
3105
+ const name = toks[j];
3106
+ const [size, next] = arraySuffix(j + 1);
3107
+ j = next;
3108
+ if (IDENT.test(name)) found.push({ name, size: size === void 0 ? typeSize : size });
3109
+ while (j < toks.length && toks[j] !== "," && toks[j] !== ";" && toks[j] !== "}") {
3110
+ j = toks[j] === "(" || toks[j] === "[" || toks[j] === "{" ? closer(j) + 1 : j + 1;
3111
+ }
3112
+ if (toks[j] === ",") j++;
3113
+ }
3114
+ return [found, j];
3115
+ };
3116
+ const structSpecifier = (i) => {
3117
+ let j = i + 1;
3118
+ const name = toks[j] !== "{" ? toks[j++] : void 0;
3119
+ const members = [];
3120
+ if (toks[j] !== "{") return [members, j];
3121
+ const end = closer(j);
3122
+ j++;
3123
+ while (j < end) {
3124
+ while (QUALIFIERS.has(toks[j])) j++;
3125
+ let type;
3126
+ if (toks[j] === "struct") [type, j] = structSpecifier(j);
3127
+ else type = toks[j++];
3128
+ const [typeSize, afterType] = arraySuffix(j);
3129
+ const [decls, term] = declarators(afterType, typeSize);
3130
+ for (const d of decls) if (d.size !== null) members.push({ type, name: d.name, size: d.size ?? null });
3131
+ j = term + 1;
3132
+ }
3133
+ if (name) {
3134
+ const prior = structs.get(name) ?? [];
3135
+ for (const m of members) if (!prior.some((p) => p.name === m.name)) prior.push(m);
3136
+ structs.set(name, prior);
3137
+ }
3138
+ return [members, end + 1];
3139
+ };
3140
+ const expand = (name, type, size, depth2) => {
3141
+ if (depth2 > MAX_DEPTH) return;
3142
+ if (size !== null) {
3143
+ if (typeof type === "string" && !structs.has(type)) {
3144
+ const prior = arrays.get(name);
3145
+ if (!prior || size > prior.size) arrays.set(name, { type, size });
3146
+ }
3147
+ for (let k = 0; k < size; k++) expand(`${name}[${k}]`, type, null, depth2 + 1);
3148
+ return;
3149
+ }
3150
+ const members = typeof type === "string" ? structs.get(type) : type;
3151
+ if (!members) {
3152
+ out.add(name);
3153
+ return;
3154
+ }
3155
+ for (const m of members) expand(`${name}.${m.name}`, m.type, m.size, depth2 + 1);
3156
+ };
3157
+ let depth = 0;
3158
+ for (let i = 0; i < toks.length; i++) {
3159
+ const t = toks[i];
3160
+ if (t === "{") depth++;
3161
+ else if (t === "}") depth--;
3162
+ else if (t === "struct") i = structSpecifier(i)[1] - 1;
3163
+ else if (depth === 0 && t === "const") {
3164
+ let j = i + 1;
3165
+ while (QUALIFIERS.has(toks[j])) j++;
3166
+ if ((toks[j] === "int" || toks[j] === "uint") && IDENT.test(toks[j + 1] ?? "") && toks[j + 2] === "=") {
3167
+ let end = j + 3;
3168
+ while (end < toks.length && toks[end] !== ";") end++;
3169
+ const v = evalSize(j + 3, end);
3170
+ const prior = consts.get(toks[j + 1]);
3171
+ if (v !== null && (prior === void 0 || v > prior)) consts.set(toks[j + 1], v);
3172
+ }
3173
+ } else if (depth === 0 && t === "uniform") {
3174
+ let j = i + 1;
3175
+ while (QUALIFIERS.has(toks[j])) j++;
3176
+ let type;
3177
+ if (toks[j] === "struct") {
3178
+ [type, j] = structSpecifier(j);
3179
+ } else if (toks[j + 1] === "{") {
3180
+ j = closer(j + 1) + 1;
3181
+ while (j < toks.length && toks[j] !== ";") j++;
3182
+ i = j;
3183
+ continue;
3184
+ } else {
3185
+ type = toks[j++];
3186
+ }
3187
+ const [typeSize, afterType] = arraySuffix(j);
3188
+ const [decls, term] = declarators(afterType, typeSize);
3189
+ for (const d of decls) if (d.size !== null) expand(d.name, type, d.size ?? null, 0);
3190
+ i = term;
3191
+ }
3192
+ }
3193
+ return { names: out, arrays };
3194
+ }
3195
+ function preprocess(glsl) {
3196
+ const spliced = glsl.replace(/\\\r?\n/g, "");
3197
+ let stripped = "";
3198
+ for (let k = 0; k < spliced.length; k++) {
3199
+ if (spliced[k] === "/" && spliced[k + 1] === "/") {
3200
+ while (k < spliced.length && spliced[k] !== "\n") k++;
3201
+ stripped += "\n";
3202
+ } else if (spliced[k] === "/" && spliced[k + 1] === "*") {
3203
+ const end = spliced.indexOf("*/", k + 2);
3204
+ k = end < 0 ? spliced.length : end + 1;
3205
+ stripped += " ";
3206
+ } else {
3207
+ stripped += spliced[k];
3208
+ }
3209
+ }
3210
+ const defines = /* @__PURE__ */ new Map();
3211
+ const lines = stripped.split("\n").map((line) => {
3212
+ if (!/^\s*#/.test(line)) return line;
3213
+ const def = /^\s*#\s*define\s+([A-Za-z_]\w*)(\(?)(.*)$/.exec(line);
3214
+ if (def && !def[2]) defines.set(def[1], [...defines.get(def[1]) ?? [], def[3].trim()]);
3215
+ return "";
3216
+ });
3217
+ return { code: lines.join("\n"), defines };
3218
+ }
3219
+ function tokenize(src) {
3220
+ return src.match(/[A-Za-z_]\w*|\d[\w.]*|\S/g) ?? [];
3221
+ }
3222
+ function intLiteral(tok) {
3223
+ const m = /^(?:0[xX]([0-9a-fA-F]+)|(0[0-7]*)|([1-9]\d*))[uU]?$/.exec(tok);
3224
+ if (!m) return null;
3225
+ if (m[1] !== void 0) return parseInt(m[1], 16);
3226
+ if (m[2] !== void 0) return parseInt(m[2], 8);
3227
+ return parseInt(m[3], 10);
3228
+ }
3229
+ function evalInt(toks, lookup) {
3230
+ let p = 0;
3231
+ const primary = () => {
3232
+ const t = toks[p++];
3233
+ if (t === "(") {
3234
+ const v2 = sum();
3235
+ return toks[p++] === ")" ? v2 : null;
3236
+ }
3237
+ if (t === "-") {
3238
+ const v2 = primary();
3239
+ return v2 === null ? null : -v2;
3240
+ }
3241
+ if (t === "+") return primary();
3242
+ if (t === void 0) return null;
3243
+ return intLiteral(t) ?? (IDENT.test(t) ? lookup(t) : null);
3244
+ };
3245
+ const product = () => {
3246
+ let v2 = primary();
3247
+ while (v2 !== null && (toks[p] === "*" || toks[p] === "/" || toks[p] === "%")) {
3248
+ const op = toks[p++];
3249
+ const r = primary();
3250
+ if (r === null || op !== "*" && r === 0) return null;
3251
+ v2 = op === "*" ? v2 * r : op === "/" ? Math.trunc(v2 / r) : v2 % r;
3252
+ }
3253
+ return v2;
3254
+ };
3255
+ const sum = () => {
3256
+ let v2 = product();
3257
+ while (v2 !== null && (toks[p] === "+" || toks[p] === "-")) {
3258
+ const op = toks[p++];
3259
+ const r = product();
3260
+ if (r === null) return null;
3261
+ v2 = op === "+" ? v2 + r : v2 - r;
3262
+ }
3263
+ return v2;
3264
+ };
3265
+ const v = sum();
3266
+ return p === toks.length && v !== null && v >= 0 ? v : null;
3267
+ }
3268
+
3269
+ // src/WeaselRenderer.ts
3270
+ function customUniformNames(vertSrc, fragSrc2) {
3271
+ return [...CUSTOM_KIT_UNIFORMS, ...extractUniformNames(vertSrc), ...extractUniformNames(fragSrc2)];
3272
+ }
3273
+ function compileCustomProgram(gl, src) {
3274
+ const vertSrc = src.vert === "" ? CUSTOM_VERT_SRC : src.vert;
3275
+ const program = new ShaderProgram(gl, vertSrc, src.frag);
3276
+ program.lookupUniforms(customUniformNames(vertSrc, src.frag));
3277
+ program.lookupUniformArrays(new Map([...extractUniformArrays(vertSrc), ...extractUniformArrays(src.frag)]));
3278
+ program.lookupAttributes(CUSTOM_ATTRIBUTES);
3279
+ return program;
3280
+ }
3281
+ function now() {
3282
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
3283
+ }
3284
+ var GLYPH_PAINT_SOURCES = {
3285
+ pattern: [GLYPH_PATTERN_FRAG_SRC, GLYPH_PATTERN_UNIFORMS],
3286
+ gradient: [GLYPH_GRAD_FRAG_SRC, GLYPH_GRAD_UNIFORMS],
3287
+ texture: [GLYPH_TEXTURE_FRAG_SRC, GLYPH_TEXTURE_UNIFORMS]
3288
+ };
3289
+ var WeaselRenderer = class {
3290
+ gl;
3291
+ stats = { drawCalls: 0, ms: 0, spans: [] };
3292
+ pathFill;
3293
+ pathFillVColor;
3294
+ batchFill;
3295
+ gradFill;
3296
+ patternFill;
3297
+ /** Compiled on first use: only atlas text under a texture paint needs one. */
3298
+ glyphPaintPrograms = /* @__PURE__ */ new Map();
3299
+ meshCache;
3300
+ textureCache;
3301
+ imageCache;
3302
+ gradRamps;
3303
+ programRegistry = /* @__PURE__ */ new Map();
3304
+ /** The source each compiled custom program was built from — or, after a
3305
+ * failed recompile, the source that failed, so it is not retried every frame. */
3306
+ programSources = /* @__PURE__ */ new Map();
3307
+ quadVbo = null;
3308
+ quadIbo = null;
3309
+ drawBatch;
3310
+ /** 1x1 white, so a batch flush sampling no image still samples something —
3311
+ * see `shaders/batchFill.ts`. */
3312
+ whiteTexture = null;
3313
+ groupState = new GroupState();
3314
+ widthCss;
3315
+ heightCss;
3316
+ dpr;
3317
+ canvas = null;
3318
+ /** Where context loss is announced: the context's own canvas, which the
3319
+ * renderer need not own the size of. */
3320
+ lossTarget = null;
3321
+ target = null;
3322
+ /** Offscreen buffers for group effects. Allocates nothing until a group
3323
+ * with effects asks, so a canvas without them pays no memory. */
3324
+ effectTargets;
3325
+ paintScratch;
3326
+ imageMinification;
3327
+ flattenTolerance;
3328
+ bakeBudget;
3329
+ textOutlineMinScreenSize;
3330
+ contextLost = false;
3331
+ boundOnLost = (e) => this.onContextLost(e);
3332
+ boundOnRestored = () => this.onContextRestored();
3333
+ /** True after `dispose()`. A disposed renderer ignores further `render()`
3334
+ * calls and `registerProgram()` calls. */
3335
+ disposed = false;
3336
+ /** What registered paint kinds hang their GPU state on. One per context
3337
+ * generation: a restore releases it and opens the next. */
3338
+ paintResources = createPaintResources();
3339
+ constructor(opts) {
3340
+ if (!opts.gl && !opts.canvas) {
3341
+ throw new Error("WeaselRenderer requires either gl or canvas");
3342
+ }
3343
+ const gl = opts.gl ?? opts.canvas.getContext("webgl2", { stencil: true });
3344
+ if (!gl) throw new Error("WeaselRenderer: WebGL2 not available");
3345
+ this.gl = gl;
3346
+ this.effectTargets = new EffectTargets(this.gl);
3347
+ this.paintScratch = new PaintScratch(this.gl);
3348
+ const attrs = typeof this.gl.getContextAttributes === "function" ? this.gl.getContextAttributes() : null;
3349
+ if (attrs && attrs.stencil === false) {
3350
+ throw new Error(
3351
+ "WeaselRenderer: the supplied WebGL2 context has no stencil buffer. Create it with { stencil: true } \u2014 clips and even-odd fills need one."
3352
+ );
3353
+ }
3354
+ this.widthCss = opts.width;
3355
+ this.heightCss = opts.height;
3356
+ this.dpr = opts.dpr;
3357
+ this.imageMinification = opts.imageMinification ?? "linear";
3358
+ this.flattenTolerance = opts.flattenTolerance;
3359
+ this.bakeBudget = opts.bakeBudget ?? DEFAULT_BAKE_BUDGET;
3360
+ this.textOutlineMinScreenSize = opts.textOutlineMinScreenSize ?? OUTLINE_MIN_SCREEN_PX;
3361
+ this.canvas = opts.canvas ?? null;
3362
+ if (this.canvas) {
3363
+ this.canvas.width = Math.round(opts.width * opts.dpr);
3364
+ this.canvas.height = Math.round(opts.height * opts.dpr);
3365
+ if (this.canvas.style) {
3366
+ this.canvas.style.width = `${opts.width}px`;
3367
+ this.canvas.style.height = `${opts.height}px`;
3368
+ }
3369
+ }
3370
+ const lossTarget = this.canvas ?? this.gl.canvas ?? null;
3371
+ if (lossTarget && typeof lossTarget.addEventListener === "function") {
3372
+ this.lossTarget = lossTarget;
3373
+ this.lossTarget.addEventListener("webglcontextlost", this.boundOnLost);
3374
+ this.lossTarget.addEventListener("webglcontextrestored", this.boundOnRestored);
3375
+ }
3376
+ this.applyGlState();
3377
+ this.applyTarget();
3378
+ this.pathFill = new ShaderProgram(this.gl, VERT_SRC, FRAG_SRC);
3379
+ this.pathFill.lookupUniforms(PATH_FILL_UNIFORMS);
3380
+ this.pathFill.lookupAttributes(PATH_FILL_ATTRIBUTES);
3381
+ this.pathFillVColor = new ShaderProgram(this.gl, VCOLOR_VERT_SRC, VCOLOR_FRAG_SRC);
3382
+ this.pathFillVColor.lookupUniforms(PATH_FILL_UNIFORMS);
3383
+ this.pathFillVColor.lookupAttributes(PATH_FILL_VCOLOR_ATTRIBUTES);
3384
+ this.batchFill = new ShaderProgram(this.gl, BATCH_VERT_SRC, BATCH_FRAG_SRC);
3385
+ this.batchFill.lookupUniforms(BATCH_FILL_UNIFORMS);
3386
+ this.batchFill.lookupAttributes(BATCH_FILL_ATTRIBUTES);
3387
+ this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
3388
+ this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
3389
+ this.gradFill.lookupAttributes(GRAD_FILL_ATTRIBUTES);
3390
+ this.patternFill = new ShaderProgram(this.gl, PATTERN_VERT_SRC, PATTERN_FRAG_SRC);
3391
+ this.patternFill.lookupUniforms(PATTERN_FILL_UNIFORMS);
3392
+ this.patternFill.lookupAttributes(PATTERN_FILL_ATTRIBUTES);
3393
+ this.meshCache = new GLMeshCache(this.gl);
3394
+ this.textureCache = new GLTextureCache(this.gl);
3395
+ this.imageCache = new GLImageCache(this.gl, this.imageMinification);
3396
+ this.gradRamps = new GradientRampAtlas(this.gl);
3397
+ this.uploadQuadGeometry();
3398
+ this.drawBatch = new DrawBatch(this.gl, this.batchFill);
3399
+ this.whiteTexture = createWhiteTexture(this.gl);
3400
+ }
3401
+ uploadQuadGeometry() {
3402
+ const gl = this.gl;
3403
+ this.quadVbo = gl.createBuffer();
3404
+ this.quadIbo = gl.createBuffer();
3405
+ if (!this.quadVbo || !this.quadIbo) {
3406
+ throw new Error("WeaselRenderer: failed to create quad geometry buffers");
3407
+ }
3408
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVbo);
3409
+ gl.bufferData(gl.ARRAY_BUFFER, QUAD_VERTICES, gl.STATIC_DRAW);
3410
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
3411
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadIbo);
3412
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, QUAD_INDICES, gl.STATIC_DRAW);
3413
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
3414
+ }
3415
+ /**
3416
+ * Compile a consumer-registered shader program against this renderer's GL context.
3417
+ *
3418
+ * Call once per renderer after the module-level `registerProgram()`.
3419
+ * Throws `ShaderCompileError` if compilation fails.
3420
+ *
3421
+ * In dev mode, calling again with the same handle replaces the compiled program.
3422
+ *
3423
+ * @experimental
3424
+ */
3425
+ registerProgram(handle) {
3426
+ if (this.disposed) return;
3427
+ const src = getProgramSource(handle.id);
3428
+ if (!src) {
3429
+ throw new Error(
3430
+ `WeaselRenderer.registerProgram: program "${handle.id}" not found in source registry. Call the module-level registerProgram() first.`
3431
+ );
3432
+ }
3433
+ this.installProgram(handle.id, src, compileCustomProgram(this.gl, src));
3434
+ }
3435
+ installProgram(id, src, program) {
3436
+ const previous = this.programRegistry.get(id);
3437
+ if (previous) this.gl.deleteProgram(previous.handle);
3438
+ this.programRegistry.set(id, program);
3439
+ this.programSources.set(id, src);
3440
+ }
3441
+ /** Recompile every program whose module-level source was re-registered
3442
+ * since it was compiled. A source that fails keeps the old program drawing. */
3443
+ refreshPrograms() {
3444
+ for (const [id, compiled] of this.programSources) {
3445
+ const src = getProgramSource(id);
3446
+ if (!src || src === compiled) continue;
3447
+ if (src.vert === compiled.vert && src.frag === compiled.frag) {
3448
+ this.programSources.set(id, src);
3449
+ continue;
3450
+ }
3451
+ try {
3452
+ this.installProgram(id, src, compileCustomProgram(this.gl, src));
3453
+ } catch (e) {
3454
+ this.programSources.set(id, src);
3455
+ console.error(`weasel: failed to recompile re-registered program "${id}"; still drawing the previous one:`, e);
3456
+ }
3457
+ }
3458
+ }
3459
+ /**
3460
+ * The compiled program for `id`, compiling it against this context on first
3461
+ * use. Unlike {@link registerProgram} this neither throws on a missing
3462
+ * source nor on a compile failure: a paint kind asking for a program it
3463
+ * never registered must decline the paint, not take down the frame.
3464
+ */
3465
+ ensureProgram(id) {
3466
+ const existing = this.programRegistry.get(id);
3467
+ if (existing) return existing;
3468
+ if (this.disposed || !getProgramSource(id)) return null;
3469
+ try {
3470
+ this.registerProgram({ id });
3471
+ } catch (e) {
3472
+ console.error(`weasel: failed to compile paint program "${id}":`, e);
3473
+ return null;
3474
+ }
3475
+ return this.programRegistry.get(id) ?? null;
3476
+ }
3477
+ glyphPaintProgram(kind) {
3478
+ const existing = this.glyphPaintPrograms.get(kind);
3479
+ if (existing) return existing;
3480
+ const [frag, uniforms] = GLYPH_PAINT_SOURCES[kind];
3481
+ const program = new ShaderProgram(this.gl, GLYPH_PAINT_VERT_SRC, frag);
3482
+ program.lookupUniforms(uniforms);
3483
+ program.lookupAttributes(GLYPH_PAINT_ATTRIBUTES);
3484
+ this.glyphPaintPrograms.set(kind, program);
3485
+ return program;
3486
+ }
3487
+ /** The blend, depth, cull, clear-color, color-mask, VAO, and unpack-flip
3488
+ * state every frame assumes.
3489
+ * Applied per `render()` rather than once at construction because a
3490
+ * co-tenant sharing this context moves all of it between our frames. */
3491
+ applyGlState() {
3492
+ const gl = this.gl;
3493
+ gl.enable(gl.BLEND);
3494
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
3495
+ gl.disable(gl.DEPTH_TEST);
3496
+ gl.disable(gl.CULL_FACE);
3497
+ gl.clearColor(0, 0, 0, 0);
3498
+ gl.colorMask(true, true, true, true);
3499
+ gl.bindVertexArray(null);
3500
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
3501
+ }
3502
+ /** Viewport and scissor for this frame. Re-applied inside `render()` because
3503
+ * a co-tenant on the same context moves both between our frames. */
3504
+ /** This renderer's rect in device pixels, top-left origin. A tile rounds its
3505
+ * edges rather than its size, so two that share a CSS edge share a device one. */
3506
+ deviceRect() {
3507
+ const ox = this.target?.origin.x ?? 0;
3508
+ const oy = this.target?.origin.y ?? 0;
3509
+ const x = Math.round(ox * this.dpr);
3510
+ const y = Math.round(oy * this.dpr);
3511
+ return {
3512
+ x,
3513
+ y,
3514
+ width: Math.round((ox + this.widthCss) * this.dpr) - x,
3515
+ height: Math.round((oy + this.heightCss) * this.dpr) - y
3516
+ };
3517
+ }
3518
+ applyTarget() {
3519
+ const gl = this.gl;
3520
+ const { x, y, width, height } = this.deviceRect();
3521
+ if (!this.target) {
3522
+ gl.disable(gl.SCISSOR_TEST);
3523
+ gl.viewport(0, 0, width, height);
3524
+ return;
3525
+ }
3526
+ const glY = gl.drawingBufferHeight - y - height;
3527
+ gl.viewport(x, glY, width, height);
3528
+ gl.scissor(x, glY, width, height);
3529
+ gl.enable(gl.SCISSOR_TEST);
3530
+ }
3531
+ /** Confine this renderer to a rect of its drawing buffer, or pass null to
3532
+ * give it the whole buffer back. Takes effect from the next `render()`. */
3533
+ setTarget(target) {
3534
+ this.target = target;
3535
+ this.applyTarget();
3536
+ }
3537
+ getTarget() {
3538
+ return this.target;
3539
+ }
3540
+ isContextLost() {
3541
+ return this.contextLost;
3542
+ }
3543
+ onContextLost(e) {
3544
+ e.preventDefault();
3545
+ this.contextLost = true;
3546
+ }
3547
+ onContextRestored() {
3548
+ this.contextLost = false;
3549
+ this.applyGlState();
3550
+ this.applyTarget();
3551
+ this.effectTargets.releaseAll();
3552
+ this.paintScratch.release();
3553
+ this.paintResources.release();
3554
+ this.paintResources = createPaintResources();
3555
+ this.pathFill = new ShaderProgram(this.gl, VERT_SRC, FRAG_SRC);
3556
+ this.pathFill.lookupUniforms(PATH_FILL_UNIFORMS);
3557
+ this.pathFill.lookupAttributes(PATH_FILL_ATTRIBUTES);
3558
+ this.pathFillVColor = new ShaderProgram(this.gl, VCOLOR_VERT_SRC, VCOLOR_FRAG_SRC);
3559
+ this.pathFillVColor.lookupUniforms(PATH_FILL_UNIFORMS);
3560
+ this.pathFillVColor.lookupAttributes(PATH_FILL_VCOLOR_ATTRIBUTES);
3561
+ this.batchFill = new ShaderProgram(this.gl, BATCH_VERT_SRC, BATCH_FRAG_SRC);
3562
+ this.batchFill.lookupUniforms(BATCH_FILL_UNIFORMS);
3563
+ this.batchFill.lookupAttributes(BATCH_FILL_ATTRIBUTES);
3564
+ this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
3565
+ this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
3566
+ this.gradFill.lookupAttributes(GRAD_FILL_ATTRIBUTES);
3567
+ this.patternFill = new ShaderProgram(this.gl, PATTERN_VERT_SRC, PATTERN_FRAG_SRC);
3568
+ this.patternFill.lookupUniforms(PATTERN_FILL_UNIFORMS);
3569
+ this.patternFill.lookupAttributes(PATTERN_FILL_ATTRIBUTES);
3570
+ this.glyphPaintPrograms.clear();
3571
+ this.meshCache = new GLMeshCache(this.gl);
3572
+ this.textureCache = new GLTextureCache(this.gl);
3573
+ this.imageCache.abandon();
3574
+ this.imageCache = new GLImageCache(this.gl, this.imageMinification);
3575
+ this.gradRamps = new GradientRampAtlas(this.gl);
3576
+ markAllFontsNotUploaded();
3577
+ this.uploadQuadGeometry();
3578
+ this.drawBatch = new DrawBatch(this.gl, this.batchFill);
3579
+ this.whiteTexture = createWhiteTexture(this.gl);
3580
+ for (const id of this.programRegistry.keys()) {
3581
+ const src = getProgramSource(id);
3582
+ if (!src) continue;
3583
+ try {
3584
+ this.programRegistry.set(id, compileCustomProgram(this.gl, src));
3585
+ this.programSources.set(id, src);
3586
+ } catch (e) {
3587
+ console.error(`weasel: failed to recompile program "${id}" after context restore:`, e);
3588
+ }
3589
+ }
3590
+ }
3591
+ /** Free every GL resource this renderer created and detach its
3592
+ * context-loss listeners. Idempotent.
3593
+ *
3594
+ * That covers the built-in and consumer-registered programs (their shaders
3595
+ * go with them), the shared quad geometry, the batch, the effect targets,
3596
+ * and every cache: atlas and gradient-ramp textures, bitmap and pattern
3597
+ * textures, and meshes — transient, persistent, and those whose Path was
3598
+ * already collected. The context itself is left alone, so a caller-owned
3599
+ * context can host renderer after renderer without accumulating anything.
3600
+ *
3601
+ * A disposed renderer ignores further `render()` and `registerProgram()`
3602
+ * calls. */
3603
+ dispose() {
3604
+ if (this.disposed) return;
3605
+ this.disposed = true;
3606
+ const gl = this.gl;
3607
+ if (this.lossTarget) {
3608
+ this.lossTarget.removeEventListener("webglcontextlost", this.boundOnLost);
3609
+ this.lossTarget.removeEventListener("webglcontextrestored", this.boundOnRestored);
3610
+ }
3611
+ this.meshCache.dispose();
3612
+ this.paintResources.release();
3613
+ for (const prog of [
3614
+ this.pathFill,
3615
+ this.pathFillVColor,
3616
+ this.batchFill,
3617
+ this.gradFill,
3618
+ this.patternFill,
3619
+ ...this.glyphPaintPrograms.values()
3620
+ ]) {
3621
+ gl.deleteProgram(prog.handle);
3622
+ }
3623
+ this.glyphPaintPrograms.clear();
3624
+ for (const prog of this.programRegistry.values()) {
3625
+ gl.deleteProgram(prog.handle);
3626
+ }
3627
+ this.programRegistry.clear();
3628
+ this.programSources.clear();
3629
+ this.textureCache.free();
3630
+ this.imageCache.dispose();
3631
+ this.gradRamps.free();
3632
+ if (this.quadVbo) gl.deleteBuffer(this.quadVbo);
3633
+ if (this.quadIbo) gl.deleteBuffer(this.quadIbo);
3634
+ this.drawBatch.dispose();
3635
+ if (this.whiteTexture) gl.deleteTexture(this.whiteTexture);
3636
+ this.whiteTexture = null;
3637
+ this.effectTargets.releaseAll();
3638
+ this.paintScratch.release();
3639
+ }
3640
+ /**
3641
+ * Draw one frame.
3642
+ *
3643
+ * `viewMatrix` is the frame's world→screen transform. It is only consulted
3644
+ * by `units: 'world'` gradients, which fall back to screen space without
3645
+ * it — every other command carries its own transform in the stream, so
3646
+ * callers with no view concept can keep calling `render(commands)`.
3647
+ */
3648
+ render(commands, viewMatrix, opts) {
3649
+ if (this.contextLost || this.disposed) return;
3650
+ const t0 = now();
3651
+ const counters = { drawCalls: 0 };
3652
+ const spanStats = [];
3653
+ this.stats = { drawCalls: 0, ms: 0, spans: spanStats };
3654
+ const gl = this.gl;
3655
+ this.meshCache.drainPendingDeletes();
3656
+ this.imageCache.drainPendingDeletes();
3657
+ this.textureCache.sweep(textureRegistry);
3658
+ this.refreshPrograms();
3659
+ resetBakeBudget(this.bakeBudget);
3660
+ this.groupState.reset();
3661
+ this.drawBatch.reset();
3662
+ this.effectTargets.reclaim();
3663
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
3664
+ this.applyGlState();
3665
+ this.applyTarget();
3666
+ if (this.target?.clear !== false) {
3667
+ gl.stencilMask(255);
3668
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
3669
+ }
3670
+ const device = this.deviceRect();
3671
+ const ctx = {
3672
+ gl,
3673
+ pathFill: this.pathFill,
3674
+ pathFillVColor: this.pathFillVColor,
3675
+ batchFill: this.batchFill,
3676
+ gradFill: this.gradFill,
3677
+ patternFill: this.patternFill,
3678
+ meshCache: this.meshCache,
3679
+ textureCache: this.textureCache,
3680
+ imageCache: this.imageCache,
3681
+ gradRamps: this.gradRamps,
3682
+ programRegistry: this.programRegistry,
3683
+ ensureProgram: (id) => this.ensureProgram(id),
3684
+ glyphPaintProgram: (kind) => this.glyphPaintProgram(kind),
3685
+ quadVbo: this.quadVbo,
3686
+ quadIbo: this.quadIbo,
3687
+ drawBatch: this.drawBatch,
3688
+ whiteTexture: this.whiteTexture,
3689
+ state: this.groupState,
3690
+ widthCss: this.widthCss,
3691
+ heightCss: this.heightCss,
3692
+ clipDepth: 0,
3693
+ clipStack: [],
3694
+ flattenTolerance: this.flattenTolerance,
3695
+ textOutlineMinScreenSize: this.textOutlineMinScreenSize,
3696
+ viewMatrix,
3697
+ effectTargets: this.effectTargets,
3698
+ paintScratch: this.paintScratch,
3699
+ renderTarget: null,
3700
+ deviceWidth: device.width,
3701
+ deviceHeight: device.height,
3702
+ restoreTargetRect: () => this.applyTarget(),
3703
+ paintResources: this.paintResources,
3704
+ stats: counters
3705
+ };
3706
+ this.drawBatch.clip = null;
3707
+ const spans = opts?.spans ?? [];
3708
+ let next = 0;
3709
+ for (let si = 0; si < spans.length; si++) {
3710
+ const span = spans[si];
3711
+ for (; next < span.start; next++) dispatch(ctx, commands[next]);
3712
+ const drawsBefore = counters.drawCalls;
3713
+ const spanT0 = now();
3714
+ for (; next < span.end; next++) dispatch(ctx, commands[next]);
3715
+ if (si === spans.length - 1 && next >= commands.length) flushBatch(ctx);
3716
+ spanStats.push({ id: span.id, drawCalls: counters.drawCalls - drawsBefore, ms: now() - spanT0 });
3717
+ }
3718
+ for (; next < commands.length; next++) dispatch(ctx, commands[next]);
3719
+ flushBatch(ctx);
3720
+ this.meshCache.freeTransient();
3721
+ this.stats.drawCalls = counters.drawCalls;
3722
+ this.stats.ms = now() - t0;
3723
+ }
3724
+ /** What the last `render()` cost: GL draw calls issued and CPU milliseconds
3725
+ * spent issuing them, in total and per requested span. GPU time is not
3726
+ * measured; the GPU runs asynchronously. */
3727
+ lastFrameStats() {
3728
+ return this.stats;
3729
+ }
3730
+ /** Change the curve-flattening tolerance after construction. `undefined`
3731
+ * restores the cached `DEFAULT_FLATTEN_TOLERANCE` route. */
3732
+ setFlattenTolerance(tolerance) {
3733
+ this.flattenTolerance = tolerance;
3734
+ }
3735
+ resize(dims) {
3736
+ this.widthCss = dims.width;
3737
+ this.heightCss = dims.height;
3738
+ this.dpr = dims.dpr;
3739
+ if (this.canvas) {
3740
+ this.canvas.width = Math.round(dims.width * dims.dpr);
3741
+ this.canvas.height = Math.round(dims.height * dims.dpr);
3742
+ if (this.canvas.style) {
3743
+ this.canvas.style.width = `${dims.width}px`;
3744
+ this.canvas.style.height = `${dims.height}px`;
3745
+ }
3746
+ }
3747
+ this.applyTarget();
3748
+ }
3749
+ /** @internal */
3750
+ _gl() {
3751
+ return this.gl;
3752
+ }
3753
+ /** @internal */
3754
+ _pathFill() {
3755
+ return this.pathFill;
3756
+ }
3757
+ /** @internal */
3758
+ _pathFillVColor() {
3759
+ return this.pathFillVColor;
3760
+ }
3761
+ /** @internal */
3762
+ _drawBatch() {
3763
+ return this.drawBatch;
3764
+ }
3765
+ /** @internal */
3766
+ _batchFill() {
3767
+ return this.batchFill;
3768
+ }
3769
+ /** @internal */
3770
+ _gradFill() {
3771
+ return this.gradFill;
3772
+ }
3773
+ /** @internal */
3774
+ _patternFill() {
3775
+ return this.patternFill;
3776
+ }
3777
+ /** @internal */
3778
+ _meshCache() {
3779
+ return this.meshCache;
3780
+ }
3781
+ /** @internal */
3782
+ _textureCache() {
3783
+ return this.textureCache;
3784
+ }
3785
+ /** @internal */
3786
+ _imageCache() {
3787
+ return this.imageCache;
3788
+ }
3789
+ /** @internal */
3790
+ _gradRamps() {
3791
+ return this.gradRamps;
3792
+ }
3793
+ /** @internal */
3794
+ _groupState() {
3795
+ return this.groupState;
3796
+ }
3797
+ /** @internal */
3798
+ _widthCss() {
3799
+ return this.widthCss;
3800
+ }
3801
+ /** @internal */
3802
+ _heightCss() {
3803
+ return this.heightCss;
3804
+ }
3805
+ /** @internal */
3806
+ _dpr() {
3807
+ return this.dpr;
3808
+ }
3809
+ };
3810
+ function createWhiteTexture(gl) {
3811
+ const tex = gl.createTexture();
3812
+ if (!tex) return null;
3813
+ gl.bindTexture(gl.TEXTURE_2D, tex);
3814
+ gl.texImage2D(
3815
+ gl.TEXTURE_2D,
3816
+ 0,
3817
+ gl.RGBA,
3818
+ 1,
3819
+ 1,
3820
+ 0,
3821
+ gl.RGBA,
3822
+ gl.UNSIGNED_BYTE,
3823
+ new Uint8Array([255, 255, 255, 255])
3824
+ );
3825
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
3826
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
3827
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
3828
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
3829
+ gl.bindTexture(gl.TEXTURE_2D, null);
3830
+ return tex;
3831
+ }
3832
+
3833
+ // src/spriteSheet.ts
3834
+ function frameRect(sheet, index) {
3835
+ const { frameWidth, frameHeight, columns } = sheet;
3836
+ const margin = sheet.margin ?? 0;
3837
+ const spacing = sheet.spacing ?? 0;
3838
+ const col = index % columns;
3839
+ const row = Math.floor(index / columns);
3840
+ return {
3841
+ x: margin + col * (frameWidth + spacing),
3842
+ y: margin + row * (frameHeight + spacing),
3843
+ w: frameWidth,
3844
+ h: frameHeight
3845
+ };
3846
+ }
3847
+ var SLACK_PX = 1;
3848
+ var DEFAULT_MITER_LIMIT = 4;
3849
+ function cullDrawCommands(cmds, transform, rect) {
3850
+ return cullList(cmds, transform, slackBox(rect));
3851
+ }
3852
+ function cullList(cmds, m, box) {
3853
+ let out = null;
3854
+ for (let i = 0; i < cmds.length; i++) {
3855
+ const cmd = cmds[i];
3856
+ const kept = cullOne(cmd, m, box);
3857
+ if (kept === cmd) {
3858
+ out?.push(cmd);
3859
+ continue;
3860
+ }
3861
+ out ??= cmds.slice(0, i);
3862
+ if (kept) out.push(kept);
3863
+ }
3864
+ return out ?? cmds;
3865
+ }
3866
+ function cullOne(cmd, m, box) {
3867
+ switch (cmd.kind) {
3868
+ case "group": {
3869
+ if (cmd.effects && cmd.effects.length > 0) return cmd;
3870
+ const inner = cmd.transform ? mat3.multiply(m, cmd.transform) : m;
3871
+ const children = cullList(cmd.children, inner, box);
3872
+ if (children === cmd.children) return cmd;
3873
+ return children.length === 0 ? null : { ...cmd, children };
3874
+ }
3875
+ case "path":
3876
+ return pathMisses(cmd.path, cmd.stroke, m, box) ? null : cmd;
3877
+ case "image":
3878
+ return quadMisses(cmd.x, cmd.y, cmd.x + cmd.w, cmd.y + cmd.h, m, box) ? null : cmd;
3879
+ case "sprites":
3880
+ return spritesMiss(cmd.sprites, m, box) ? null : cmd;
3881
+ default:
3882
+ return cmd;
3883
+ }
3884
+ }
3885
+ function pathMisses(path, stroke, m, box) {
3886
+ let minX, minY, maxX, maxY;
3887
+ if (path.kind === "rect") {
3888
+ minX = Math.min(path.x, path.x + path.width);
3889
+ maxX = Math.max(path.x, path.x + path.width);
3890
+ minY = Math.min(path.y, path.y + path.height);
3891
+ maxY = Math.max(path.y, path.y + path.height);
3892
+ } else if (path.kind === "polygon") {
3893
+ const c = path.coords;
3894
+ if (c.length < 2) return false;
3895
+ minX = maxX = c[0];
3896
+ minY = maxY = c[1];
3897
+ for (let i = 2; i + 1 < c.length; i += 2) {
3898
+ const x = c[i], y = c[i + 1];
3899
+ if (x < minX) minX = x;
3900
+ else if (x > maxX) maxX = x;
3901
+ if (y < minY) minY = y;
3902
+ else if (y > maxY) maxY = y;
3903
+ }
3904
+ } else {
3905
+ return false;
3906
+ }
3907
+ const reach = stroke ? strokeReach(stroke, m) : 0;
3908
+ return quadMisses(minX - reach, minY - reach, maxX + reach, maxY + reach, m, box);
3909
+ }
3910
+ function strokeReach(stroke, m) {
3911
+ return strokeReachAt(stroke, mat3.meanScaleOf(m), mat3.minScaleOf(m));
3912
+ }
3913
+ function strokeReachAt(stroke, scale, least = scale) {
3914
+ const at = least > 0 ? least : scale;
3915
+ const base = resolveStrokeWidth(stroke.width ?? 1, typeof stroke.width === "object" ? at : scale);
3916
+ let width = base;
3917
+ if (stroke.vertexWidths) {
3918
+ for (const v of stroke.vertexWidths) if (v > width) width = v;
3919
+ }
3920
+ const spike = Math.max(stroke.miterLimit ?? DEFAULT_MITER_LIMIT, Math.SQRT2);
3921
+ const heads = markerReach(stroke);
3922
+ return Math.max(Math.abs(width) * spike, heads.world, resolveScreenLength({ px: heads.px }, at));
3923
+ }
3924
+ function spritesMiss(sprites, m, box) {
3925
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3926
+ for (let i = 0; i + SPRITE_STRIDE <= sprites.length; i += SPRITE_STRIDE) {
3927
+ const x0 = sprites[i], y0 = sprites[i + 1];
3928
+ const x1 = x0 + sprites[i + 2], y1 = y0 + sprites[i + 3];
3929
+ minX = Math.min(minX, x0, x1);
3930
+ maxX = Math.max(maxX, x0, x1);
3931
+ minY = Math.min(minY, y0, y1);
3932
+ maxY = Math.max(maxY, y0, y1);
3933
+ }
3934
+ if (minX === Infinity) return false;
3935
+ return quadMisses(minX, minY, maxX, maxY, m, box);
3936
+ }
3937
+ function boundsMissRect(bounds, transform, rect) {
3938
+ const x0 = Math.min(bounds.x, bounds.x + bounds.width);
3939
+ const x1 = Math.max(bounds.x, bounds.x + bounds.width);
3940
+ const y0 = Math.min(bounds.y, bounds.y + bounds.height);
3941
+ const y1 = Math.max(bounds.y, bounds.y + bounds.height);
3942
+ return quadMisses(x0, y0, x1, y1, transform, slackBox(rect));
3943
+ }
3944
+ function slackBox(rect) {
3945
+ return {
3946
+ minX: rect.x - SLACK_PX,
3947
+ minY: rect.y - SLACK_PX,
3948
+ maxX: rect.x + rect.width + SLACK_PX,
3949
+ maxY: rect.y + rect.height + SLACK_PX
3950
+ };
3951
+ }
3952
+ function quadMisses(x0, y0, x1, y1, m, box) {
3953
+ const [ax, ay] = mat3.apply(m, x0, y0);
3954
+ const [bx, by] = mat3.apply(m, x1, y0);
3955
+ const [cx, cy] = mat3.apply(m, x1, y1);
3956
+ const [dx, dy] = mat3.apply(m, x0, y1);
3957
+ return Math.max(ax, bx, cx, dx) < box.minX || Math.min(ax, bx, cx, dx) > box.maxX || Math.max(ay, by, cy, dy) < box.minY || Math.min(ay, by, cy, dy) > box.maxY;
3958
+ }
3959
+
3960
+ // src/effects/types.ts
3961
+ function registerEffect(id, frag) {
3962
+ return registerProgram(id, EFFECT_VERT_SRC, frag);
3963
+ }
3964
+
3965
+ // src/effects/builtins.ts
3966
+ var BLUR_FRAG = (
3967
+ /* glsl */
3968
+ `#version 300 es
3969
+ precision highp float;
3970
+ in vec2 v_uv;
3971
+ uniform sampler2D u_source;
3972
+ uniform vec2 u_texel;
3973
+ uniform vec2 u_direction;
3974
+ uniform float u_radius;
3975
+ out vec4 outColor;
3976
+
3977
+ void main() {
3978
+ // Nine taps on a Gaussian, spread across the requested radius. The weights
3979
+ // are a normalized binomial row, so a radius of 0 collapses to a copy.
3980
+ const float W[5] = float[5](0.2270270, 0.1945946, 0.1216216, 0.0540541, 0.0162162);
3981
+ vec2 step = u_direction * u_texel * (u_radius / 4.0);
3982
+ vec4 sum = texture(u_source, v_uv) * W[0];
3983
+ for (int i = 1; i < 5; i++) {
3984
+ vec2 off = step * float(i);
3985
+ sum += texture(u_source, v_uv + off) * W[i];
3986
+ sum += texture(u_source, v_uv - off) * W[i];
3987
+ }
3988
+ outColor = sum;
3989
+ }
3990
+ `
3991
+ );
3992
+ var VIGNETTE_FRAG = (
3993
+ /* glsl */
3994
+ `#version 300 es
3995
+ precision highp float;
3996
+ in vec2 v_uv;
3997
+ uniform sampler2D u_source;
3998
+ uniform float u_amount;
3999
+ uniform float u_feather;
4000
+ out vec4 outColor;
4001
+
4002
+ void main() {
4003
+ vec4 src = texture(u_source, v_uv);
4004
+ float d = distance(v_uv, vec2(0.5)) * 1.41421356;
4005
+ float shade = 1.0 - u_amount * smoothstep(1.0 - u_feather, 1.0, d);
4006
+ // Premultiplied in, premultiplied out: scaling all four channels darkens
4007
+ // toward transparent black, which is what a vignette over a transparent
4008
+ // canvas should do.
4009
+ outColor = src * shade;
4010
+ }
4011
+ `
4012
+ );
4013
+ var BLUR = registerEffect("weasel:blur", BLUR_FRAG);
4014
+ var VIGNETTE = registerEffect("weasel:vignette", VIGNETTE_FRAG);
4015
+ function blur({ radius }) {
4016
+ return [
4017
+ { program: BLUR, uniforms: { u_direction: [1, 0], u_radius: radius } },
4018
+ { program: BLUR, uniforms: { u_direction: [0, 1], u_radius: radius } }
4019
+ ];
4020
+ }
4021
+ function vignette({ amount, feather = 0.6 }) {
4022
+ return [{ program: VIGNETTE, uniforms: { u_amount: amount, u_feather: feather } }];
4023
+ }
4024
+ function textCommandFromRuns(x, y, runs, style, maxWidth, height, verticalAlign, paint, width, scale = 1) {
4025
+ const resolved = resolveTextStyle(style, paint, scale);
4026
+ return {
4027
+ kind: "text",
4028
+ x,
4029
+ y,
4030
+ runs: resolveRuns(runs, resolved, scale),
4031
+ align: resolved.align,
4032
+ direction: resolved.direction,
4033
+ justify: resolved.align === "justify",
4034
+ maxWidth,
4035
+ style: style ?? {},
4036
+ height,
4037
+ verticalAlign,
4038
+ width
4039
+ };
4040
+ }
4041
+ function textCommand(x, y, text, style, maxWidth, height, verticalAlign, paint, width, scale = 1) {
4042
+ return textCommandFromRuns(
4043
+ x,
4044
+ y,
4045
+ [{ text }],
4046
+ style,
4047
+ maxWidth,
4048
+ height,
4049
+ verticalAlign,
4050
+ paint,
4051
+ width,
4052
+ scale
4053
+ );
4054
+ }
4055
+ function textCommandFromPose(pose, scale = 1) {
4056
+ const { runs, opts } = textPoseLayoutInput(pose, scale);
4057
+ return {
4058
+ kind: "text",
4059
+ x: pose.x,
4060
+ y: pose.y,
4061
+ runs: runsInPoseFrame(runs, pose),
4062
+ align: opts.align,
4063
+ direction: opts.direction,
4064
+ justify: opts.justify,
4065
+ maxWidth: opts.maxWidth,
4066
+ width: opts.alignWidth,
4067
+ style: pose.style ?? {},
4068
+ height: pose.height,
4069
+ verticalAlign: pose.verticalAlign
4070
+ };
4071
+ }
4072
+ function runsInPoseFrame(runs, box) {
4073
+ if (runs.every((r) => isSolid(r.fill) && isSolid(r.stroke?.paint))) return runs;
4074
+ return runs.map((r) => {
4075
+ const fill = r.fill && commandFill(r.fill, box);
4076
+ const paint = r.stroke?.paint && commandFill(r.stroke.paint, box);
4077
+ return { ...r, fill, stroke: r.stroke && { ...r.stroke, paint: paint || void 0 } };
4078
+ });
4079
+ }
4080
+ function isSolid(paint) {
4081
+ return paint == null || (paint.fill ?? "solid") === "solid";
4082
+ }
4083
+
4084
+ export { EFFECT_KIT_UNIFORMS, EFFECT_VERT_SRC, IDENTITY_COLOR_MATRIX, OUTLINE_MIN_SCREEN_PX, SPRITE_STRIDE, ShaderCompileError, WeaselRenderer, blur, boundsMissRect, buildGradientRamp, cullDrawCommands, frameRect, registerEffect, strokeReachAt, textCommand, textCommandFromPose, textCommandFromRuns, vignette };
4085
+ //# sourceMappingURL=index.js.map
4086
+ //# sourceMappingURL=index.js.map