@weasel-js/renderer 1.9.4 → 1.10.1

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 CHANGED
@@ -1,18 +1,18 @@
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';
1
+ export { createTilePattern } from './chunk-43YL7HZV.js';
2
+ import { BATCH_ATTRIBUTE_LOCATIONS, BATCH_TEXTURE_SLOTS, registerProgram, STRAIGHT_RGB_GLSL, BATCH_FILL_ATTRIBUTES, BATCH_FILL_UNIFORMS, BATCH_MATRICES_FRAG_SRC, BATCH_VERT_SRC, GLImageCache, DrawBatch, getProgramSource, commandFill, commandPaint, BATCH_FRAG_SRC, uploadPremultiplied, RAMP_SLOT, withMatrix, cachedMarkerCommands, BATCH_MATRIX_SLOTS, packSlot, WHITE_SLOT, PAINT_MODE_PLAIN, PAINT_MODE_RADIAL, PAINT_MODE_CONIC, getMesh, quantizeStrokeScale, strokeMesh, axisAlignedClipRect, intersectClipRects, getPaintDrawKind, outlineMesh, outlineStrokeMesh, quantizeEmWidth } from './chunk-K7XCWVQH.js';
3
+ export { commandFill, commandPaint, commandStroke, fillInPoseFrame, fillToBoundsFrame, getImageBitmap, getPaintDrawKind, getProgramSource, imageStatus, isPaintKindKnown, isVectorImageSrc, paintDrawKindRegistry, programSourceRegistry, registerPaintDrawKind, registerPaintDrawKindLoader, registerProgram, releaseImage, releaseImageSource, resolveFillPattern, resolvePatternSpec, retryImage, setImageCacheBudget, subscribeImageReady, warmPaintKinds } from './chunk-K7XCWVQH.js';
4
+ import { textureRegistry, getTexture } from './chunk-Q4SIHV7Q.js';
5
+ export { registerTexture, textureRegistry, unregisterTexture } from './chunk-Q4SIHV7Q.js';
6
+ import { DEFAULT_BAKE_BUDGET, markAllFontsNotUploaded, resetBakeBudget, GLYPH_COVERAGE_GLSL, syncColorPageTexture, colorPageTextureId, syncDynamicPageTexture, ensureFontTexture, dynamicPageTextureId, textureCacheKey, GLYPH_MODE_R8, GLYPH_MODE_MSDF, glyphFieldScale } from '@weasel-js/font';
7
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';
8
+ import { resolveGradientStops, sampleResolvedStops, resolveScreenLength, isSolidPaint, resolveColor, strokeHasInk } from '@weasel-js/paint';
9
9
  import { mat3 } from '@weasel-js/geom';
10
10
  export { mat3, viewToMat3 } from '@weasel-js/geom';
11
11
  import { tessellate } from '@weasel-js/geom/tessellate';
12
12
  export { tessellate } from '@weasel-js/geom/tessellate';
13
13
  import { resolveStrokeWidth, markerReach, metricKeepsRects, strokeSpaceOf, resolveMarkerSize } from '@weasel-js/stroke';
14
14
  export { tessellateStroke } from '@weasel-js/stroke';
15
- import { resolveTextStyle, resolveRuns, textPoseLayoutInput, cachedLayoutRuns, verticalAlignOffset, SYNTHETIC_ITALIC_RADIANS } from '@weasel-js/text';
15
+ import { resolveTextStyle, resolveRuns, textPoseLayoutInput, cachedLayoutRuns, cachedPaintRuns, verticalAlignOffset, SYNTHETIC_ITALIC_RADIANS } from '@weasel-js/text';
16
16
 
17
17
  // src/paintResources.ts
