@weasel-js/renderer 1.9.4 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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, BATCH_VERT_SRC, BATCH_FRAG_SRC, BATCH_FILL_UNIFORMS, BATCH_FILL_ATTRIBUTES, STRAIGHT_RGB_GLSL, GLImageCache, DrawBatch, getProgramSource, commandFill, commandPaint, uploadPremultiplied, cachedMarkerCommands, PAINT_MODE_PLAIN, PAINT_MODE_RADIAL, PAINT_MODE_CONIC, getMesh, quantizeStrokeScale, strokeMesh, axisAlignedClipRect, intersectClipRects, getPaintDrawKind, outlineMesh, outlineStrokeMesh, quantizeEmWidth } from './chunk-LFD47OV2.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-LFD47OV2.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() {
@@ -320,10 +320,11 @@ uniform float u_opacity;
320
320
  uniform float u_alpha;
321
321
  uniform mat4 u_colorMatrix;
322
322
  uniform vec4 u_colorBias;
323
+ ${STRAIGHT_RGB_GLSL}
323
324
  vec4 shadePaint(vec2 world) {
324
325
  vec2 uv = world / max(u_tileSize, vec2(0.0001));
325
326
  vec4 texel = texture(u_sampler, uv);
326
- vec4 mapped = clamp(u_colorMatrix * texel + u_colorBias, 0.0, 1.0);
327
+ vec4 mapped = clamp(u_colorMatrix * vec4(straightRgb(texel), texel.a) + u_colorBias, 0.0, 1.0);
327
328
  float a = mapped.a * u_opacity * u_alpha;
328
329
  return vec4(mapped.rgb * a, a);
329
330
  }
@@ -510,11 +511,9 @@ var GLMeshCache = class {
510
511
  return handle;
511
512
  }
512
513
  /**
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.
514
+ * Upload a Mesh that the caller knows is single-use. Returns a handle backed
515
+ * by fresh GL resources that the renderer frees at the end of the current
516
+ * frame via `freeTransient()`, so transient meshes never wait on GC.
518
517
  */