18
18
  function createPaintResources() {
@@ -229,7 +229,6 @@ uniform mat4 u_colorMatrix;
229
229
  uniform vec4 u_colorBias;
230
230
  uniform float u_opacity;
231
231
  uniform int u_gradKind;
232
- uniform vec2 u_gradP0;
233
232
  uniform vec2 u_gradDir;
234
233
  uniform float u_gradLen;
235
234
  uniform float u_gradRadius;
@@ -240,12 +239,11 @@ const float PI = 3.14159265358979323846;
240
239
  vec4 shadePaint(vec2 world) {
241
240
  float t;
242
241
  if (u_gradKind == 0) {
243
- vec2 d = world - u_gradP0;
244
- t = dot(d, u_gradDir) / max(u_gradLen, 0.0001);
242
+ t = dot(world, u_gradDir) / max(u_gradLen, 0.0001);
245
243
  } else if (u_gradKind == 1) {
246
- t = length(world - u_gradP0) / max(u_gradRadius, 0.0001);
244
+ t = length(world) / max(u_gradRadius, 0.0001);
247
245
  } else {
248
- float a = atan(world.y - u_gradP0.y, world.x - u_gradP0.x) - u_gradAngle;
246
+ float a = atan(world.y, world.x) - u_gradAngle;
249
247
  t = fract(a / (2.0 * PI));
250
248
  }
251
249
  t = clamp(t, 0.0, 1.0);
@@ -284,7 +282,6 @@ var GRAD_FILL_UNIFORMS = [
284
282
  "u_colorMatrix",
285
283
  "u_colorBias",
286
284
  "u_gradKind",
287
- "u_gradP0",
288
285
  "u_gradDir",
289
286
  "u_gradLen",
290
287
  "u_gradRadius",
@@ -320,10 +317,11 @@ uniform float u_opacity;
320
317
  uniform float u_alpha;
321
318
  uniform mat4 u_colorMatrix;
322
319
  uniform vec4 u_colorBias;
320
+ ${STRAIGHT_RGB_GLSL}
323
321
  vec4 shadePaint(vec2 world) {
324
322
  vec2 uv = world / max(u_tileSize, vec2(0.0001));
325
323
  vec4 texel = texture(u_sampler, uv);
326
- vec4 mapped = clamp(u_colorMatrix * texel + u_colorBias, 0.0, 1.0);
324
+ vec4 mapped = clamp(u_colorMatrix * vec4(straightRgb(texel), texel.a) + u_colorBias, 0.0, 1.0);
327
325
  float a = mapped.a * u_opacity * u_alpha;
328
326
  return vec4(mapped.rgb * a, a);
329
327
  }
@@ -354,6 +352,72 @@ var PATTERN_FILL_UNIFORMS = [
354
352
  "u_colorBias"
355
353
  ];
356
354
  var PATTERN_FILL_ATTRIBUTES = ["a_position"];
355
+
356
+ // src/shaders/builtinPrograms.ts
357
+ var LazyProgram = class {
358
+ constructor(gl, spec) {
359
+ this.gl = gl;
360
+ this.spec = spec;
361
+ }
362
+ gl;
363
+ spec;
364
+ program = null;
365
+ get() {
366
+ if (!this.program) {
367
+ const { vert, frag, uniforms, attributes } = this.spec;
368
+ const program = new ShaderProgram(this.gl, vert, frag);
369
+ program.lookupUniforms(uniforms);
370
+ program.lookupAttributes(attributes);
371
+ this.program = program;
372
+ }
373
+ return this.program;
374
+ }
375
+ /** Delete the program if it was built. It builds again on the next `get`. */
376
+ release() {
377
+ if (this.program) this.gl.deleteProgram(this.program.handle);
378
+ this.program = null;
379
+ }
380
+ /** Forget a program its lost context already took with it. */
381
+ abandon() {
382
+ this.program = null;
383
+ }
384
+ };
385
+ var PATH_FILL_SPEC = {
386
+ vert: VERT_SRC,
387
+ frag: FRAG_SRC,
388
+ uniforms: PATH_FILL_UNIFORMS,
389
+ attributes: PATH_FILL_ATTRIBUTES
390
+ };
391
+ var PATH_FILL_VCOLOR_SPEC = {
392
+ vert: VCOLOR_VERT_SRC,
393
+ frag: VCOLOR_FRAG_SRC,
394
+ uniforms: PATH_FILL_UNIFORMS,
395
+ attributes: PATH_FILL_VCOLOR_ATTRIBUTES
396
+ };
397
+ var GRAD_FILL_SPEC = {
398
+ vert: GRAD_VERT_SRC,
399
+ frag: GRAD_FRAG_SRC,
400
+ uniforms: GRAD_FILL_UNIFORMS,
401
+ attributes: GRAD_FILL_ATTRIBUTES
402
+ };
403
+ var BATCH_FILL_MATRICES_SPEC = {
404
+ vert: BATCH_VERT_SRC,
405
+ frag: BATCH_MATRICES_FRAG_SRC,
406
+ uniforms: BATCH_FILL_UNIFORMS,
407
+ attributes: BATCH_FILL_ATTRIBUTES
408
+ };
409
+ var PATTERN_FILL_SPEC = {
410
+ vert: PATTERN_VERT_SRC,
411
+ frag: PATTERN_FRAG_SRC,
412
+ uniforms: PATTERN_FILL_UNIFORMS,
413
+ attributes: PATTERN_FILL_ATTRIBUTES
414
+ };
415
+ function createBatchFill(gl) {
416
+ const program = new ShaderProgram(gl, BATCH_VERT_SRC, BATCH_FRAG_SRC);
417
+ program.lookupUniforms(BATCH_FILL_UNIFORMS);
418
+ program.lookupAttributes(BATCH_FILL_ATTRIBUTES);
419
+ return program;
420
+ }
357
421
  var L = BATCH_ATTRIBUTE_LOCATIONS;
358
422
  var GLYPH_PAINT_VERT_SRC = (
359
423
  /* glsl */
@@ -510,11 +574,9 @@ var GLMeshCache = class {
510
574
  return handle;
511
575
  }
512
576
  /**
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.
577
+ * Upload a Mesh that the caller knows is single-use. Returns a handle backed
578
+ * by fresh GL resources that the renderer frees at the end of the current
579
+ * frame via `freeTransient()`, so transient meshes never wait on GC.
518
580
  */
519
581
  uploadTransient(mesh) {
520
582
  const { handle, vbo, ibo } = this.upload(mesh);
@@ -628,6 +690,9 @@ var GLMeshCache = class {
628
690
  };
629
691
 
630
692
  // src/cache/GLTextureCache.ts
693
+ function uploadAsHeld(gl, source) {
694
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
695
+ }
631
696
  var GLTextureCache = class {
632
697
  constructor(gl) {
633
698
  this.gl = gl;
@@ -644,8 +709,11 @@ var GLTextureCache = class {
644
709
  /** Uploads the pixels once per id. `wrap` is re-applied whenever it differs
645
710
  * from what the id was last uploaded under: the same registered image can
646
711
  * 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. */
712
+ * the next. */
648
713
  upload(id, source, wrap = "clamp") {
714
+ return this.ensure(id, source, wrap, uploadAsHeld);
715
+ }
716
+ ensure(id, source, wrap, send) {
649
717
  const cached = this.map.get(id);
650
718
  if (cached) {
651
719
  if (this.wraps.get(id) !== wrap) this.applyWrap(id, cached, wrap);
@@ -656,7 +724,7 @@ var GLTextureCache = class {
656
724
  if (!tex) throw new Error(`GLTextureCache: createTexture failed for id="${id}"`);
657
725
  const wrapMode = wrap === "repeat" ? gl.REPEAT : gl.CLAMP_TO_EDGE;
658
726
  gl.bindTexture(gl.TEXTURE_2D, tex);
659
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
727
+ send(gl, source);
660
728
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
661
729
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
662
730
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapMode);
@@ -667,17 +735,18 @@ var GLTextureCache = class {
667
735
  return id;
668
736
  }
669
737
  /** `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. */
738
+ * under the same id: a changed source sends its pixels again. A registered
739
+ * texture is an image, so its texels are premultiplied. */
671
740
  uploadRegistered(id, source, wrap = "clamp") {
672
741
  const cached = this.map.get(id);
673
742
  if (cached && this.registered.get(id) !== source) {
674
743
  const gl = this.gl;
675
744
  gl.bindTexture(gl.TEXTURE_2D, cached);
676
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
745
+ uploadPremultiplied(gl, source);
677
746
  gl.bindTexture(gl.TEXTURE_2D, null);
678
747
  }
679
748
  this.registered.set(id, source);
680
- return this.upload(id, source, wrap);
749
+ return this.ensure(id, source, wrap, uploadPremultiplied);
681
750
  }
682
751
  /**
683
752
  * Delete the texture of every registry-backed id the registry has dropped.
@@ -736,6 +805,35 @@ var GLTextureCache = class {
736
805
  gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
737
806
  gl.bindTexture(gl.TEXTURE_2D, null);
738
807
  }
808
+ /** Create a texture from premultiplied RGBA bytes that are a color: a page
809
+ * of color glyphs. Mipmapped, as a font atlas must never be, because one is
810
+ * drawn well below the size it was baked at. No-op if `id` already exists. */
811
+ uploadRGBA(id, width, height, data) {
812
+ if (this.map.has(id)) return;
813
+ const gl = this.gl;
814
+ const tex = gl.createTexture();
815
+ if (!tex) throw new Error(`GLTextureCache: createTexture failed for id="${id}"`);
816
+ gl.bindTexture(gl.TEXTURE_2D, tex);
817
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
818
+ gl.generateMipmap(gl.TEXTURE_2D);
819
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
820
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
821
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
822
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
823
+ gl.bindTexture(gl.TEXTURE_2D, null);
824
+ this.map.set(id, tex);
825
+ this.wraps.set(id, "clamp");
826
+ }
827
+ /** Patch a rect of an existing RGBA texture with tightly-packed w×h texels. */
828
+ subImageRGBA(id, x, y, w, h, data) {
829
+ const tex = this.map.get(id);
830
+ if (!tex) throw new Error(`GLTextureCache: texture "${id}" not uploaded`);
831
+ const gl = this.gl;
832
+ gl.bindTexture(gl.TEXTURE_2D, tex);
833
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, data);
834
+ gl.generateMipmap(gl.TEXTURE_2D);
835
+ gl.bindTexture(gl.TEXTURE_2D, null);
836
+ }
739
837
  bind(id, unit) {
740
838
  const tex = this.map.get(id);
741
839
  if (!tex) throw new Error(`GLTextureCache: texture "${id}" not uploaded`);
@@ -744,10 +842,8 @@ var GLTextureCache = class {
744
842
  gl.bindTexture(gl.TEXTURE_2D, tex);
745
843
  }
746
844
  /**
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.
845
+ * Delete every uploaded GL texture and clear the map. The cache is
846
+ * refillable afterward.
751
847
  */
752
848
  free() {
753
849
  const gl = this.gl;
@@ -858,10 +954,8 @@ var GradientRampAtlas = class {
858
954
  this.cacheHits = 0;
859
955
  }
860
956
  /**
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).
957
+ * Delete the atlas texture and forget every row in it. The CPU mirror goes
958
+ * with it, so the atlas is refillable afterward. Stats are left as-is.
865
959
  */
866
960
  free() {
867
961
  if (this.texture) this.gl.deleteTexture(this.texture);
@@ -999,16 +1093,13 @@ var GroupState = class {
999
1093
  this.colorMatrixStack.push(nextCM);
1000
1094
  }
1001
1095
  /**
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.
1096
+ * Push a frame whose children paint into a surface of their own. The
1097
+ * transform still accumulates. Alpha and color do not: applying them on the
1098
+ * way in would fade the pixels an effect is about to read, then fade them
1099
+ * again on the way out.
1008
1100
  *
1009
1101
  * 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.
1102
+ * would have left on the stack.
1012
1103
  */
1013
1104
  pushIsolated(frame) {
1014
1105
  this.push(frame);
@@ -1085,6 +1176,49 @@ function compositeProgramId() {
1085
1176
  return COMPOSITE_PROGRAM_ID;
1086
1177
  }
1087
1178
 
1179
+ // src/effects/compositeOp.ts
1180
+ var OPERATORS = {
1181
+ "source-over": { source: "one", destination: "one-minus-source-alpha" },
1182
+ "source-in": { source: "destination-alpha", destination: "zero" },
1183
+ "source-out": { source: "one-minus-destination-alpha", destination: "zero" },
1184
+ "source-atop": { source: "destination-alpha", destination: "one-minus-source-alpha" },
1185
+ "destination-over": { source: "one-minus-destination-alpha", destination: "one" },
1186
+ "destination-in": { source: "zero", destination: "source-alpha" },
1187
+ "destination-out": { source: "zero", destination: "one-minus-source-alpha" },
1188
+ "destination-atop": { source: "one-minus-destination-alpha", destination: "source-alpha" },
1189
+ xor: { source: "one-minus-destination-alpha", destination: "one-minus-source-alpha" },
1190
+ lighter: { source: "one", destination: "one" },
1191
+ copy: { source: "one", destination: "zero" }
1192
+ };
1193
+ function blendFactorsOf(composite) {
1194
+ return typeof composite === "string" ? OPERATORS[composite] : composite;
1195
+ }
1196
+ function isOwnComposite(composite) {
1197
+ if (composite === void 0) return false;
1198
+ const { source, destination } = blendFactorsOf(composite);
1199
+ return source !== "one" || destination !== "one-minus-source-alpha";
1200
+ }
1201
+ function glFactor(gl, factor) {
1202
+ switch (factor) {
1203
+ case "zero":
1204
+ return gl.ZERO;
1205
+ case "one":
1206
+ return gl.ONE;
1207
+ case "source-alpha":
1208
+ return gl.SRC_ALPHA;
1209
+ case "one-minus-source-alpha":
1210
+ return gl.ONE_MINUS_SRC_ALPHA;
1211
+ case "destination-alpha":
1212
+ return gl.DST_ALPHA;
1213
+ case "one-minus-destination-alpha":
1214
+ return gl.ONE_MINUS_DST_ALPHA;
1215
+ }
1216
+ }
1217
+ function applyComposite(gl, composite) {
1218
+ const { source, destination } = blendFactorsOf(composite);
1219
+ gl.blendFunc(glFactor(gl, source), glFactor(gl, destination));
1220
+ }
1221
+
1088
1222
  // src/draw/drawTriangles.ts
1089
1223
  function drawTriangles(ctx, count, type) {
1090
1224
  ctx.gl.drawElements(ctx.gl.TRIANGLES, count, type, 0);
@@ -1128,14 +1262,17 @@ function endParityMask(ctx) {
1128
1262
 
1129
1263
  // src/draw/customShader.ts
1130
1264
  var warnedUniforms = /* @__PURE__ */ new Set();
1131
- function warnOnceUniform(programId, name) {
1132
- const key = `${programId}:${name}`;
1265
+ function warnOnce(key, message) {
1133
1266
  if (warnedUniforms.has(key)) return;
1134
1267
  warnedUniforms.add(key);
1135
1268
  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
- }
1269
+ if (isDev) console.warn(message);
1270
+ }
1271
+ function warnOnceUniform(programId, name) {
1272
+ warnOnce(
1273
+ `${programId}:${name}`,
1274
+ `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).`
1275
+ );
1139
1276
  }
1140
1277
  function setUniform(gl, loc, value, textureCache, nextTexUnit) {
1141
1278
  if (value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Float32Array) && "id" in value && typeof value.id === "string") {
@@ -1259,10 +1396,11 @@ function setUniforms(gl, program, programId, uniforms, textureCache, nextTexUnit
1259
1396
  }
1260
1397
  function drawShader(ctx, cmd) {
1261
1398
  const { gl, programRegistry, quadVbo, quadIbo, textureCache } = ctx;
1262
- const program = programRegistry.get(cmd.program.id);
1399
+ const program = ctx.ensureProgram?.(cmd.program.id) ?? programRegistry.get(cmd.program.id);
1263
1400
  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().`
1401
+ warnOnce(
1402
+ `${cmd.program.id}:(program)`,
1403
+ `weasel drawShader: program "${cmd.program.id}" has no compiled program on this renderer. Register its source with the module-level registerProgram() first; a source that failed to compile is reported above.`
1266
1404
  );
1267
1405
  return;
1268
1406
  }
@@ -1478,6 +1616,83 @@ function meshHandle(ctx, mesh) {
1478
1616
  function fillMeshHandle(ctx, path) {
1479
1617
  return meshHandle(ctx, fillMesh(ctx, path));
1480
1618
  }
1619
+
1620
+ // src/draw/foldColor.ts
1621
+ function isIdentityColorMatrix(cm) {
1622
+ return cm === IDENTITY_COLOR_MATRIX || sameValues(IDENTITY_COLOR_MATRIX, cm);
1623
+ }
1624
+ var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
1625
+ function foldColor(ctx, r, g, b, a) {
1626
+ const cm = ctx.state.colorMatrix;
1627
+ const alpha = ctx.state.alpha;
1628
+ if (isIdentityColorMatrix(cm)) return [r, g, b, a * alpha];
1629
+ return [
1630
+ clamp01(cm[0] * r + cm[1] * g + cm[2] * b + cm[3] * a + cm[4]),
1631
+ clamp01(cm[5] * r + cm[6] * g + cm[7] * b + cm[8] * a + cm[9]),
1632
+ clamp01(cm[10] * r + cm[11] * g + cm[12] * b + cm[13] * a + cm[14]),
1633
+ clamp01(cm[15] * r + cm[16] * g + cm[17] * b + cm[18] * a + cm[19]) * alpha
1634
+ ];
1635
+ }
1636
+ var NO_TINT = [1, 1, 1, 1];
1637
+ var DIAGONAL = [0, 6, 12, 18];
1638
+ function textureTint(ctx) {
1639
+ const cm = ctx.state.colorMatrix;
1640
+ const alpha = ctx.state.alpha;
1641
+ if (isIdentityColorMatrix(cm)) return alpha === 1 ? NO_TINT : [1, 1, 1, alpha];
1642
+ for (let i = 0; i < 20; i++) {
1643
+ const v = cm[i];
1644
+ if (DIAGONAL.includes(i) ? v < 0 || v > 1 : v !== 0) return null;
1645
+ }
1646
+ return [cm[0], cm[6], cm[12], cm[18] * alpha];
1647
+ }
1648
+
1649
+ // src/draw/batchMatrices.ts
1650
+ function matrixIndex(matrices, colorMatrix, alpha) {
1651
+ for (let i = 0; i < matrices.length; i++) {
1652
+ const m = matrices[i];
1653
+ if (m.alpha === alpha && (m.colorMatrix === colorMatrix || sameValues(m.colorMatrix, colorMatrix))) return i;
1654
+ }
1655
+ return matrices.length < (BATCH_MATRIX_SLOTS ) ? matrices.length : -1;
1656
+ }
1657
+ var MATRICES = new Float32Array(16 * BATCH_MATRIX_SLOTS);
1658
+ var BIASES = new Float32Array(4 * BATCH_MATRIX_SLOTS);
1659
+ var ALPHAS = new Float32Array(BATCH_MATRIX_SLOTS);
1660
+ var UPLOADED = /* @__PURE__ */ new WeakMap();
1661
+ function holds(held, matrices) {
1662
+ if (held.length < matrices.length) return false;
1663
+ for (let i = 0; i < matrices.length; i++) {
1664
+ const a = held[i];
1665
+ const b = matrices[i];
1666
+ if (a.alpha !== b.alpha) return false;
1667
+ if (a.colorMatrix !== b.colorMatrix && !sameValues(a.colorMatrix, b.colorMatrix)) return false;
1668
+ }
1669
+ return true;
1670
+ }
1671
+ function uploadBatchMatrices(ctx, prog, matrices) {
1672
+ let byProgram = UPLOADED.get(ctx);
1673
+ if (!byProgram) {
1674
+ byProgram = /* @__PURE__ */ new WeakMap();
1675
+ UPLOADED.set(ctx, byProgram);
1676
+ }
1677
+ const held = byProgram.get(prog);
1678
+ if (held !== void 0 && holds(held, matrices)) return;
1679
+ const n = matrices.length;
1680
+ for (let i = 0; i < n; i++) {
1681
+ const cm = matrices[i].colorMatrix;
1682
+ for (let row = 0; row < 4; row++) {
1683
+ for (let col = 0; col < 4; col++) MATRICES[16 * i + col * 4 + row] = cm[row * 5 + col];
1684
+ BIASES[4 * i + row] = cm[row * 5 + 4];
1685
+ }
1686
+ ALPHAS[i] = matrices[i].alpha;
1687
+ }
1688
+ const gl = ctx.gl;
1689
+ gl.uniformMatrix4fv(prog.uniform("u_colorMatrix"), false, MATRICES.subarray(0, 16 * n));
1690
+ gl.uniform4fv(prog.uniform("u_colorBias"), BIASES.subarray(0, 4 * n));
1691
+ gl.uniform1fv(prog.uniform("u_alpha"), ALPHAS.subarray(0, n));
1692
+ byProgram.set(prog, matrices.slice());
1693
+ }
1694
+
1695
+ // src/draw/flushBatch.ts
1481
1696
  var BATCH_MODEL = mat3.identity();
1482
1697
  var SAMPLER_UNITS = new Int32Array(
1483
1698
  Array.from({ length: BATCH_TEXTURE_SLOTS }, (_, i) => i)
@@ -1491,14 +1706,12 @@ function flushBatch(ctx) {
1491
1706
  return;
1492
1707
  }
1493
1708
  const gl = ctx.gl;
1494
- const prog = ctx.batchFill;
1709
+ const prog = staged.matrices.length > 1 ? ctx.batchFillMatrices ?? ctx.batchFill : ctx.batchFill;
1495
1710
  gl.useProgram(prog.handle);
1496
1711
  const indexCount = batch.uploadAndBind();
1497
1712
  setProjAndModel(ctx, prog, BATCH_MODEL);
1498
1713
  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);
1714
+ uploadBatchMatrices(ctx, prog, staged.matrices);
1502
1715
  gl.activeTexture(gl.TEXTURE0);
1503
1716
  gl.bindTexture(gl.TEXTURE_2D, ctx.whiteTexture);
1504
1717
  for (let i = 0; i < staged.textures.length; i++) {
@@ -1509,16 +1722,13 @@ function flushBatch(ctx) {
1509
1722
  FIELD_SCALES[2 * (i + 1) + 1] = entry.fieldScale[1];
1510
1723
  continue;
1511
1724
  }
1512
- if (entry.kind === "ramps") {
1513
- ctx.gradRamps.bind(i + 1);
1514
- continue;
1515
- }
1516
1725
  ctx.imageCache.bind(entry.image, i + 1);
1517
1726
  ctx.imageCache.setMagFilter(
1518
1727
  entry.image,
1519
1728
  entry.sampling === "nearest" ? gl.NEAREST : gl.LINEAR
1520
1729
  );
1521
1730
  }
1731
+ if (staged.ramps) ctx.gradRamps.bind(RAMP_SLOT);
1522
1732
  gl.uniform1iv(prog.uniform("u_samplers"), SAMPLER_UNITS);
1523
1733
  gl.uniform2fv(prog.uniform("u_fieldScale"), FIELD_SCALES);
1524
1734
  applyClipTest(ctx, staged.clipDepth);
@@ -1693,9 +1903,46 @@ function isPxMarker(ref) {
1693
1903
  function resolvedMarker(ref, scale) {
1694
1904
  return { ...ref, size: resolveMarkerSize(ref, 0, quantizeStrokeScale(ref.size.px, scale)) };
1695
1905
  }
1906
+
1907
+ // src/draw/batchRun.ts
1908
+ function slotForBitmap(staged, image, sampling) {
1909
+ for (let i = 0; i < staged.textures.length; i++) {
1910
+ const entry = staged.textures[i];
1911
+ if (entry.kind !== "bitmap" || entry.image !== image) continue;
1912
+ return entry.sampling === sampling ? i + 1 : -1;
1913
+ }
1914
+ return nextFreeSlot(staged);
1915
+ }
1916
+ function slotForRamps(staged) {
1917
+ if (staged.ramps) return RAMP_SLOT;
1918
+ return nextFreeSlot(staged) < 0 ? -1 : RAMP_SLOT;
1919
+ }
1920
+ function slotForAtlas(staged, id) {
1921
+ for (let i = 0; i < staged.textures.length; i++) {
1922
+ const entry = staged.textures[i];
1923
+ if (entry.kind === "atlas" && entry.id === id) return i + 1;
1924
+ }
1925
+ return nextFreeSlot(staged);
1926
+ }
1927
+ function nextFreeSlot(staged) {
1928
+ const taken = staged.textures.length + (staged.ramps ? 1 : 0);
1929
+ return taken + 1 < BATCH_TEXTURE_SLOTS ? staged.textures.length + 1 : -1;
1930
+ }
1931
+ function runTakes(ctx, staged, colorMatrix, alpha) {
1932
+ return staged.clipDepth === runClipDepth(ctx) && matrixIndex(staged.matrices, colorMatrix, alpha) >= 0;
1933
+ }
1934
+ function openRun(ctx) {
1935
+ ctx.batchState ??= { clipDepth: runClipDepth(ctx), matrices: [], textures: [], ramps: false };
1936
+ return ctx.batchState;
1937
+ }
1938
+ function takeMatrix(staged, colorMatrix, alpha) {
1939
+ const index = matrixIndex(staged.matrices, colorMatrix, alpha);
1940
+ if (index === staged.matrices.length) staged.matrices.push({ colorMatrix, alpha });
1941
+ return index;
1942
+ }
1696
1943
  function batchPaint(ctx, fill, hasVColors) {
1697
1944
  if (hasVColors) return void 0;
1698
- const kind = fill.fill ?? "solid";
1945
+ const kind = fill.kind ?? "solid";
1699
1946
  if (kind === "solid") {
1700
1947
  const solid = fill;
1701
1948
  return { kind: "solid", color: solid.color, opacity: solid.opacity };
@@ -1706,8 +1953,8 @@ function batchPaint(ctx, fill, hasVColors) {
1706
1953
  const grad = fill;
1707
1954
  const g = gradientMap(ctx, grad.units);
1708
1955
  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));
1956
+ if (grad.kind === "linear-gradient") return ramp(grad, PAINT_MODE_PLAIN, linearRampUV(g, grad));
1957
+ if (grad.kind === "radial-gradient") return ramp(grad, PAINT_MODE_RADIAL, radialRampUV(g, grad));
1711
1958
  return ramp(grad, PAINT_MODE_CONIC, conicRampUV(g, grad));
1712
1959
  }
1713
1960
  function ramp(fill, mode, uv) {
@@ -1780,41 +2027,6 @@ function spaceToDevice(ctx, units) {
1780
2027
  }
1781
2028
 
1782
2029
  // 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
2030
  var MAX_BATCHED_MESH_VERTICES = 96;
1819
2031
  var MAX_BATCHED_MESH_VERTICES_IN_RUN = 1024;
1820
2032
  var meshBatching = {
@@ -1827,27 +2039,26 @@ function canBatchMesh(ctx, mesh) {
1827
2039
  return mesh.vertices.length >> 1 <= cap;
1828
2040
  }
1829
2041
  function stageSolid(ctx, vertices) {
1830
- if (ctx.batchState !== void 0 && !stagedStateIsLive(ctx, ctx.batchState)) flushBatch(ctx);
2042
+ const colorMatrix = IDENTITY_COLOR_MATRIX ;
2043
+ const alpha = 1 ;
2044
+ const open = ctx.batchState;
2045
+ if (open !== void 0 && !runTakes(ctx, open, colorMatrix, alpha)) flushBatch(ctx);
1831
2046
  if (ctx.drawBatch.wouldOverflow(vertices)) flushBatch(ctx);
1832
- return openRun(ctx);
2047
+ const matrix = takeMatrix(openRun(ctx), colorMatrix, alpha);
2048
+ return packSlot(withMatrix(WHITE_SLOT, matrix), PAINT_MODE_PLAIN);
1833
2049
  }
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;
2050
+ var UNTINTED = [1, 1, 1, 1];
2051
+ function texturedUnder(ctx, alphaInEntry) {
2052
+ const tint = textureTint(ctx);
2053
+ if (tint !== null) return { colorMatrix: IDENTITY_COLOR_MATRIX, alpha: 1, tint, slot: 0 };
2054
+ const colorMatrix = ctx.state.colorMatrix;
2055
+ const alpha = ctx.state.alpha;
2056
+ return alphaInEntry ? { colorMatrix, alpha, tint: UNTINTED, slot: 0 } : { colorMatrix, alpha: 1, tint: [1, 1, 1, alpha], slot: 0 };
1848
2057
  }
1849
2058
  function stageImage(ctx, image, sampling) {
1850
- if (ctx.batchState !== void 0 && !stagedStateIsLive(ctx, ctx.batchState, image, sampling)) {
2059
+ const under = texturedUnder(ctx, false);
2060
+ const open = ctx.batchState;
2061
+ if (open !== void 0 && (!runTakes(ctx, open, under.colorMatrix, under.alpha) || slotForBitmap(open, image, sampling) < 0)) {
1851
2062
  flushBatch(ctx);
1852
2063
  }
1853
2064
  if (ctx.drawBatch.wouldOverflow(4)) flushBatch(ctx);
@@ -1857,46 +2068,49 @@ function stageImage(ctx, image, sampling) {
1857
2068
  staged.textures.push({ kind: "bitmap", image, sampling });
1858
2069
  slot = staged.textures.length;
1859
2070
  }
1860
- return { staged, slot };
2071
+ under.slot = withMatrix(slot, takeMatrix(staged, under.colorMatrix, under.alpha));
2072
+ return under;
1861
2073
  }
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)) {
2074
+ function stageGlyphs(ctx, atlasId, fieldScale) {
2075
+ const colorMatrix = IDENTITY_COLOR_MATRIX ;
2076
+ const alpha = 1 ;
2077
+ const open = ctx.batchState;
2078
+ if (open !== void 0 && (!runTakes(ctx, open, colorMatrix, alpha) || slotForAtlas(open, atlasId) < 0)) {
1864
2079
  flushBatch(ctx);
1865
2080
  }
1866
2081
  if (ctx.drawBatch.wouldOverflow(4)) flushBatch(ctx);
1867
2082
  const staged = openRun(ctx);
1868
- staged.synthBold = synthBold;
1869
2083
  let slot = slotForAtlas(staged, atlasId);
1870
2084
  if (slot > staged.textures.length) {
1871
2085
  staged.textures.push({ kind: "atlas", id: atlasId, fieldScale });
1872
2086
  slot = staged.textures.length;
1873
2087
  }
1874
- return { staged, slot };
2088
+ return withMatrix(slot, takeMatrix(staged, colorMatrix, alpha));
1875
2089
  }
1876
2090
  function stageRamps(ctx, stops, space, vertices) {
1877
2091
  if (ctx.gradRamps.wouldReshape(stops, space)) flushBatch(ctx);
1878
- if (ctx.batchState !== void 0 && (!stagedStateIsLive(ctx, ctx.batchState) || slotForRamps(ctx.batchState) < 0)) {
2092
+ const under = texturedUnder(ctx, true);
2093
+ const open = ctx.batchState;
2094
+ if (open !== void 0 && (!runTakes(ctx, open, under.colorMatrix, under.alpha) || slotForRamps(open) < 0)) {
1879
2095
  flushBatch(ctx);
1880
2096
  }
1881
2097
  if (ctx.drawBatch.wouldOverflow(vertices)) flushBatch(ctx);
1882
2098
  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);
2099
+ const slot = slotForRamps(staged);
2100
+ staged.ramps = true;
2101
+ return {
2102
+ slot: withMatrix(slot, takeMatrix(staged, under.colorMatrix, under.alpha)),
2103
+ tint: under.tint,
2104
+ rowV: ctx.gradRamps.rowV(ctx.gradRamps.upload(stops, space))
2105
+ };
1892
2106
  }
1893
- function stagedColor(ctx, staged, paint) {
2107
+ function stagedColor(ctx, paint) {
1894
2108
  const [r, g, b, a] = resolveColor(paint.color);
1895
- return [r, g, b, a * stagedAlpha(ctx, staged, paint.opacity)];
2109
+ return foldColor(ctx, r, g, b, a * (paint.opacity ?? 1));
1896
2110
  }
1897
2111
  function pushRect(ctx, rect, fill) {
1898
- const staged = stageSolid(ctx, 4);
1899
- const [r, g, b, a] = stagedColor(ctx, staged, fill);
2112
+ const slot = stageSolid(ctx, 4);
2113
+ const [r, g, b, a] = stagedColor(ctx, fill);
1900
2114
  if (!ctx.drawBatch.pushRect(
1901
2115
  rect.x,
1902
2116
  rect.y,
@@ -1906,18 +2120,19 @@ function pushRect(ctx, rect, fill) {
1906
2120
  r,
1907
2121
  g,
1908
2122
  b,
1909
- a
2123
+ a,
2124
+ slot
1910
2125
  )) retryUncut(ctx, rect, fill, pushRect);
1911
2126
  }
1912
2127
  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)) {
2128
+ const slot = stageSolid(ctx, mesh.vertices.length >> 1);
2129
+ const [r, g, b, a] = stagedColor(ctx, paint);
2130
+ if (!ctx.drawBatch.pushMesh(mesh, ctx.state.transform, r, g, b, a, slot)) {
1916
2131
  retryUncut(ctx, mesh, paint, pushMesh);
1917
2132
  }
1918
2133
  }
1919
2134
  function pushRampRect(ctx, rect, paint) {
1920
- const { staged, slot, rowV } = stageRamps(ctx, paint.stops, paint.space, 4);
2135
+ const { slot, tint, rowV } = stageRamps(ctx, paint.stops, paint.space, 4);
1921
2136
  const { uv, post } = rampVertices(paint, rowV);
1922
2137
  if (!ctx.drawBatch.pushGradientRect(
1923
2138
  rect.x,
@@ -1929,14 +2144,14 @@ function pushRampRect(ctx, rect, paint) {
1929
2144
  post,
1930
2145
  slot,
1931
2146
  paint.mode,
1932
- 1,
1933
- 1,
1934
- 1,
1935
- stagedAlpha(ctx, staged, paint.opacity)
2147
+ tint[0],
2148
+ tint[1],
2149
+ tint[2],
2150
+ (paint.opacity ?? 1) * tint[3]
1936
2151
  )) retryUncut(ctx, rect, paint, pushRampRect);
1937
2152
  }
1938
2153
  function pushRampMesh(ctx, mesh, paint) {
1939
- const { staged, slot, rowV } = stageRamps(ctx, paint.stops, paint.space, mesh.vertices.length >> 1);
2154
+ const { slot, tint, rowV } = stageRamps(ctx, paint.stops, paint.space, mesh.vertices.length >> 1);
1940
2155
  const { uv, post } = rampVertices(paint, rowV);
1941
2156
  if (!ctx.drawBatch.pushGradientMesh(
1942
2157
  mesh,
@@ -1945,10 +2160,10 @@ function pushRampMesh(ctx, mesh, paint) {
1945
2160
  post,
1946
2161
  slot,
1947
2162
  paint.mode,
1948
- 1,
1949
- 1,
1950
- 1,
1951
- stagedAlpha(ctx, staged, paint.opacity)
2163
+ tint[0],
2164
+ tint[1],
2165
+ tint[2],
2166
+ (paint.opacity ?? 1) * tint[3]
1952
2167
  )) retryUncut(ctx, mesh, paint, pushRampMesh);
1953
2168
  }
1954
2169
  function tryStageFill(ctx, mesh, paint) {
@@ -1960,6 +2175,23 @@ function tryStageFill(ctx, mesh, paint) {
1960
2175
  flushForOwnDraw(ctx);
1961
2176
  return false;
1962
2177
  }
2178
+ var NO_FIELD = [0, 0];
2179
+ function stageColorGlyphs(ctx, atlasId) {
2180
+ const under = texturedUnder(ctx, false);
2181
+ const open = ctx.batchState;
2182
+ if (open !== void 0 && (!runTakes(ctx, open, under.colorMatrix, under.alpha) || slotForAtlas(open, atlasId) < 0)) {
2183
+ flushBatch(ctx);
2184
+ }
2185
+ if (ctx.drawBatch.wouldOverflow(4)) flushBatch(ctx);
2186
+ const staged = openRun(ctx);
2187
+ let slot = slotForAtlas(staged, atlasId);
2188
+ if (slot > staged.textures.length) {
2189
+ staged.textures.push({ kind: "atlas", id: atlasId, fieldScale: NO_FIELD });
2190
+ slot = staged.textures.length;
2191
+ }
2192
+ under.slot = withMatrix(slot, takeMatrix(staged, under.colorMatrix, under.alpha));
2193
+ return under;
2194
+ }
1963
2195
  function patternTileSpace(spaceInverse, origin, transform) {
1964
2196
  const [a, b, c, d] = transform ?? [1, 0, 0, 1];
1965
2197
  const placement = mat3.fromAffine([a, b, c, d, origin.x, origin.y]);
@@ -2004,7 +2236,7 @@ function unownedPaintResources(gl) {
2004
2236
  return made;
2005
2237
  }
2006
2238
  function bindPattern(ctx, prog, fill, model) {
2007
- const tex = fill.pattern;
2239
+ const tex = fill.texture;
2008
2240
  const entry = getTexture(tex.id);
2009
2241
  if (!entry) {
2010
2242
  const isDev = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
@@ -2020,8 +2252,10 @@ function bindPattern(ctx, prog, fill, model) {
2020
2252
  gl.useProgram(prog.handle);
2021
2253
  setProjAndModel(ctx, prog, model);
2022
2254
  setColorMatrixUniforms(ctx, prog);
2023
- gl.uniformMatrix3fv(prog.uniform("u_worldInv"), false, mat3.toUniform(tileSpace));
2024
2255
  const [tw, th] = textureSize(entry.source);
2256
+ tileSpace[6] -= Math.floor(tileSpace[6] / tw) * tw;
2257
+ tileSpace[7] -= Math.floor(tileSpace[7] / th) * th;
2258
+ gl.uniformMatrix3fv(prog.uniform("u_worldInv"), false, mat3.toUniform(tileSpace));
2025
2259
  gl.uniform2f(prog.uniform("u_tileSize"), tw, th);
2026
2260
  ctx.textureCache.bind(tex.id, 0);
2027
2261
  gl.uniform1i(prog.uniform("u_sampler"), 0);
@@ -2037,37 +2271,36 @@ function textureSize(source) {
2037
2271
  function bindGradient(ctx, prog, fill, model) {
2038
2272
  const inverse = gradientSpaceInverse(ctx, fill.units);
2039
2273
  if (!inverse) return null;
2274
+ const about = fill.kind === "linear-gradient" ? fill.from : fill.center;
2275
+ const aboutStart = mat3.multiply(mat3.translated(mat3.identity(), -about.x, -about.y), inverse);
2040
2276
  const gl = ctx.gl;
2041
2277
  const row = ctx.gradRamps.upload(fill.stops, fill.interpolate ?? "rgb");
2042
2278
  gl.useProgram(prog.handle);
2043
2279
  setProjAndModel(ctx, prog, model);
2044
- gl.uniformMatrix3fv(prog.uniform("u_worldInv"), false, mat3.toUniform(inverse));
2280
+ gl.uniformMatrix3fv(prog.uniform("u_worldInv"), false, mat3.toUniform(aboutStart));
2045
2281
  setColorMatrixUniforms(ctx, prog);
2046
2282
  ctx.gradRamps.bind(0);
2047
2283
  gl.uniform1i(prog.uniform("u_ramp"), 0);
2048
2284
  gl.uniform1f(prog.uniform("u_rampV"), ctx.gradRamps.rowV(row));
2049
2285
  setAlphaUniform(ctx, prog, ctx.state.alpha);
2050
2286
  gl.uniform1f(prog.uniform("u_opacity"), fill.opacity ?? 1);
2051
- if (fill.fill === "linear-gradient") {
2287
+ if (fill.kind === "linear-gradient") {
2052
2288
  gl.uniform1i(prog.uniform("u_gradKind"), 0);
2053
2289
  const dx = fill.to.x - fill.from.x;
2054
2290
  const dy = fill.to.y - fill.from.y;
2055
2291
  const len = Math.hypot(dx, dy) || 1;
2056
- gl.uniform2f(prog.uniform("u_gradP0"), fill.from.x, fill.from.y);
2057
2292
  gl.uniform2f(prog.uniform("u_gradDir"), dx / len, dy / len);
2058
2293
  gl.uniform1f(prog.uniform("u_gradLen"), len);
2059
2294
  gl.uniform1f(prog.uniform("u_gradRadius"), 0);
2060
2295
  gl.uniform1f(prog.uniform("u_gradAngle"), 0);
2061
- } else if (fill.fill === "radial-gradient") {
2296
+ } else if (fill.kind === "radial-gradient") {
2062
2297
  gl.uniform1i(prog.uniform("u_gradKind"), 1);
2063
- gl.uniform2f(prog.uniform("u_gradP0"), fill.center.x, fill.center.y);
2064
2298
  gl.uniform2f(prog.uniform("u_gradDir"), 0, 0);
2065
2299
  gl.uniform1f(prog.uniform("u_gradLen"), 0);
2066
2300
  gl.uniform1f(prog.uniform("u_gradRadius"), fill.radius);
2067
2301
  gl.uniform1f(prog.uniform("u_gradAngle"), 0);
2068
2302
  } else {
2069
2303
  gl.uniform1i(prog.uniform("u_gradKind"), 2);
2070
- gl.uniform2f(prog.uniform("u_gradP0"), fill.center.x, fill.center.y);
2071
2304
  gl.uniform2f(prog.uniform("u_gradDir"), 0, 0);
2072
2305
  gl.uniform1f(prog.uniform("u_gradLen"), 0);
2073
2306
  gl.uniform1f(prog.uniform("u_gradRadius"), 0);
@@ -2078,12 +2311,11 @@ function bindGradient(ctx, prog, fill, model) {
2078
2311
 
2079
2312
  // src/draw/pathPaint.ts
2080
2313
  function drawPath(ctx, cmd) {
2081
- if (cmd.fill) drawPathFill(ctx, cmd);
2314
+ if (cmd.fill?.paint) drawPathFill(ctx, cmd, cmd.fill.paint);
2082
2315
  if (cmd.stroke?.paint) drawPathStroke(ctx, cmd);
2083
2316
  }
2084
- function drawPathFill(ctx, cmd) {
2085
- const fill = cmd.fill;
2086
- const isSolid2 = fill.fill === void 0 || fill.fill === "solid";
2317
+ function drawPathFill(ctx, cmd, fill) {
2318
+ const isSolid2 = fill.kind === void 0 || fill.kind === "solid";
2087
2319
  const hasVColors = !!(cmd.vertexColors && cmd.vertexColors.length > 0);
2088
2320
  const solid = fill;
2089
2321
  const paint = batchPaint(ctx, fill, hasVColors);
@@ -2161,9 +2393,9 @@ function drawPathFillVColor(ctx, cmd, fill, handle) {
2161
2393
  gl.bindVertexArray(null);
2162
2394
  }
2163
2395
  function bindPathFillByKind(ctx, fill) {
2164
- const kind = fill.fill ?? "solid";
2396
+ const kind = fill.kind ?? "solid";
2165
2397
  if (kind === "solid") return bindPathFillSolid(ctx, fill);
2166
- if (kind === "pattern") return bindPathFillPattern(ctx, fill);
2398
+ if (kind === "texture") return bindPathFillPattern(ctx, fill);
2167
2399
  if (kind === "linear-gradient" || kind === "radial-gradient" || kind === "conic-gradient") {
2168
2400
  return bindPathFillGradient(ctx, fill);
2169
2401
  }
@@ -2224,7 +2456,7 @@ function drawPathStroke(ctx, rawCmd) {
2224
2456
  function drawPathStrokeUnclipped(ctx, cmd, spaces) {
2225
2457
  const stroke = cmd.stroke;
2226
2458
  const paint = stroke.paint;
2227
- const isSolid2 = paint.fill === void 0 || paint.fill === "solid";
2459
+ const isSolid2 = paint.kind === void 0 || paint.kind === "solid";
2228
2460
  const solid = paint;
2229
2461
  const mesh = strokeMesh(cmd.path, stroke, ctx.flattenTolerance, spaces);
2230
2462
  if (mesh.indices.length === 0) return;
@@ -2273,7 +2505,7 @@ function doubledVertexWidths(widths) {
2273
2505
  function drawPathStrokeStenciled(ctx, cmd, align, spaces) {
2274
2506
  const stroke = cmd.stroke;
2275
2507
  const paint = stroke.paint;
2276
- const isSolid2 = paint.fill === void 0 || paint.fill === "solid";
2508
+ const isSolid2 = paint.kind === void 0 || paint.kind === "solid";
2277
2509
  const solid = paint;
2278
2510
  const widerStroke = {
2279
2511
  ...stroke,
@@ -2333,10 +2565,9 @@ function outlineGroupMesh(group, dx, dy) {
2333
2565
  }
2334
2566
  function outlineGroupStrokeMesh(group, dx, dy, transform) {
2335
2567
  const stroke = group.stroke;
2336
- if (!stroke) return null;
2568
+ if (!strokeHasInk(stroke)) return null;
2337
2569
  const width = stroke.width ?? 1;
2338
2570
  if (typeof width === "object") {
2339
- if (!(width.px > 0)) return null;
2340
2571
  const shear = shearOf(group);
2341
2572
  const [a, b, c, d] = [transform[0], transform[1], transform[3], transform[4]];
2342
2573
  return mergeGlyphMeshes(group, dx, dy, (glyph) => {
@@ -2355,7 +2586,6 @@ function outlineGroupStrokeMesh(group, dx, dy, transform) {
2355
2586
  );
2356
2587
  });
2357
2588
  }
2358
- if (!(width > 0)) return null;
2359
2589
  return mergeGlyphMeshes(group, dx, dy, (glyph) => glyph.scale > 0 ? outlineStrokeMesh(
2360
2590
  glyph.key,
2361
2591
  glyph.d,
@@ -2395,6 +2625,49 @@ function mergeGlyphMeshes(group, dx, dy, meshFor) {
2395
2625
  }
2396
2626
  return { vertices, indices };
2397
2627
  }
2628
+ function drawColorGlyphs(ctx, group, dx, dy) {
2629
+ const fill = group.fill?.paint;
2630
+ if (fill === void 0 || group.quads.length === 0) return;
2631
+ if (!syncColorPageTexture(ctx.textureCache, group.page)) return;
2632
+ const opacity = isSolidPaint(fill) ? resolveColor(fill.color)[3] : 1;
2633
+ const atlasId = colorPageTextureId(group.page);
2634
+ const batch = ctx.drawBatch;
2635
+ const m = ctx.state.transform;
2636
+ let { slot, tint } = stageColorGlyphs(ctx, atlasId);
2637
+ const restage = () => {
2638
+ ({ slot, tint } = stageColorGlyphs(ctx, atlasId));
2639
+ };
2640
+ for (const q of group.quads) {
2641
+ if (batch.wouldOverflow(4)) {
2642
+ flushBatch(ctx);
2643
+ restage();
2644
+ }
2645
+ let cut;
2646
+ while (!batch.pushQuad(
2647
+ q.x0 + dx,
2648
+ q.y0 + dy,
2649
+ q.x1 - q.x0,
2650
+ q.y1 - q.y0,
2651
+ m,
2652
+ q.u0,
2653
+ q.v0,
2654
+ q.u1,
2655
+ q.v1,
2656
+ opacity * tint[3],
2657
+ slot,
2658
+ tint[0],
2659
+ tint[1],
2660
+ tint[2]
2661
+ )) {
2662
+ cut = enterUncut(ctx);
2663
+ restage();
2664
+ }
2665
+ if (cut !== void 0) {
2666
+ leaveUncut(ctx, cut);
2667
+ restage();
2668
+ }
2669
+ }
2670
+ }
2398
2671
 
2399
2672
  // src/draw/scratchPass.ts
2400
2673
  var UNIT_QUAD = {
@@ -2403,7 +2676,7 @@ var UNIT_QUAD = {
2403
2676
  };
2404
2677
  function renderPaintToScratch(ctx, fill, rect) {
2405
2678
  const scratch = ctx.paintScratch;
2406
- const bind = getPaintDrawKind(fill.fill)?.bind;
2679
+ const bind = getPaintDrawKind(fill.kind)?.bind;
2407
2680
  if (!scratch || !bind) return null;
2408
2681
  const ratio = ctx.deviceWidth && ctx.widthCss > 0 ? ctx.deviceWidth / ctx.widthCss : 1;
2409
2682
  const deviceW = ctx.deviceWidth ?? Math.round(ctx.widthCss);
@@ -2492,21 +2765,22 @@ function drawText(ctx, cmd) {
2492
2765
  align,
2493
2766
  direction,
2494
2767
  tabSize: style.tabSize,
2495
- justify: cmd.justify,
2496
- outlineMinSize
2768
+ justify: cmd.justify
2497
2769
  });
2498
- if (laid.groups.length === 0 && laid.decorations.length === 0) return;
2770
+ const { groups, decorations } = cachedPaintRuns(laid, cmd.runs, { outlineMinSize });
2771
+ if (groups.length === 0 && decorations.length === 0) return;
2499
2772
  const dx = cmd.x;
2500
2773
  const dy = cmd.y + verticalAlignOffset(cmd.verticalAlign, cmd.height, laid.bounds.height);
2501
- for (const group of laid.groups) {
2774
+ for (const group of groups) {
2502
2775
  if (group.source === "outline") drawTextOutlineGroup(ctx, group, dx, dy);
2776
+ else if (group.source === "color") drawColorGlyphs(ctx, group, dx, dy);
2503
2777
  else drawTextGroup(ctx, group, dx, dy);
2504
2778
  }
2505
- drawTextDecorations(ctx, laid.decorations, dx, dy);
2779
+ drawTextDecorations(ctx, decorations, dx, dy);
2506
2780
  }
2507
2781
  function drawTextOutlineGroup(ctx, group, dx, dy) {
2508
- const fill = group.fill;
2509
- if (fill !== null) {
2782
+ const fill = group.fill?.paint;
2783
+ if (fill !== void 0) {
2510
2784
  const mesh = outlineGroupMesh(group, dx, dy);
2511
2785
  if (mesh) drawOutlineMesh(ctx, fill, mesh);
2512
2786
  }
@@ -2523,24 +2797,28 @@ function drawOutlineMesh(ctx, fill, mesh) {
2523
2797
  }
2524
2798
  function drawTextDecorations(ctx, decorations, dx, dy) {
2525
2799
  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));
2800
+ const paint = d.fill.paint;
2801
+ if (paint === void 0) continue;
2802
+ if (glyphPaintKindOf(paint) !== null) {
2803
+ drawOutlineMesh(ctx, paint, ruleMesh(d.x0 + dx, d.y0 + dy, d.x1 + dx, d.y1 + dy));
2528
2804
  continue;
2529
2805
  }
2530
- const [r, g, b, a] = "color" in d.fill ? resolveColor(d.fill.color) : [0, 0, 0, 1];
2806
+ const [r, g, b, a] = isSolidPaint(paint) ? resolveColor(paint.color) : [0, 0, 0, 1];
2531
2807
  let cut;
2532
2808
  for (; ; ) {
2533
- const staged = stageSolid(ctx, 4);
2809
+ const slot = stageSolid(ctx, 4);
2810
+ const rgba = foldColor(ctx, r, g, b, a);
2534
2811
  if (ctx.drawBatch.pushRect(
2535
2812
  d.x0 + dx,
2536
2813
  d.y0 + dy,
2537
2814
  d.x1 - d.x0,
2538
2815
  d.y1 - d.y0,
2539
2816
  ctx.state.transform,
2540
- r,
2541
- g,
2542
- b,
2543
- a * (staged.foldsAlpha ? ctx.state.alpha : 1)
2817
+ rgba[0],
2818
+ rgba[1],
2819
+ rgba[2],
2820
+ rgba[3],
2821
+ slot
2544
2822
  )) break;
2545
2823
  cut = enterUncut(ctx);
2546
2824
  }
@@ -2572,21 +2850,22 @@ function drawTextGroup(ctx, group, dx, dy) {
2572
2850
  if (!fieldScale) return;
2573
2851
  const bold = group.synthetic.bold ? SYNTH_BOLD_AMOUNT : 0;
2574
2852
  const tanItalic = group.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
2575
- const paintKind = glyphPaintKindOf(group.fill);
2853
+ const fill = group.fill?.paint;
2854
+ if (fill === void 0) return;
2855
+ const paintKind = glyphPaintKindOf(fill);
2576
2856
  if (paintKind !== null) {
2577
2857
  drawPaintedGlyphs(ctx, group, dx, dy, paintKind, atlasId, mode, bold, tanItalic, fieldScale);
2578
2858
  return;
2579
2859
  }
2580
- const color = group.fill !== null && "color" in group.fill ? resolveColor(group.fill.color) : [0, 0, 0, 1];
2860
+ const color = isSolidPaint(fill) ? resolveColor(fill.color) : [0, 0, 0, 1];
2581
2861
  const batch = ctx.drawBatch;
2582
2862
  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);
2863
+ let slot = stageGlyphs(ctx, atlasId, fieldScale);
2864
+ const rgba = foldColor(ctx, color[0], color[1], color[2], color[3]);
2585
2865
  for (const q of group.quads) {
2586
2866
  if (batch.wouldOverflow(4)) {
2587
2867
  flushBatch(ctx);
2588
- ({ staged, slot } = stageGlyphs(ctx, atlasId, fieldScale, bold));
2589
- alpha = color[3] * (staged.foldsAlpha ? ctx.state.alpha : 1);
2868
+ slot = stageGlyphs(ctx, atlasId, fieldScale);
2590
2869
  }
2591
2870
  let cut;
2592
2871
  while (!batch.pushGlyph(
@@ -2601,27 +2880,26 @@ function drawTextGroup(ctx, group, dx, dy) {
2601
2880
  q.v0,
2602
2881
  q.u1,
2603
2882
  q.v1,
2604
- color[0],
2605
- color[1],
2606
- color[2],
2607
- alpha,
2883
+ rgba[0],
2884
+ rgba[1],
2885
+ rgba[2],
2886
+ rgba[3],
2608
2887
  slot,
2609
- mode
2888
+ mode,
2889
+ bold
2610
2890
  )) {
2611
2891
  cut = enterUncut(ctx);
2612
- ({ staged, slot } = stageGlyphs(ctx, atlasId, fieldScale, bold));
2613
- alpha = color[3] * (staged.foldsAlpha ? ctx.state.alpha : 1);
2892
+ slot = stageGlyphs(ctx, atlasId, fieldScale);
2614
2893
  }
2615
2894
  if (cut !== void 0) {
2616
2895
  leaveUncut(ctx, cut);
2617
- ({ staged, slot } = stageGlyphs(ctx, atlasId, fieldScale, bold));
2618
- alpha = color[3] * (staged.foldsAlpha ? ctx.state.alpha : 1);
2896
+ slot = stageGlyphs(ctx, atlasId, fieldScale);
2619
2897
  }
2620
2898
  }
2621
2899
  }
2622
2900
  function glyphPaintKindOf(fill) {
2623
- const kind = fill?.fill ?? "solid";
2624
- if (kind === "pattern") return "pattern";
2901
+ const kind = fill.kind ?? "solid";
2902
+ if (kind === "texture") return "pattern";
2625
2903
  if (kind === "linear-gradient" || kind === "radial-gradient" || kind === "conic-gradient") {
2626
2904
  return "gradient";
2627
2905
  }
@@ -2632,7 +2910,7 @@ function drawPaintedGlyphs(ctx, group, dx, dy, kind, atlasId, mode, bold, tanIta
2632
2910
  const prog = ctx.glyphPaintProgram?.(kind);
2633
2911
  if (!prog) return;
2634
2912
  flushForOwnDraw(ctx);
2635
- const fill = group.fill;
2913
+ const fill = group.fill.paint;
2636
2914
  const m = ctx.state.transform;
2637
2915
  let bound;
2638
2916
  if (kind === "pattern") {
@@ -2672,7 +2950,8 @@ function drawPaintedGlyphs(ctx, group, dx, dy, kind, atlasId, mode, bold, tanIta
2672
2950
  1,
2673
2951
  1,
2674
2952
  GLYPH_PAINT_ATLAS_UNIT,
2675
- mode
2953
+ mode,
2954
+ 0
2676
2955
  );
2677
2956
  }
2678
2957
  drawStagedGlyphs(ctx);
@@ -2713,9 +2992,22 @@ function glyphScreenBounds(group, dx, dy, tanItalic, m) {
2713
2992
  var SPRITE_STRIDE = 9;
2714
2993
 
2715
2994
  // src/draw/image.ts
2995
+ var MINIFIED = 2;
2996
+ function minifiedLimits(ctx) {
2997
+ const m = ctx.state.transform;
2998
+ const dpr = ctx.deviceWidth && ctx.widthCss ? ctx.deviceWidth / ctx.widthCss : 1;
2999
+ const k = (MINIFIED * dpr) ** 2;
3000
+ return [k * (m[0] * m[0] + m[1] * m[1]), k * (m[3] * m[3] + m[4] * m[4])];
3001
+ }
3002
+ var isMinified = (limit, sw, sh, dw, dh) => sw * sw >= limit[0] * dw * dw || sh * sh >= limit[1] * dh * dh;
2716
3003
  function drawImage(ctx, cmd) {
2717
3004
  ctx.imageCache.upload(cmd.image, cmd.image);
2718
- const { staged, slot } = stageImage(ctx, cmd.image, cmd.sampling ?? "linear");
3005
+ if (ctx.imageCache.lacksMipmaps(cmd.image)) {
3006
+ const sw = cmd.source?.w ?? cmd.image.width;
3007
+ const sh = cmd.source?.h ?? cmd.image.height;
3008
+ if (isMinified(minifiedLimits(ctx), sw, sh, cmd.w, cmd.h)) ctx.imageCache.buildMipmaps(cmd.image);
3009
+ }
3010
+ const { slot, tint } = stageImage(ctx, cmd.image, cmd.sampling ?? "linear");
2719
3011
  const src = cmd.source;
2720
3012
  let u0 = 0, v0 = 0, u1 = 1, v1 = 1;
2721
3013
  if (src) {
@@ -2745,11 +3037,12 @@ function drawImage(ctx, cmd) {
2745
3037
  v0,
2746
3038
  u1,
2747
3039
  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
3040
+ // Group alpha rides here with the opacity, in the tint's fourth channel.
3041
+ (cmd.opacity ?? 1) * tint[3],
3042
+ slot,
3043
+ tint[0],
3044
+ tint[1],
3045
+ tint[2]
2753
3046
  )) retryUncut(ctx, cmd, void 0, drawImage);
2754
3047
  }
2755
3048
  function drawSprites(ctx, cmd) {
@@ -2757,18 +3050,24 @@ function drawSprites(ctx, cmd) {
2757
3050
  const count = Math.floor(data.length / SPRITE_STRIDE);
2758
3051
  if (count === 0) return;
2759
3052
  ctx.imageCache.upload(cmd.image, cmd.image);
3053
+ if (ctx.imageCache.lacksMipmaps(cmd.image)) {
3054
+ const limit = minifiedLimits(ctx);
3055
+ for (let i = 0; i < count * SPRITE_STRIDE; i += SPRITE_STRIDE) {
3056
+ if (!isMinified(limit, data[i + 6], data[i + 7], data[i + 2], data[i + 3])) continue;
3057
+ ctx.imageCache.buildMipmaps(cmd.image);
3058
+ break;
3059
+ }
3060
+ }
2760
3061
  const sampling = cmd.sampling ?? "linear";
2761
- let { staged, slot } = stageImage(ctx, cmd.image, sampling);
3062
+ let { slot, tint } = stageImage(ctx, cmd.image, sampling);
2762
3063
  const batch = ctx.drawBatch;
2763
3064
  const m = ctx.state.transform;
2764
- let groupAlpha = staged.foldsAlpha ? ctx.state.alpha : 1;
2765
3065
  const tw = cmd.image.width;
2766
3066
  const th = cmd.image.height;
2767
3067
  for (let s = 0, i = 0; s < count; s++, i += SPRITE_STRIDE) {
2768
3068
  if (batch.wouldOverflow(4)) {
2769
3069
  flushBatch(ctx);
2770
- ({ staged, slot } = stageImage(ctx, cmd.image, sampling));
2771
- groupAlpha = staged.foldsAlpha ? ctx.state.alpha : 1;
3070
+ ({ slot, tint } = stageImage(ctx, cmd.image, sampling));
2772
3071
  }
2773
3072
  const sx = data[i + 4], sy = data[i + 5], sw = data[i + 6], sh = data[i + 7];
2774
3073
  let cut;
@@ -2782,17 +3081,18 @@ function drawSprites(ctx, cmd) {
2782
3081
  sy / th,
2783
3082
  (sx + sw) / tw,
2784
3083
  (sy + sh) / th,
2785
- data[i + 8] * groupAlpha,
2786
- slot
3084
+ data[i + 8] * tint[3],
3085
+ slot,
3086
+ tint[0],
3087
+ tint[1],
3088
+ tint[2]
2787
3089
  )) {
2788
3090
  cut = enterUncut(ctx);
2789
- ({ staged, slot } = stageImage(ctx, cmd.image, sampling));
2790
- groupAlpha = staged.foldsAlpha ? ctx.state.alpha : 1;
3091
+ ({ slot, tint } = stageImage(ctx, cmd.image, sampling));
2791
3092
  }
2792
3093
  if (cut !== void 0) {
2793
3094
  leaveUncut(ctx, cut);
2794
- ({ staged, slot } = stageImage(ctx, cmd.image, sampling));
2795
- groupAlpha = staged.foldsAlpha ? ctx.state.alpha : 1;
3095
+ ({ slot, tint } = stageImage(ctx, cmd.image, sampling));
2796
3096
  }
2797
3097
  }
2798
3098
  }
@@ -2842,7 +3142,7 @@ function drawGroupWithEffects(ctx, cmd, targets, width, height) {
2842
3142
  flushBatch(ctx);
2843
3143
  gl.disable(gl.BLEND);
2844
3144
  gl.disable(gl.STENCIL_TEST);
2845
- for (const effect of cmd.effects) {
3145
+ for (const effect of cmd.effects ?? []) {
2846
3146
  const program = ctx.ensureProgram?.(effect.program.id) ?? null;
2847
3147
  if (!program) {
2848
3148
  warnOnceUniform(effect.program.id, "(program)");
@@ -2875,15 +3175,21 @@ function drawGroupWithEffects(ctx, cmd, targets, width, height) {
2875
3175
  const composite = ctx.ensureProgram?.(compositeProgramId()) ?? null;
2876
3176
  if (composite) {
2877
3177
  applyClipTest(ctx);
3178
+ const ownBlend = isOwnComposite(cmd.composite);
3179
+ if (ownBlend) applyComposite(gl, cmd.composite);
2878
3180
  drawFullscreenQuad(ctx, composite, front.texture, width, height, void 0, {
2879
3181
  alpha: composited.alpha,
2880
3182
  colorMatrix: composited.colorMatrix
2881
3183
  });
3184
+ if (ownBlend) applyComposite(gl, "source-over");
2882
3185
  }
2883
3186
  targets.release(front);
2884
3187
  }
3188
+ function ownsSurface(cmd) {
3189
+ return cmd.isolate === true || isOwnComposite(cmd.composite) || cmd.effects !== void 0 && cmd.effects.length > 0;
3190
+ }
2885
3191
  function drawGroup(ctx, cmd) {
2886
- if (cmd.effects && cmd.effects.length > 0 && ctx.effectTargets && ctx.deviceWidth && ctx.deviceHeight) {
3192
+ if (ownsSurface(cmd) && ctx.effectTargets && ctx.deviceWidth && ctx.deviceHeight) {
2887
3193
  if (cmd.clip) {
2888
3194
  ctx.state.push({ transform: cmd.transform });
2889
3195
  try {
@@ -2985,8 +3291,7 @@ var EffectTargets = class {
2985
3291
  }
2986
3292
  /**
2987
3293
  * 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.
3294
+ * drops every pooled buffer, borrowed ones included.
2990
3295
  */
2991
3296
  acquire(width, height) {
2992
3297
  if (width !== this.width || height !== this.height) {
@@ -3289,9 +3594,12 @@ var GLYPH_PAINT_SOURCES = {
3289
3594
  var WeaselRenderer = class {
3290
3595
  gl;
3291
3596
  stats = { drawCalls: 0, ms: 0, spans: [] };
3597
+ /** Built on the first frame that draws with each, so a one-shot render of
3598
+ * plain shapes builds the batch program alone. */
3292
3599
  pathFill;
3293
3600
  pathFillVColor;
3294
3601
  batchFill;
3602
+ batchFillMatrices;
3295
3603
  gradFill;
3296
3604
  patternFill;
3297
3605
  /** Compiled on first use: only atlas text under a texture paint needs one. */
@@ -3362,10 +3670,6 @@ var WeaselRenderer = class {
3362
3670
  if (this.canvas) {
3363
3671
  this.canvas.width = Math.round(opts.width * opts.dpr);
3364
3672
  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
3673
  }
3370
3674
  const lossTarget = this.canvas ?? this.gl.canvas ?? null;
3371
3675
  if (lossTarget && typeof lossTarget.addEventListener === "function") {
@@ -3375,21 +3679,12 @@ var WeaselRenderer = class {
3375
3679
  }
3376
3680
  this.applyGlState();
3377
3681
  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);
3682
+ this.pathFill = new LazyProgram(this.gl, PATH_FILL_SPEC);
3683
+ this.pathFillVColor = new LazyProgram(this.gl, PATH_FILL_VCOLOR_SPEC);
3684
+ this.gradFill = new LazyProgram(this.gl, GRAD_FILL_SPEC);
3685
+ this.patternFill = new LazyProgram(this.gl, PATTERN_FILL_SPEC);
3686
+ this.batchFillMatrices = new LazyProgram(this.gl, BATCH_FILL_MATRICES_SPEC);
3687
+ this.batchFill = createBatchFill(this.gl);
3393
3688
  this.meshCache = new GLMeshCache(this.gl);
3394
3689
  this.textureCache = new GLTextureCache(this.gl);
3395
3690
  this.imageCache = new GLImageCache(this.gl, this.imageMinification);
@@ -3398,6 +3693,9 @@ var WeaselRenderer = class {
3398
3693
  this.drawBatch = new DrawBatch(this.gl, this.batchFill);
3399
3694
  this.whiteTexture = createWhiteTexture(this.gl);
3400
3695
  }
3696
+ lazyPrograms() {
3697
+ return [this.pathFill, this.pathFillVColor, this.gradFill, this.patternFill, this.batchFillMatrices];
3698
+ }
3401
3699
  uploadQuadGeometry() {
3402
3700
  const gl = this.gl;
3403
3701
  this.quadVbo = gl.createBuffer();
@@ -3415,12 +3713,11 @@ var WeaselRenderer = class {
3415
3713
  /**
3416
3714
  * Compile a consumer-registered shader program against this renderer's GL context.
3417
3715
  *
3418
- * Call once per renderer after the module-level `registerProgram()`.
3419
- * Throws `ShaderCompileError` if compilation fails.
3716
+ * Optional: a program compiles on the first frame that draws with it. Call
3717
+ * this after the module-level `registerProgram()` to pay for the compile
3718
+ * before that frame, or to catch the `ShaderCompileError` it throws.
3420
3719
  *
3421
3720
  * In dev mode, calling again with the same handle replaces the compiled program.
3422
- *
3423
- * @experimental
3424
3721
  */
3425
3722
  registerProgram(handle) {
3426
3723
  if (this.disposed) return;
@@ -3499,8 +3796,6 @@ var WeaselRenderer = class {
3499
3796
  gl.bindVertexArray(null);
3500
3797
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
3501
3798
  }
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
3799
  /** This renderer's rect in device pixels, top-left origin. A tile rounds its
3505
3800
  * edges rather than its size, so two that share a CSS edge share a device one. */
3506
3801
  deviceRect() {
@@ -3515,6 +3810,8 @@ var WeaselRenderer = class {
3515
3810
  height: Math.round((oy + this.heightCss) * this.dpr) - y
3516
3811
  };
3517
3812
  }
3813
+ /** Viewport and scissor for this frame. Re-applied inside `render()` because
3814
+ * a co-tenant on the same context moves both between our frames. */
3518
3815
  applyTarget() {
3519
3816
  const gl = this.gl;
3520
3817
  const { x, y, width, height } = this.deviceRect();
@@ -3552,21 +3849,8 @@ var WeaselRenderer = class {
3552
3849
  this.paintScratch.release();
3553
3850
  this.paintResources.release();
3554
3851
  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);
3852
+ for (const lazy of this.lazyPrograms()) lazy.abandon();
3853
+ this.batchFill = createBatchFill(this.gl);
3570
3854
  this.glyphPaintPrograms.clear();
3571
3855
  this.meshCache = new GLMeshCache(this.gl);
3572
3856
  this.textureCache = new GLTextureCache(this.gl);
@@ -3589,14 +3873,9 @@ var WeaselRenderer = class {
3589
3873
  }
3590
3874
  }
3591
3875
  /** 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.
3876
+ * context-loss listeners. Idempotent. The context itself is left alone, so
3877
+ * a caller-owned context can host renderer after renderer without
3878
+ * accumulating anything.
3600
3879
  *
3601
3880
  * A disposed renderer ignores further `render()` and `registerProgram()`
3602
3881
  * calls. */
@@ -3610,14 +3889,8 @@ var WeaselRenderer = class {
3610
3889
  }
3611
3890
  this.meshCache.dispose();
3612
3891
  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
- ]) {
3892
+ for (const lazy of this.lazyPrograms()) lazy.release();
3893
+ for (const prog of [this.batchFill, ...this.glyphPaintPrograms.values()]) {
3621
3894
  gl.deleteProgram(prog.handle);
3622
3895
  }
3623
3896
  this.glyphPaintPrograms.clear();
@@ -3653,7 +3926,7 @@ var WeaselRenderer = class {
3653
3926
  this.stats = { drawCalls: 0, ms: 0, spans: spanStats };
3654
3927
  const gl = this.gl;
3655
3928
  this.meshCache.drainPendingDeletes();
3656
- this.imageCache.drainPendingDeletes();
3929
+ this.imageCache.beginFrame();
3657
3930
  this.textureCache.sweep(textureRegistry);
3658
3931
  this.refreshPrograms();
3659
3932
  resetBakeBudget(this.bakeBudget);
@@ -3668,13 +3941,25 @@ var WeaselRenderer = class {
3668
3941
  gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
3669
3942
  }
3670
3943
  const device = this.deviceRect();
3944
+ const { pathFill, pathFillVColor, gradFill, patternFill, batchFillMatrices } = this;
3671
3945
  const ctx = {
3672
3946
  gl,
3673
- pathFill: this.pathFill,
3674
- pathFillVColor: this.pathFillVColor,
3947
+ get pathFill() {
3948
+ return pathFill.get();
3949
+ },
3950
+ get pathFillVColor() {
3951
+ return pathFillVColor.get();
3952
+ },
3675
3953
  batchFill: this.batchFill,
3676
- gradFill: this.gradFill,
3677
- patternFill: this.patternFill,
3954
+ get batchFillMatrices() {
3955
+ return batchFillMatrices.get();
3956
+ },
3957
+ get gradFill() {
3958
+ return gradFill.get();
3959
+ },
3960
+ get patternFill() {
3961
+ return patternFill.get();
3962
+ },
3678
3963
  meshCache: this.meshCache,
3679
3964
  textureCache: this.textureCache,
3680
3965
  imageCache: this.imageCache,
@@ -3739,10 +4024,6 @@ var WeaselRenderer = class {
3739
4024
  if (this.canvas) {
3740
4025
  this.canvas.width = Math.round(dims.width * dims.dpr);
3741
4026
  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
4027
  }
3747
4028
  this.applyTarget();
3748
4029
  }
@@ -3752,11 +4033,11 @@ var WeaselRenderer = class {
3752
4033
  }
3753
4034
  /** @internal */
3754
4035
  _pathFill() {
3755
- return this.pathFill;
4036
+ return this.pathFill.get();
3756
4037
  }
3757
4038
  /** @internal */
3758
4039
  _pathFillVColor() {
3759
- return this.pathFillVColor;
4040
+ return this.pathFillVColor.get();
3760
4041
  }
3761
4042
  /** @internal */
3762
4043
  _drawBatch() {
@@ -3767,12 +4048,16 @@ var WeaselRenderer = class {
3767
4048
  return this.batchFill;
3768
4049
  }
3769
4050
  /** @internal */
4051
+ _batchFillMatrices() {
4052
+ return this.batchFillMatrices.get();
4053
+ }
4054
+ /** @internal */
3770
4055
  _gradFill() {
3771
- return this.gradFill;
4056
+ return this.gradFill.get();
3772
4057
  }
3773
4058
  /** @internal */
3774
4059
  _patternFill() {
3775
- return this.patternFill;
4060
+ return this.patternFill.get();
3776
4061
  }
3777
4062
  /** @internal */
3778
4063
  _meshCache() {
@@ -3867,6 +4152,7 @@ function cullOne(cmd, m, box) {
3867
4152
  switch (cmd.kind) {
3868
4153
  case "group": {
3869
4154
  if (cmd.effects && cmd.effects.length > 0) return cmd;
4155
+ if (isOwnComposite(cmd.composite)) return cmd;
3870
4156
  const inner = cmd.transform ? mat3.multiply(m, cmd.transform) : m;
3871
4157
  const children = cullList(cmd.children, inner, box);
3872
4158
  if (children === cmd.children) return cmd;
@@ -4070,15 +4356,15 @@ function textCommandFromPose(pose, scale = 1) {
4070
4356
  };
4071
4357
  }
4072
4358
  function runsInPoseFrame(runs, box) {
4073
- if (runs.every((r) => isSolid(r.fill) && isSolid(r.stroke?.paint))) return runs;
4359
+ if (runs.every((r) => isSolid(r.fill?.paint) && isSolid(r.stroke?.paint))) return runs;
4074
4360
  return runs.map((r) => {
4075
4361
  const fill = r.fill && commandFill(r.fill, box);
4076
- const paint = r.stroke?.paint && commandFill(r.stroke.paint, box);
4362
+ const paint = r.stroke?.paint && commandPaint(r.stroke.paint, box);
4077
4363
  return { ...r, fill, stroke: r.stroke && { ...r.stroke, paint: paint || void 0 } };
4078
4364
  });
4079
4365
  }
4080
4366
  function isSolid(paint) {
4081
- return paint == null || (paint.fill ?? "solid") === "solid";
4367
+ return paint == null || isSolidPaint(paint);
4082
4368
  }
4083
4369
 
4084
4370
  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 };