519
518
  uploadTransient(mesh) {
520
519
  const { handle, vbo, ibo } = this.upload(mesh);
@@ -628,6 +627,9 @@ var GLMeshCache = class {
628
627
  };
629
628
 
630
629
  // src/cache/GLTextureCache.ts
630
+ function uploadAsHeld(gl, source) {
631
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
632
+ }
631
633
  var GLTextureCache = class {
632
634
  constructor(gl) {
633
635
  this.gl = gl;
@@ -644,8 +646,11 @@ var GLTextureCache = class {
644
646
  /** Uploads the pixels once per id. `wrap` is re-applied whenever it differs
645
647
  * from what the id was last uploaded under: the same registered image can
646
648
  * 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. */
649
+ * the next. */
648
650
  upload(id, source, wrap = "clamp") {
651
+ return this.ensure(id, source, wrap, uploadAsHeld);
652
+ }
653
+ ensure(id, source, wrap, send) {
649
654
  const cached = this.map.get(id);
650
655
  if (cached) {
651
656
  if (this.wraps.get(id) !== wrap) this.applyWrap(id, cached, wrap);
@@ -656,7 +661,7 @@ var GLTextureCache = class {
656
661
  if (!tex) throw new Error(`GLTextureCache: createTexture failed for id="${id}"`);
657
662
  const wrapMode = wrap === "repeat" ? gl.REPEAT : gl.CLAMP_TO_EDGE;
658
663
  gl.bindTexture(gl.TEXTURE_2D, tex);
659
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
664
+ send(gl, source);
660
665
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
661
666
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
662
667
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapMode);
@@ -667,17 +672,18 @@ var GLTextureCache = class {
667
672
  return id;
668
673
  }
669
674
  /** `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. */
675
+ * under the same id: a changed source sends its pixels again. A registered
676
+ * texture is an image, so its texels are premultiplied. */
671
677
  uploadRegistered(id, source, wrap = "clamp") {
672
678
  const cached = this.map.get(id);
673
679
  if (cached && this.registered.get(id) !== source) {
674
680
  const gl = this.gl;
675
681
  gl.bindTexture(gl.TEXTURE_2D, cached);
676
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
682
+ uploadPremultiplied(gl, source);
677
683
  gl.bindTexture(gl.TEXTURE_2D, null);
678
684
  }
679
685
  this.registered.set(id, source);
680
- return this.upload(id, source, wrap);
686
+ return this.ensure(id, source, wrap, uploadPremultiplied);
681
687
  }
682
688
  /**
683
689
  * Delete the texture of every registry-backed id the registry has dropped.
@@ -736,6 +742,35 @@ var GLTextureCache = class {
736
742
  gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
737
743
  gl.bindTexture(gl.TEXTURE_2D, null);
738
744
  }
745
+ /** Create a texture from premultiplied RGBA bytes that are a color: a page
746
+ * of color glyphs. Mipmapped, as a font atlas must never be, because one is
747
+ * drawn well below the size it was baked at. No-op if `id` already exists. */
748
+ uploadRGBA(id, width, height, data) {
749
+ if (this.map.has(id)) return;
750
+ const gl = this.gl;
751
+ const tex = gl.createTexture();
752
+ if (!tex) throw new Error(`GLTextureCache: createTexture failed for id="${id}"`);
753
+ gl.bindTexture(gl.TEXTURE_2D, tex);
754
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
755
+ gl.generateMipmap(gl.TEXTURE_2D);
756
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
757
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
758
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
759
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
760
+ gl.bindTexture(gl.TEXTURE_2D, null);
761
+ this.map.set(id, tex);
762
+ this.wraps.set(id, "clamp");
763
+ }
764
+ /** Patch a rect of an existing RGBA texture with tightly-packed w×h texels. */
765
+ subImageRGBA(id, x, y, w, h, data) {
766
+ const tex = this.map.get(id);
767
+ if (!tex) throw new Error(`GLTextureCache: texture "${id}" not uploaded`);
768
+ const gl = this.gl;
769
+ gl.bindTexture(gl.TEXTURE_2D, tex);
770
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, data);
771
+ gl.generateMipmap(gl.TEXTURE_2D);
772
+ gl.bindTexture(gl.TEXTURE_2D, null);
773
+ }
739
774
  bind(id, unit) {
740
775
  const tex = this.map.get(id);
741
776
  if (!tex) throw new Error(`GLTextureCache: texture "${id}" not uploaded`);
@@ -744,10 +779,8 @@ var GLTextureCache = class {
744
779
  gl.bindTexture(gl.TEXTURE_2D, tex);
745
780
  }
746
781
  /**
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.
782
+ * Delete every uploaded GL texture and clear the map. The cache is
783
+ * refillable afterward.
751
784
  */
752
785
  free() {
753
786
  const gl = this.gl;
@@ -858,10 +891,8 @@ var GradientRampAtlas = class {
858
891
  this.cacheHits = 0;
859
892
  }
860
893
  /**
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).
894
+ * Delete the atlas texture and forget every row in it. The CPU mirror goes
895
+ * with it, so the atlas is refillable afterward. Stats are left as-is.
865
896
  */
866
897
  free() {
867
898
  if (this.texture) this.gl.deleteTexture(this.texture);
@@ -999,16 +1030,13 @@ var GroupState = class {
999
1030
  this.colorMatrixStack.push(nextCM);
1000
1031
  }
1001
1032
  /**
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.
1033
+ * Push a frame whose children paint into a surface of their own. The
1034
+ * transform still accumulates. Alpha and color do not: applying them on the
1035
+ * way in would fade the pixels an effect is about to read, then fade them
1036
+ * again on the way out.
1008
1037
  *
1009
1038
  * 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.
1039
+ * would have left on the stack.
1012
1040
  */
1013
1041
  pushIsolated(frame) {
1014
1042
  this.push(frame);
@@ -1085,6 +1113,49 @@ function compositeProgramId() {
1085
1113
  return COMPOSITE_PROGRAM_ID;
1086
1114
  }
1087
1115
 
1116
+ // src/effects/compositeOp.ts
1117
+ var OPERATORS = {
1118
+ "source-over": { source: "one", destination: "one-minus-source-alpha" },
1119
+ "source-in": { source: "destination-alpha", destination: "zero" },
1120
+ "source-out": { source: "one-minus-destination-alpha", destination: "zero" },
1121
+ "source-atop": { source: "destination-alpha", destination: "one-minus-source-alpha" },
1122
+ "destination-over": { source: "one-minus-destination-alpha", destination: "one" },
1123
+ "destination-in": { source: "zero", destination: "source-alpha" },
1124
+ "destination-out": { source: "zero", destination: "one-minus-source-alpha" },
1125
+ "destination-atop": { source: "one-minus-destination-alpha", destination: "source-alpha" },
1126
+ xor: { source: "one-minus-destination-alpha", destination: "one-minus-source-alpha" },
1127
+ lighter: { source: "one", destination: "one" },
1128
+ copy: { source: "one", destination: "zero" }
1129
+ };
1130
+ function blendFactorsOf(composite) {
1131
+ return typeof composite === "string" ? OPERATORS[composite] : composite;
1132
+ }
1133
+ function isOwnComposite(composite) {
1134
+ if (composite === void 0) return false;
1135
+ const { source, destination } = blendFactorsOf(composite);
1136
+ return source !== "one" || destination !== "one-minus-source-alpha";
1137
+ }
1138
+ function glFactor(gl, factor) {
1139
+ switch (factor) {
1140
+ case "zero":
1141
+ return gl.ZERO;
1142
+ case "one":
1143
+ return gl.ONE;
1144
+ case "source-alpha":
1145
+ return gl.SRC_ALPHA;
1146
+ case "one-minus-source-alpha":
1147
+ return gl.ONE_MINUS_SRC_ALPHA;
1148
+ case "destination-alpha":
1149
+ return gl.DST_ALPHA;
1150
+ case "one-minus-destination-alpha":
1151
+ return gl.ONE_MINUS_DST_ALPHA;
1152
+ }
1153
+ }
1154
+ function applyComposite(gl, composite) {
1155
+ const { source, destination } = blendFactorsOf(composite);
1156
+ gl.blendFunc(glFactor(gl, source), glFactor(gl, destination));
1157
+ }
1158
+
1088
1159
  // src/draw/drawTriangles.ts
1089
1160
  function drawTriangles(ctx, count, type) {
1090
1161
  ctx.gl.drawElements(ctx.gl.TRIANGLES, count, type, 0);
@@ -1128,14 +1199,17 @@ function endParityMask(ctx) {
1128
1199
 
1129
1200
  // src/draw/customShader.ts
1130
1201
  var warnedUniforms = /* @__PURE__ */ new Set();
1131
- function warnOnceUniform(programId, name) {
1132
- const key = `${programId}:${name}`;
1202
+ function warnOnce(key, message) {
1133
1203
  if (warnedUniforms.has(key)) return;
1134
1204
  warnedUniforms.add(key);
1135
1205
  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
- }
1206
+ if (isDev) console.warn(message);
1207
+ }
1208
+ function warnOnceUniform(programId, name) {
1209
+ warnOnce(
1210
+ `${programId}:${name}`,
1211
+ `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).`
1212
+ );
1139
1213
  }
1140
1214
  function setUniform(gl, loc, value, textureCache, nextTexUnit) {
1141
1215
  if (value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Float32Array) && "id" in value && typeof value.id === "string") {
@@ -1259,10 +1333,11 @@ function setUniforms(gl, program, programId, uniforms, textureCache, nextTexUnit
1259
1333
  }
1260
1334
  function drawShader(ctx, cmd) {
1261
1335
  const { gl, programRegistry, quadVbo, quadIbo, textureCache } = ctx;
1262
- const program = programRegistry.get(cmd.program.id);
1336
+ const program = ctx.ensureProgram?.(cmd.program.id) ?? programRegistry.get(cmd.program.id);
1263
1337
  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().`
1338
+ warnOnce(
1339
+ `${cmd.program.id}:(program)`,
1340
+ `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
1341
  );
1267
1342
  return;
1268
1343
  }
@@ -1693,9 +1768,41 @@ function isPxMarker(ref) {
1693
1768
  function resolvedMarker(ref, scale) {
1694
1769
  return { ...ref, size: resolveMarkerSize(ref, 0, quantizeStrokeScale(ref.size.px, scale)) };
1695
1770
  }
1771
+
1772
+ // src/draw/foldColor.ts
1773
+ function isIdentityColorMatrix(cm) {
1774
+ return cm === IDENTITY_COLOR_MATRIX || sameValues(IDENTITY_COLOR_MATRIX, cm);
1775
+ }
1776
+ var colorFolding = { enabled: true };
1777
+ var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
1778
+ function foldColor(ctx, staged, r, g, b, a) {
1779
+ if (!staged.foldsAlpha) return [r, g, b, a];
1780
+ const cm = ctx.state.colorMatrix;
1781
+ const alpha = ctx.state.alpha;
1782
+ if (isIdentityColorMatrix(cm)) return [r, g, b, a * alpha];
1783
+ return [
1784
+ clamp01(cm[0] * r + cm[1] * g + cm[2] * b + cm[3] * a + cm[4]),
1785
+ clamp01(cm[5] * r + cm[6] * g + cm[7] * b + cm[8] * a + cm[9]),
1786
+ clamp01(cm[10] * r + cm[11] * g + cm[12] * b + cm[13] * a + cm[14]),
1787
+ clamp01(cm[15] * r + cm[16] * g + cm[17] * b + cm[18] * a + cm[19]) * alpha
1788
+ ];
1789
+ }
1790
+ var NO_TINT = [1, 1, 1, 1];
1791
+ var DIAGONAL = [0, 6, 12, 18];
1792
+ function textureTint(cm) {
1793
+ if (isIdentityColorMatrix(cm)) return NO_TINT;
1794
+ for (let i = 0; i < 20; i++) {
1795
+ const v = cm[i];
1796
+ if (DIAGONAL.includes(i) ? v < 0 || v > 1 : v !== 0) return null;
1797
+ }
1798
+ return [cm[0], cm[6], cm[12], cm[18]];
1799
+ }
1800
+ function stagedTint(staged, tint) {
1801
+ return staged.foldsAlpha && tint !== null ? tint : NO_TINT;
1802
+ }
1696
1803
  function batchPaint(ctx, fill, hasVColors) {
1697
1804
  if (hasVColors) return void 0;
1698
- const kind = fill.fill ?? "solid";
1805
+ const kind = fill.kind ?? "solid";
1699
1806
  if (kind === "solid") {
1700
1807
  const solid = fill;
1701
1808
  return { kind: "solid", color: solid.color, opacity: solid.opacity };
@@ -1706,8 +1813,8 @@ function batchPaint(ctx, fill, hasVColors) {
1706
1813
  const grad = fill;
1707
1814
  const g = gradientMap(ctx, grad.units);
1708
1815
  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));
1816
+ if (grad.kind === "linear-gradient") return ramp(grad, PAINT_MODE_PLAIN, linearRampUV(g, grad));
1817
+ if (grad.kind === "radial-gradient") return ramp(grad, PAINT_MODE_RADIAL, radialRampUV(g, grad));
1711
1818
  return ramp(grad, PAINT_MODE_CONIC, conicRampUV(g, grad));
1712
1819
  }
1713
1820
  function ramp(fill, mode, uv) {
@@ -1780,16 +1887,14 @@ function spaceToDevice(ctx, units) {
1780
1887
  }
1781
1888
 
1782
1889
  // 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) {
1890
+ function stagedStateIsLive(ctx, staged, image, sampling, synthBold = 0, folds = false) {
1787
1891
  if (staged.clipDepth !== (ctx.clipStack.length === 0 ? ctx.clipDepth : runClipDepth(ctx))) return false;
1788
1892
  if (staged.synthBold !== synthBold) return false;
1789
1893
  if (!staged.foldsAlpha && staged.alpha !== ctx.state.alpha) return false;
1790
1894
  if (image !== void 0 && slotForBitmap(staged, image, sampling) < 0) return false;
1791
1895
  const colorMatrix = ctx.state.colorMatrix;
1792
- return staged.colorMatrix === colorMatrix || sameValues(staged.colorMatrix, colorMatrix);
1896
+ if (staged.colorMatrix === colorMatrix || sameValues(staged.colorMatrix, colorMatrix)) return true;
1897
+ return folds && staged.foldsAlpha;
1793
1898
  }
1794
1899
  function slotForBitmap(staged, image, sampling) {
1795
1900
  for (let i = 0; i < staged.textures.length; i++) {
@@ -1827,13 +1932,15 @@ function canBatchMesh(ctx, mesh) {
1827
1932
  return mesh.vertices.length >> 1 <= cap;
1828
1933
  }
1829
1934
  function stageSolid(ctx, vertices) {
1830
- if (ctx.batchState !== void 0 && !stagedStateIsLive(ctx, ctx.batchState)) flushBatch(ctx);
1935
+ if (ctx.batchState !== void 0 && !stagedStateIsLive(ctx, ctx.batchState, void 0, void 0, 0, colorFolding.enabled)) {
1936
+ flushBatch(ctx);
1937
+ }
1831
1938
  if (ctx.drawBatch.wouldOverflow(vertices)) flushBatch(ctx);
1832
- return openRun(ctx);
1939
+ return openRun(ctx, colorFolding.enabled);
1833
1940
  }
1834
- function openRun(ctx) {
1941
+ function openRun(ctx, folds = false) {
1835
1942
  if (ctx.batchState === void 0) {
1836
- const colorMatrix = ctx.state.colorMatrix;
1943
+ const colorMatrix = folds ? IDENTITY_COLOR_MATRIX : ctx.state.colorMatrix;
1837
1944
  const foldsAlpha = isIdentityColorMatrix(colorMatrix);
1838
1945
  ctx.batchState = {
1839
1946
  alpha: foldsAlpha ? 1 : ctx.state.alpha,
@@ -1847,24 +1954,25 @@ function openRun(ctx) {
1847
1954
  return ctx.batchState;
1848
1955
  }
1849
1956
  function stageImage(ctx, image, sampling) {
1850
- if (ctx.batchState !== void 0 && !stagedStateIsLive(ctx, ctx.batchState, image, sampling)) {
1957
+ const tint = textureTint(ctx.state.colorMatrix) ;
1958
+ if (ctx.batchState !== void 0 && !stagedStateIsLive(ctx, ctx.batchState, image, sampling, 0, tint !== null)) {
1851
1959
  flushBatch(ctx);
1852
1960
  }
1853
1961
  if (ctx.drawBatch.wouldOverflow(4)) flushBatch(ctx);
1854
- const staged = openRun(ctx);
1962
+ const staged = openRun(ctx, tint !== null);
1855
1963
  let slot = slotForBitmap(staged, image, sampling);
1856
1964
  if (slot > staged.textures.length) {
1857
1965
  staged.textures.push({ kind: "bitmap", image, sampling });
1858
1966
  slot = staged.textures.length;
1859
1967
  }
1860
- return { staged, slot };
1968
+ return { staged, slot, tint: stagedTint(staged, tint) };
1861
1969
  }
1862
1970
  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)) {
1971
+ if (ctx.batchState !== void 0 && (!stagedStateIsLive(ctx, ctx.batchState, void 0, void 0, synthBold, colorFolding.enabled) || slotForAtlas(ctx.batchState, atlasId) < 0)) {
1864
1972
  flushBatch(ctx);
1865
1973
  }
1866
1974
  if (ctx.drawBatch.wouldOverflow(4)) flushBatch(ctx);
1867
- const staged = openRun(ctx);
1975
+ const staged = openRun(ctx, colorFolding.enabled);
1868
1976
  staged.synthBold = synthBold;
1869
1977
  let slot = slotForAtlas(staged, atlasId);
1870
1978
  if (slot > staged.textures.length) {
@@ -1892,7 +2000,7 @@ function stagedAlpha(ctx, staged, opacity) {
1892
2000
  }
1893
2001
  function stagedColor(ctx, staged, paint) {
1894
2002
  const [r, g, b, a] = resolveColor(paint.color);
1895
- return [r, g, b, a * stagedAlpha(ctx, staged, paint.opacity)];
2003
+ return foldColor(ctx, staged, r, g, b, a * (paint.opacity ?? 1));
1896
2004
  }
1897
2005
  function pushRect(ctx, rect, fill) {
1898
2006
  const staged = stageSolid(ctx, 4);
@@ -1960,6 +2068,20 @@ function tryStageFill(ctx, mesh, paint) {
1960
2068
  flushForOwnDraw(ctx);
1961
2069
  return false;
1962
2070
  }
2071
+ var NO_FIELD = [0, 0];
2072
+ function stageColorGlyphs(ctx, atlasId) {
2073
+ if (ctx.batchState !== void 0 && (!stagedStateIsLive(ctx, ctx.batchState) || slotForAtlas(ctx.batchState, atlasId) < 0)) {
2074
+ flushBatch(ctx);
2075
+ }
2076
+ if (ctx.drawBatch.wouldOverflow(4)) flushBatch(ctx);
2077
+ const staged = openRun(ctx);
2078
+ let slot = slotForAtlas(staged, atlasId);
2079
+ if (slot > staged.textures.length) {
2080
+ staged.textures.push({ kind: "atlas", id: atlasId, fieldScale: NO_FIELD });
2081
+ slot = staged.textures.length;
2082
+ }
2083
+ return { staged, slot };
2084
+ }
1963
2085
  function patternTileSpace(spaceInverse, origin, transform) {
1964
2086
  const [a, b, c, d] = transform ?? [1, 0, 0, 1];
1965
2087
  const placement = mat3.fromAffine([a, b, c, d, origin.x, origin.y]);
@@ -2004,7 +2126,7 @@ function unownedPaintResources(gl) {
2004
2126
  return made;
2005
2127
  }
2006
2128
  function bindPattern(ctx, prog, fill, model) {
2007
- const tex = fill.pattern;
2129
+ const tex = fill.texture;
2008
2130
  const entry = getTexture(tex.id);
2009
2131
  if (!entry) {
2010
2132
  const isDev = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
@@ -2048,7 +2170,7 @@ function bindGradient(ctx, prog, fill, model) {
2048
2170
  gl.uniform1f(prog.uniform("u_rampV"), ctx.gradRamps.rowV(row));
2049
2171
  setAlphaUniform(ctx, prog, ctx.state.alpha);
2050
2172
  gl.uniform1f(prog.uniform("u_opacity"), fill.opacity ?? 1);
2051
- if (fill.fill === "linear-gradient") {
2173
+ if (fill.kind === "linear-gradient") {
2052
2174
  gl.uniform1i(prog.uniform("u_gradKind"), 0);
2053
2175
  const dx = fill.to.x - fill.from.x;
2054
2176
  const dy = fill.to.y - fill.from.y;
@@ -2058,7 +2180,7 @@ function bindGradient(ctx, prog, fill, model) {
2058
2180
  gl.uniform1f(prog.uniform("u_gradLen"), len);
2059
2181
  gl.uniform1f(prog.uniform("u_gradRadius"), 0);
2060
2182
  gl.uniform1f(prog.uniform("u_gradAngle"), 0);
2061
- } else if (fill.fill === "radial-gradient") {
2183
+ } else if (fill.kind === "radial-gradient") {
2062
2184
  gl.uniform1i(prog.uniform("u_gradKind"), 1);
2063
2185
  gl.uniform2f(prog.uniform("u_gradP0"), fill.center.x, fill.center.y);
2064
2186
  gl.uniform2f(prog.uniform("u_gradDir"), 0, 0);
@@ -2078,12 +2200,11 @@ function bindGradient(ctx, prog, fill, model) {
2078
2200
 
2079
2201
  // src/draw/pathPaint.ts
2080
2202
  function drawPath(ctx, cmd) {
2081
- if (cmd.fill) drawPathFill(ctx, cmd);
2203
+ if (cmd.fill?.paint) drawPathFill(ctx, cmd, cmd.fill.paint);
2082
2204
  if (cmd.stroke?.paint) drawPathStroke(ctx, cmd);
2083
2205
  }
2084
- function drawPathFill(ctx, cmd) {
2085
- const fill = cmd.fill;
2086
- const isSolid2 = fill.fill === void 0 || fill.fill === "solid";
2206
+ function drawPathFill(ctx, cmd, fill) {
2207
+ const isSolid2 = fill.kind === void 0 || fill.kind === "solid";
2087
2208
  const hasVColors = !!(cmd.vertexColors && cmd.vertexColors.length > 0);
2088
2209
  const solid = fill;
2089
2210
  const paint = batchPaint(ctx, fill, hasVColors);
@@ -2161,9 +2282,9 @@ function drawPathFillVColor(ctx, cmd, fill, handle) {
2161
2282
  gl.bindVertexArray(null);
2162
2283
  }
2163
2284
  function bindPathFillByKind(ctx, fill) {
2164
- const kind = fill.fill ?? "solid";
2285
+ const kind = fill.kind ?? "solid";
2165
2286
  if (kind === "solid") return bindPathFillSolid(ctx, fill);
2166
- if (kind === "pattern") return bindPathFillPattern(ctx, fill);
2287
+ if (kind === "texture") return bindPathFillPattern(ctx, fill);
2167
2288
  if (kind === "linear-gradient" || kind === "radial-gradient" || kind === "conic-gradient") {
2168
2289
  return bindPathFillGradient(ctx, fill);
2169
2290
  }
@@ -2224,7 +2345,7 @@ function drawPathStroke(ctx, rawCmd) {
2224
2345
  function drawPathStrokeUnclipped(ctx, cmd, spaces) {
2225
2346
  const stroke = cmd.stroke;
2226
2347
  const paint = stroke.paint;
2227
- const isSolid2 = paint.fill === void 0 || paint.fill === "solid";
2348
+ const isSolid2 = paint.kind === void 0 || paint.kind === "solid";
2228
2349
  const solid = paint;
2229
2350
  const mesh = strokeMesh(cmd.path, stroke, ctx.flattenTolerance, spaces);
2230
2351
  if (mesh.indices.length === 0) return;
@@ -2273,7 +2394,7 @@ function doubledVertexWidths(widths) {
2273
2394
  function drawPathStrokeStenciled(ctx, cmd, align, spaces) {
2274
2395
  const stroke = cmd.stroke;
2275
2396
  const paint = stroke.paint;
2276
- const isSolid2 = paint.fill === void 0 || paint.fill === "solid";
2397
+ const isSolid2 = paint.kind === void 0 || paint.kind === "solid";
2277
2398
  const solid = paint;
2278
2399
  const widerStroke = {
2279
2400
  ...stroke,
@@ -2333,10 +2454,9 @@ function outlineGroupMesh(group, dx, dy) {
2333
2454
  }
2334
2455
  function outlineGroupStrokeMesh(group, dx, dy, transform) {
2335
2456
  const stroke = group.stroke;
2336
- if (!stroke) return null;
2457
+ if (!strokeHasInk(stroke)) return null;
2337
2458
  const width = stroke.width ?? 1;
2338
2459
  if (typeof width === "object") {
2339
- if (!(width.px > 0)) return null;
2340
2460
  const shear = shearOf(group);
2341
2461
  const [a, b, c, d] = [transform[0], transform[1], transform[3], transform[4]];
2342
2462
  return mergeGlyphMeshes(group, dx, dy, (glyph) => {
@@ -2355,7 +2475,6 @@ function outlineGroupStrokeMesh(group, dx, dy, transform) {
2355
2475
  );
2356
2476
  });
2357
2477
  }
2358
- if (!(width > 0)) return null;
2359
2478
  return mergeGlyphMeshes(group, dx, dy, (glyph) => glyph.scale > 0 ? outlineStrokeMesh(
2360
2479
  glyph.key,
2361
2480
  glyph.d,
@@ -2395,6 +2514,48 @@ function mergeGlyphMeshes(group, dx, dy, meshFor) {
2395
2514
  }
2396
2515
  return { vertices, indices };
2397
2516
  }
2517
+ function drawColorGlyphs(ctx, group, dx, dy) {
2518
+ const fill = group.fill?.paint;
2519
+ if (fill === void 0 || group.quads.length === 0) return;
2520
+ if (!syncColorPageTexture(ctx.textureCache, group.page)) return;
2521
+ const opacity = isSolidPaint(fill) ? resolveColor(fill.color)[3] : 1;
2522
+ const atlasId = colorPageTextureId(group.page);
2523
+ const batch = ctx.drawBatch;
2524
+ const m = ctx.state.transform;
2525
+ let { staged, slot } = stageColorGlyphs(ctx, atlasId);
2526
+ let alpha = opacity * (staged.foldsAlpha ? ctx.state.alpha : 1);
2527
+ const restage = () => {
2528
+ ({ staged, slot } = stageColorGlyphs(ctx, atlasId));
2529
+ alpha = opacity * (staged.foldsAlpha ? ctx.state.alpha : 1);
2530
+ };
2531
+ for (const q of group.quads) {
2532
+ if (batch.wouldOverflow(4)) {
2533
+ flushBatch(ctx);
2534
+ restage();
2535
+ }
2536
+ let cut;
2537
+ while (!batch.pushQuad(
2538
+ q.x0 + dx,
2539
+ q.y0 + dy,
2540
+ q.x1 - q.x0,
2541
+ q.y1 - q.y0,
2542
+ m,
2543
+ q.u0,
2544
+ q.v0,
2545
+ q.u1,
2546
+ q.v1,
2547
+ alpha,
2548
+ slot
2549
+ )) {
2550
+ cut = enterUncut(ctx);
2551
+ restage();
2552
+ }
2553
+ if (cut !== void 0) {
2554
+ leaveUncut(ctx, cut);
2555
+ restage();
2556
+ }
2557
+ }
2558
+ }
2398
2559
 
2399
2560
  // src/draw/scratchPass.ts
2400
2561
  var UNIT_QUAD = {
@@ -2403,7 +2564,7 @@ var UNIT_QUAD = {
2403
2564
  };
2404
2565
  function renderPaintToScratch(ctx, fill, rect) {
2405
2566
  const scratch = ctx.paintScratch;
2406
- const bind = getPaintDrawKind(fill.fill)?.bind;
2567
+ const bind = getPaintDrawKind(fill.kind)?.bind;
2407
2568
  if (!scratch || !bind) return null;
2408
2569
  const ratio = ctx.deviceWidth && ctx.widthCss > 0 ? ctx.deviceWidth / ctx.widthCss : 1;
2409
2570
  const deviceW = ctx.deviceWidth ?? Math.round(ctx.widthCss);
@@ -2492,21 +2653,22 @@ function drawText(ctx, cmd) {
2492
2653
  align,
2493
2654
  direction,
2494
2655
  tabSize: style.tabSize,
2495
- justify: cmd.justify,
2496
- outlineMinSize
2656
+ justify: cmd.justify
2497
2657
  });
2498
- if (laid.groups.length === 0 && laid.decorations.length === 0) return;
2658
+ const { groups, decorations } = cachedPaintRuns(laid, cmd.runs, { outlineMinSize });
2659
+ if (groups.length === 0 && decorations.length === 0) return;
2499
2660
  const dx = cmd.x;
2500
2661
  const dy = cmd.y + verticalAlignOffset(cmd.verticalAlign, cmd.height, laid.bounds.height);
2501
- for (const group of laid.groups) {
2662
+ for (const group of groups) {
2502
2663
  if (group.source === "outline") drawTextOutlineGroup(ctx, group, dx, dy);
2664
+ else if (group.source === "color") drawColorGlyphs(ctx, group, dx, dy);
2503
2665
  else drawTextGroup(ctx, group, dx, dy);
2504
2666
  }
2505
- drawTextDecorations(ctx, laid.decorations, dx, dy);
2667
+ drawTextDecorations(ctx, decorations, dx, dy);
2506
2668
  }
2507
2669
  function drawTextOutlineGroup(ctx, group, dx, dy) {
2508
- const fill = group.fill;
2509
- if (fill !== null) {
2670
+ const fill = group.fill?.paint;
2671
+ if (fill !== void 0) {
2510
2672
  const mesh = outlineGroupMesh(group, dx, dy);
2511
2673
  if (mesh) drawOutlineMesh(ctx, fill, mesh);
2512
2674
  }
@@ -2523,11 +2685,13 @@ function drawOutlineMesh(ctx, fill, mesh) {
2523
2685
  }
2524
2686
  function drawTextDecorations(ctx, decorations, dx, dy) {
2525
2687
  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));
2688
+ const paint = d.fill.paint;
2689
+ if (paint === void 0) continue;
2690
+ if (glyphPaintKindOf(paint) !== null) {
2691
+ drawOutlineMesh(ctx, paint, ruleMesh(d.x0 + dx, d.y0 + dy, d.x1 + dx, d.y1 + dy));
2528
2692
  continue;
2529
2693
  }
2530
- const [r, g, b, a] = "color" in d.fill ? resolveColor(d.fill.color) : [0, 0, 0, 1];
2694
+ const [r, g, b, a] = isSolidPaint(paint) ? resolveColor(paint.color) : [0, 0, 0, 1];
2531
2695
  let cut;
2532
2696
  for (; ; ) {
2533
2697
  const staged = stageSolid(ctx, 4);
@@ -2537,10 +2701,7 @@ function drawTextDecorations(ctx, decorations, dx, dy) {
2537
2701
  d.x1 - d.x0,
2538
2702
  d.y1 - d.y0,
2539
2703
  ctx.state.transform,
2540
- r,
2541
- g,
2542
- b,
2543
- a * (staged.foldsAlpha ? ctx.state.alpha : 1)
2704
+ ...foldColor(ctx, staged, r, g, b, a)
2544
2705
  )) break;
2545
2706
  cut = enterUncut(ctx);
2546
2707
  }
@@ -2572,21 +2733,23 @@ function drawTextGroup(ctx, group, dx, dy) {
2572
2733
  if (!fieldScale) return;
2573
2734
  const bold = group.synthetic.bold ? SYNTH_BOLD_AMOUNT : 0;
2574
2735
  const tanItalic = group.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
2575
- const paintKind = glyphPaintKindOf(group.fill);
2736
+ const fill = group.fill?.paint;
2737
+ if (fill === void 0) return;
2738
+ const paintKind = glyphPaintKindOf(fill);
2576
2739
  if (paintKind !== null) {
2577
2740
  drawPaintedGlyphs(ctx, group, dx, dy, paintKind, atlasId, mode, bold, tanItalic, fieldScale);
2578
2741
  return;
2579
2742
  }
2580
- const color = group.fill !== null && "color" in group.fill ? resolveColor(group.fill.color) : [0, 0, 0, 1];
2743
+ const color = isSolidPaint(fill) ? resolveColor(fill.color) : [0, 0, 0, 1];
2581
2744
  const batch = ctx.drawBatch;
2582
2745
  const m = ctx.state.transform;
2583
2746
  let { staged, slot } = stageGlyphs(ctx, atlasId, fieldScale, bold);
2584
- let alpha = color[3] * (staged.foldsAlpha ? ctx.state.alpha : 1);
2747
+ let rgba = foldColor(ctx, staged, color[0], color[1], color[2], color[3]);
2585
2748
  for (const q of group.quads) {
2586
2749
  if (batch.wouldOverflow(4)) {
2587
2750
  flushBatch(ctx);
2588
2751
  ({ staged, slot } = stageGlyphs(ctx, atlasId, fieldScale, bold));
2589
- alpha = color[3] * (staged.foldsAlpha ? ctx.state.alpha : 1);
2752
+ rgba = foldColor(ctx, staged, color[0], color[1], color[2], color[3]);
2590
2753
  }
2591
2754
  let cut;
2592
2755
  while (!batch.pushGlyph(
@@ -2601,27 +2764,27 @@ function drawTextGroup(ctx, group, dx, dy) {
2601
2764
  q.v0,
2602
2765
  q.u1,
2603
2766
  q.v1,
2604
- color[0],
2605
- color[1],
2606
- color[2],
2607
- alpha,
2767
+ rgba[0],
2768
+ rgba[1],
2769
+ rgba[2],
2770
+ rgba[3],
2608
2771
  slot,
2609
2772
  mode
2610
2773
  )) {
2611
2774
  cut = enterUncut(ctx);
2612
2775
  ({ staged, slot } = stageGlyphs(ctx, atlasId, fieldScale, bold));
2613
- alpha = color[3] * (staged.foldsAlpha ? ctx.state.alpha : 1);
2776
+ rgba = foldColor(ctx, staged, color[0], color[1], color[2], color[3]);
2614
2777
  }
2615
2778
  if (cut !== void 0) {
2616
2779
  leaveUncut(ctx, cut);
2617
2780
  ({ staged, slot } = stageGlyphs(ctx, atlasId, fieldScale, bold));
2618
- alpha = color[3] * (staged.foldsAlpha ? ctx.state.alpha : 1);
2781
+ rgba = foldColor(ctx, staged, color[0], color[1], color[2], color[3]);
2619
2782
  }
2620
2783
  }
2621
2784
  }
2622
2785
  function glyphPaintKindOf(fill) {
2623
- const kind = fill?.fill ?? "solid";
2624
- if (kind === "pattern") return "pattern";
2786
+ const kind = fill.kind ?? "solid";
2787
+ if (kind === "texture") return "pattern";
2625
2788
  if (kind === "linear-gradient" || kind === "radial-gradient" || kind === "conic-gradient") {
2626
2789
  return "gradient";
2627
2790
  }
@@ -2632,7 +2795,7 @@ function drawPaintedGlyphs(ctx, group, dx, dy, kind, atlasId, mode, bold, tanIta
2632
2795
  const prog = ctx.glyphPaintProgram?.(kind);
2633
2796
  if (!prog) return;
2634
2797
  flushForOwnDraw(ctx);
2635
- const fill = group.fill;
2798
+ const fill = group.fill.paint;
2636
2799
  const m = ctx.state.transform;
2637
2800
  let bound;
2638
2801
  if (kind === "pattern") {
@@ -2713,9 +2876,22 @@ function glyphScreenBounds(group, dx, dy, tanItalic, m) {
2713
2876
  var SPRITE_STRIDE = 9;
2714
2877
 
2715
2878
  // src/draw/image.ts
2879
+ var MINIFIED = 2;
2880
+ function minifiedLimits(ctx) {
2881
+ const m = ctx.state.transform;
2882
+ const dpr = ctx.deviceWidth && ctx.widthCss ? ctx.deviceWidth / ctx.widthCss : 1;
2883
+ const k = (MINIFIED * dpr) ** 2;
2884
+ return [k * (m[0] * m[0] + m[1] * m[1]), k * (m[3] * m[3] + m[4] * m[4])];
2885
+ }
2886
+ var isMinified = (limit, sw, sh, dw, dh) => sw * sw >= limit[0] * dw * dw || sh * sh >= limit[1] * dh * dh;
2716
2887
  function drawImage(ctx, cmd) {
2717
2888
  ctx.imageCache.upload(cmd.image, cmd.image);
2718
- const { staged, slot } = stageImage(ctx, cmd.image, cmd.sampling ?? "linear");
2889
+ if (ctx.imageCache.lacksMipmaps(cmd.image)) {
2890
+ const sw = cmd.source?.w ?? cmd.image.width;
2891
+ const sh = cmd.source?.h ?? cmd.image.height;
2892
+ if (isMinified(minifiedLimits(ctx), sw, sh, cmd.w, cmd.h)) ctx.imageCache.buildMipmaps(cmd.image);
2893
+ }
2894
+ const { staged, slot, tint } = stageImage(ctx, cmd.image, cmd.sampling ?? "linear");
2719
2895
  const src = cmd.source;
2720
2896
  let u0 = 0, v0 = 0, u1 = 1, v1 = 1;
2721
2897
  if (src) {
@@ -2748,8 +2924,11 @@ function drawImage(ctx, cmd) {
2748
2924
  // Group alpha rides the vertices only where it rode the solid colors too:
2749
2925
  // with a real color matrix in the run it is `u_alpha`, and folding it here
2750
2926
  // as well would apply it twice.
2751
- (cmd.opacity ?? 1) * (staged.foldsAlpha ? ctx.state.alpha : 1),
2752
- slot
2927
+ (cmd.opacity ?? 1) * (staged.foldsAlpha ? ctx.state.alpha : 1) * tint[3],
2928
+ slot,
2929
+ tint[0],
2930
+ tint[1],
2931
+ tint[2]
2753
2932
  )) retryUncut(ctx, cmd, void 0, drawImage);
2754
2933
  }
2755
2934
  function drawSprites(ctx, cmd) {
@@ -2757,18 +2936,26 @@ function drawSprites(ctx, cmd) {
2757
2936
  const count = Math.floor(data.length / SPRITE_STRIDE);
2758
2937
  if (count === 0) return;
2759
2938
  ctx.imageCache.upload(cmd.image, cmd.image);
2939
+ if (ctx.imageCache.lacksMipmaps(cmd.image)) {
2940
+ const limit = minifiedLimits(ctx);
2941
+ for (let i = 0; i < count * SPRITE_STRIDE; i += SPRITE_STRIDE) {
2942
+ if (!isMinified(limit, data[i + 6], data[i + 7], data[i + 2], data[i + 3])) continue;
2943
+ ctx.imageCache.buildMipmaps(cmd.image);
2944
+ break;
2945
+ }
2946
+ }
2760
2947
  const sampling = cmd.sampling ?? "linear";
2761
- let { staged, slot } = stageImage(ctx, cmd.image, sampling);
2948
+ let { staged, slot, tint } = stageImage(ctx, cmd.image, sampling);
2762
2949
  const batch = ctx.drawBatch;
2763
2950
  const m = ctx.state.transform;
2764
- let groupAlpha = staged.foldsAlpha ? ctx.state.alpha : 1;
2951
+ let groupAlpha = (staged.foldsAlpha ? ctx.state.alpha : 1) * tint[3];
2765
2952
  const tw = cmd.image.width;
2766
2953
  const th = cmd.image.height;
2767
2954
  for (let s = 0, i = 0; s < count; s++, i += SPRITE_STRIDE) {
2768
2955
  if (batch.wouldOverflow(4)) {
2769
2956
  flushBatch(ctx);
2770
- ({ staged, slot } = stageImage(ctx, cmd.image, sampling));
2771
- groupAlpha = staged.foldsAlpha ? ctx.state.alpha : 1;
2957
+ ({ staged, slot, tint } = stageImage(ctx, cmd.image, sampling));
2958
+ groupAlpha = (staged.foldsAlpha ? ctx.state.alpha : 1) * tint[3];
2772
2959
  }
2773
2960
  const sx = data[i + 4], sy = data[i + 5], sw = data[i + 6], sh = data[i + 7];
2774
2961
  let cut;
@@ -2783,16 +2970,19 @@ function drawSprites(ctx, cmd) {
2783
2970
  (sx + sw) / tw,
2784
2971
  (sy + sh) / th,
2785
2972
  data[i + 8] * groupAlpha,
2786
- slot
2973
+ slot,
2974
+ tint[0],
2975
+ tint[1],
2976
+ tint[2]
2787
2977
  )) {
2788
2978
  cut = enterUncut(ctx);
2789
- ({ staged, slot } = stageImage(ctx, cmd.image, sampling));
2790
- groupAlpha = staged.foldsAlpha ? ctx.state.alpha : 1;
2979
+ ({ staged, slot, tint } = stageImage(ctx, cmd.image, sampling));
2980
+ groupAlpha = (staged.foldsAlpha ? ctx.state.alpha : 1) * tint[3];
2791
2981
  }
2792
2982
  if (cut !== void 0) {
2793
2983
  leaveUncut(ctx, cut);
2794
- ({ staged, slot } = stageImage(ctx, cmd.image, sampling));
2795
- groupAlpha = staged.foldsAlpha ? ctx.state.alpha : 1;
2984
+ ({ staged, slot, tint } = stageImage(ctx, cmd.image, sampling));
2985
+ groupAlpha = (staged.foldsAlpha ? ctx.state.alpha : 1) * tint[3];
2796
2986
  }
2797
2987
  }
2798
2988
  }
@@ -2842,7 +3032,7 @@ function drawGroupWithEffects(ctx, cmd, targets, width, height) {
2842
3032
  flushBatch(ctx);
2843
3033
  gl.disable(gl.BLEND);
2844
3034
  gl.disable(gl.STENCIL_TEST);
2845
- for (const effect of cmd.effects) {
3035
+ for (const effect of cmd.effects ?? []) {
2846
3036
  const program = ctx.ensureProgram?.(effect.program.id) ?? null;
2847
3037
  if (!program) {
2848
3038
  warnOnceUniform(effect.program.id, "(program)");
@@ -2875,15 +3065,21 @@ function drawGroupWithEffects(ctx, cmd, targets, width, height) {
2875
3065
  const composite = ctx.ensureProgram?.(compositeProgramId()) ?? null;
2876
3066
  if (composite) {
2877
3067
  applyClipTest(ctx);
3068
+ const ownBlend = isOwnComposite(cmd.composite);
3069
+ if (ownBlend) applyComposite(gl, cmd.composite);
2878
3070
  drawFullscreenQuad(ctx, composite, front.texture, width, height, void 0, {
2879
3071
  alpha: composited.alpha,
2880
3072
  colorMatrix: composited.colorMatrix
2881
3073
  });
3074
+ if (ownBlend) applyComposite(gl, "source-over");
2882
3075
  }
2883
3076
  targets.release(front);
2884
3077
  }
3078
+ function ownsSurface(cmd) {
3079
+ return cmd.isolate === true || isOwnComposite(cmd.composite) || cmd.effects !== void 0 && cmd.effects.length > 0;
3080
+ }
2885
3081
  function drawGroup(ctx, cmd) {
2886
- if (cmd.effects && cmd.effects.length > 0 && ctx.effectTargets && ctx.deviceWidth && ctx.deviceHeight) {
3082
+ if (ownsSurface(cmd) && ctx.effectTargets && ctx.deviceWidth && ctx.deviceHeight) {
2887
3083
  if (cmd.clip) {
2888
3084
  ctx.state.push({ transform: cmd.transform });
2889
3085
  try {
@@ -2985,8 +3181,7 @@ var EffectTargets = class {
2985
3181
  }
2986
3182
  /**
2987
3183
  * 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.
3184
+ * drops every pooled buffer, borrowed ones included.
2990
3185
  */
2991
3186
  acquire(width, height) {
2992
3187
  if (width !== this.width || height !== this.height) {
@@ -3362,10 +3557,6 @@ var WeaselRenderer = class {
3362
3557
  if (this.canvas) {
3363
3558
  this.canvas.width = Math.round(opts.width * opts.dpr);
3364
3559
  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
3560
  }
3370
3561
  const lossTarget = this.canvas ?? this.gl.canvas ?? null;
3371
3562
  if (lossTarget && typeof lossTarget.addEventListener === "function") {
@@ -3415,12 +3606,11 @@ var WeaselRenderer = class {
3415
3606
  /**
3416
3607
  * Compile a consumer-registered shader program against this renderer's GL context.
3417
3608
  *
3418
- * Call once per renderer after the module-level `registerProgram()`.
3419
- * Throws `ShaderCompileError` if compilation fails.
3609
+ * Optional: a program compiles on the first frame that draws with it. Call
3610
+ * this after the module-level `registerProgram()` to pay for the compile
3611
+ * before that frame, or to catch the `ShaderCompileError` it throws.
3420
3612
  *
3421
3613
  * In dev mode, calling again with the same handle replaces the compiled program.
3422
- *
3423
- * @experimental
3424
3614
  */
3425
3615
  registerProgram(handle) {
3426
3616
  if (this.disposed) return;
@@ -3499,8 +3689,6 @@ var WeaselRenderer = class {
3499
3689
  gl.bindVertexArray(null);
3500
3690
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
3501
3691
  }
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
3692
  /** This renderer's rect in device pixels, top-left origin. A tile rounds its
3505
3693
  * edges rather than its size, so two that share a CSS edge share a device one. */
3506
3694
  deviceRect() {
@@ -3515,6 +3703,8 @@ var WeaselRenderer = class {
3515
3703
  height: Math.round((oy + this.heightCss) * this.dpr) - y
3516
3704
  };
3517
3705
  }
3706
+ /** Viewport and scissor for this frame. Re-applied inside `render()` because
3707
+ * a co-tenant on the same context moves both between our frames. */
3518
3708
  applyTarget() {
3519
3709
  const gl = this.gl;
3520
3710
  const { x, y, width, height } = this.deviceRect();
@@ -3589,14 +3779,9 @@ var WeaselRenderer = class {
3589
3779
  }
3590
3780
  }
3591
3781
  /** 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.
3782
+ * context-loss listeners. Idempotent. The context itself is left alone, so
3783
+ * a caller-owned context can host renderer after renderer without
3784
+ * accumulating anything.
3600
3785
  *
3601
3786
  * A disposed renderer ignores further `render()` and `registerProgram()`
3602
3787
  * calls. */
@@ -3653,7 +3838,7 @@ var WeaselRenderer = class {
3653
3838
  this.stats = { drawCalls: 0, ms: 0, spans: spanStats };
3654
3839
  const gl = this.gl;
3655
3840
  this.meshCache.drainPendingDeletes();
3656
- this.imageCache.drainPendingDeletes();
3841
+ this.imageCache.beginFrame();
3657
3842
  this.textureCache.sweep(textureRegistry);
3658
3843
  this.refreshPrograms();
3659
3844
  resetBakeBudget(this.bakeBudget);
@@ -3739,10 +3924,6 @@ var WeaselRenderer = class {
3739
3924
  if (this.canvas) {
3740
3925
  this.canvas.width = Math.round(dims.width * dims.dpr);
3741
3926
  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
3927
  }
3747
3928
  this.applyTarget();
3748
3929
  }
@@ -3867,6 +4048,7 @@ function cullOne(cmd, m, box) {
3867
4048
  switch (cmd.kind) {
3868
4049
  case "group": {
3869
4050
  if (cmd.effects && cmd.effects.length > 0) return cmd;
4051
+ if (isOwnComposite(cmd.composite)) return cmd;
3870
4052
  const inner = cmd.transform ? mat3.multiply(m, cmd.transform) : m;
3871
4053
  const children = cullList(cmd.children, inner, box);
3872
4054
  if (children === cmd.children) return cmd;
@@ -4070,15 +4252,15 @@ function textCommandFromPose(pose, scale = 1) {
4070
4252
  };
4071
4253
  }
4072
4254
  function runsInPoseFrame(runs, box) {
4073
- if (runs.every((r) => isSolid(r.fill) && isSolid(r.stroke?.paint))) return runs;
4255
+ if (runs.every((r) => isSolid(r.fill?.paint) && isSolid(r.stroke?.paint))) return runs;
4074
4256
  return runs.map((r) => {
4075
4257
  const fill = r.fill && commandFill(r.fill, box);
4076
- const paint = r.stroke?.paint && commandFill(r.stroke.paint, box);
4258
+ const paint = r.stroke?.paint && commandPaint(r.stroke.paint, box);
4077
4259
  return { ...r, fill, stroke: r.stroke && { ...r.stroke, paint: paint || void 0 } };
4078
4260
  });
4079
4261
  }
4080
4262
  function isSolid(paint) {
4081
- return paint == null || (paint.fill ?? "solid") === "solid";
4263
+ return paint == null || isSolidPaint(paint);
4082
4264
  }
4083
4265
 
4084
4266
  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 };