emf-converter 4.13.0 → 4.15.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/browser.js CHANGED
@@ -16513,6 +16513,45 @@ function lerpUniformPathArgb(a, b, t) {
16513
16513
  };
16514
16514
  return (alpha << 24 | ch(16) << 16 | ch(8) << 8 | ch(0)) >>> 0;
16515
16515
  }
16516
+ function pathGradientQuantum(boundary, matrix = IDENTITY) {
16517
+ let x0 = Infinity;
16518
+ let y0 = Infinity;
16519
+ let x1 = -Infinity;
16520
+ let y1 = -Infinity;
16521
+ for (const p of boundary) {
16522
+ const x = matrix[0] * p.x + matrix[2] * p.y + matrix[4];
16523
+ const y = matrix[1] * p.x + matrix[3] * p.y + matrix[5];
16524
+ x0 = Math.min(x0, x);
16525
+ y0 = Math.min(y0, y);
16526
+ x1 = Math.max(x1, x);
16527
+ y1 = Math.max(y1, y);
16528
+ }
16529
+ const steps = Math.ceil(2 * Math.hypot(x1 - x0, y1 - y0));
16530
+ return Number.isFinite(steps) && steps > 0 ? steps : 0;
16531
+ }
16532
+ var TIE_EPSILON = 1e-7;
16533
+ function quantisedPathArgb(surround, centre, t, quantum) {
16534
+ const scaled2 = t * quantum;
16535
+ const tie = Math.abs(scaled2 - Math.floor(scaled2) - 0.5) < TIE_EPSILON;
16536
+ const k = tie ? scaled2 : Math.floor(scaled2 + 0.5);
16537
+ const fromBoundary = t < 0.5;
16538
+ const w = (fromBoundary ? k : quantum - k) / quantum;
16539
+ const aS = surround >>> 24 & 255;
16540
+ const aC = centre >>> 24 & 255;
16541
+ const mix2 = (s, c) => fromBoundary ? s + (c - s) * w : c + (s - c) * w;
16542
+ const alpha = Math.floor(mix2(aS, aC) + 0.5);
16543
+ if (alpha <= 0) return 0;
16544
+ const ch = (shift) => {
16545
+ const p = Math.floor(mix2((surround >>> shift & 255) * aS / 255, (centre >>> shift & 255) * aC / 255) + 0.5);
16546
+ return Math.min(255, Math.max(0, Math.round(p * 255 / alpha)));
16547
+ };
16548
+ return (alpha << 24 | ch(16) << 16 | ch(8) << 8 | ch(0)) >>> 0;
16549
+ }
16550
+ function quantisedPosition(pos, quantum) {
16551
+ const scaled2 = pos * quantum;
16552
+ const tie = Math.abs(scaled2 - Math.floor(scaled2) - 0.5) < TIE_EPSILON;
16553
+ return (tie ? scaled2 : Math.floor(scaled2 + 0.5)) / quantum;
16554
+ }
16516
16555
  function piecewise2(xs, ys, x) {
16517
16556
  const n = Math.min(xs.length, ys.length);
16518
16557
  if (n === 0) {
@@ -16547,11 +16586,13 @@ function stopColorAt(stops, t) {
16547
16586
  }
16548
16587
  return stops[stops.length - 1].argb;
16549
16588
  }
16550
- function pathGradientColorAt(shape, x, y, focusPoint) {
16589
+ function pathGradientColorAt(shape, x, y, focusPoint, quantum) {
16551
16590
  const { center, boundary, boundaryArgb } = shape;
16552
16591
  const n = boundary.length;
16553
16592
  const px = x - center.x;
16554
16593
  const py = y - center.y;
16594
+ const ux = (focusPoint?.x ?? x) - center.x;
16595
+ const uy = (focusPoint?.y ?? y) - center.y;
16555
16596
  let found = -1;
16556
16597
  let bestS = 0;
16557
16598
  let bestU = 0;
@@ -16573,8 +16614,10 @@ function pathGradientColorAt(shape, x, y, focusPoint) {
16573
16614
  continue;
16574
16615
  }
16575
16616
  found = i;
16576
- bestS = Math.max(0, alpha);
16577
- bestU = alpha > 0 ? Math.min(1, Math.max(0, beta / alpha)) : 0;
16617
+ const alphaU = quantum ? (ux * ey - uy * ex) / det : alpha;
16618
+ const betaU = quantum ? (ax * uy - ay * ux) / det : beta;
16619
+ bestS = Math.max(0, alphaU);
16620
+ bestU = alphaU > 0 ? Math.min(1, Math.max(0, betaU / alphaU)) : 0;
16578
16621
  }
16579
16622
  let s = found < 0 ? Infinity : bestS;
16580
16623
  if (shape.focus) {
@@ -16649,7 +16692,8 @@ function pathGradientColorAt(shape, x, y, focusPoint) {
16649
16692
  }
16650
16693
  }
16651
16694
  if (found < 0 || !Number.isFinite(s)) return null;
16652
- const pos = 1 - s;
16695
+ const exactPos = 1 - s;
16696
+ const pos = quantum && quantum > 0 ? quantisedPosition(exactPos, quantum) : exactPos;
16653
16697
  if (shape.preset && shape.preset.positions.length > 0) {
16654
16698
  const { positions, argb } = shape.preset;
16655
16699
  if (pos <= positions[0]) {
@@ -16668,6 +16712,9 @@ function pathGradientColorAt(shape, x, y, focusPoint) {
16668
16712
  const surround = lerpArgb(c0, c1, bestU);
16669
16713
  const factor = shape.blend ? piecewise2(shape.blend.positions, shape.blend.factors, pos) : pos;
16670
16714
  const t = Math.min(1, Math.max(0, factor));
16715
+ if (quantum && quantum > 0 && !shape.blend && boundaryArgb.every((color) => color === boundaryArgb[0])) {
16716
+ return quantisedPathArgb(surround, shape.centerArgb, Math.min(1, Math.max(0, exactPos)), quantum);
16717
+ }
16671
16718
  return surround >>> 24 !== shape.centerArgb >>> 24 && boundaryArgb.every((color) => color === boundaryArgb[0]) ? lerpUniformPathArgb(surround, shape.centerArgb, t) : lerpArgb(surround, shape.centerArgb, t);
16672
16719
  }
16673
16720
  var MAX_TILE = 2048;
@@ -29941,23 +29988,35 @@ function perpendicularVectors(width, dx0, dy0, height = width, matrixPen) {
29941
29988
  const vx = rounded[0], vy = rounded[1];
29942
29989
  return { v: flip ? [-vx, -vy] : [vx, vy], ray: flip ? [-x, -y] : [x, y] };
29943
29990
  }
29944
- function squareExtension(width, dx, dy, scale = 1, height = width, matrix, wholePixelNorm = false, symmetric = wholePixelNorm) {
29991
+ function norm(x, y) {
29992
+ return Math.sqrt(x * x + y * y);
29993
+ }
29994
+ function squareExtension(width, dx, dy, scale = 1, height = width, matrix, wholePixelNorm = false, symmetric = wholePixelNorm, logicalScale = 1) {
29995
+ const slack = 8;
29945
29996
  if (matrix) {
29946
29997
  const det = matrix[0] * matrix[3] - matrix[1] * matrix[2];
29947
29998
  const lx = (matrix[3] * dx - matrix[2] * dy) / det;
29948
29999
  const ly = (-matrix[1] * dx + matrix[0] * dy) / det;
29949
- const l = Math.hypot(lx, ly);
30000
+ let l = Math.hypot(lx, ly);
30001
+ if (wholePixelNorm) {
30002
+ l = norm(Math.floor((lx + slack) / 16) * 16, Math.floor((ly + slack) / 16) * 16);
30003
+ if (l === 0) return [0, 0];
30004
+ }
29950
30005
  const q = width / 2 * scale / l;
29951
30006
  return [Math.floor((matrix[0] * lx + matrix[2] * ly) * q + 0.5), Math.floor((matrix[1] * lx + matrix[3] * ly) * q + 0.5)];
29952
30007
  }
29953
- let len = width === height ? Math.hypot(dx, dy) : Math.hypot(dx, dy * width / height);
29954
- if (wholePixelNorm && width === height) {
29955
- const whole = Math.hypot(Math.floor(dx / 16), Math.floor(dy / 16)) * 16;
29956
- if (whole === 0) return [0, 0];
29957
- len = whole;
29958
- }
30008
+ const [ax, ay] = typeof logicalScale === "number" ? [logicalScale, logicalScale] : logicalScale;
29959
30009
  const r = width / 2 * scale;
29960
30010
  const round = symmetric ? (v) => Math.sign(v) * Math.floor(Math.abs(v) + 0.5) : (v) => Math.floor(v + 0.5);
30011
+ if (wholePixelNorm && (width === height || ax !== ay)) {
30012
+ const lx = dx / ax;
30013
+ const ly = dy / ay;
30014
+ const cut = ax === 1 && ay === 1 ? 0 : slack;
30015
+ const whole = norm(Math.floor((lx + cut) / 16) * 16, Math.floor((ly + cut) / 16) * 16);
30016
+ if (whole === 0) return [0, 0];
30017
+ return [round(lx / whole * r), round(ly * ay / ax / whole * r)];
30018
+ }
30019
+ const len = width === height ? norm(dx, dy) : norm(dx, dy * width / height);
29961
30020
  return [round(dx / len * r), round(dy / len * r)];
29962
30021
  }
29963
30022
  function turnSign(ax, ay, bx, by) {
@@ -29997,7 +30056,7 @@ var Outliner = class {
29997
30056
  const [L, R] = drawVertices(this.pen, d[0], d[1], this.opts.width, this.opts.height, nibMatrix);
29998
30057
  const perpendicular = curve && drawDir ? drawDir : [dx, dy];
29999
30058
  const vectors = perpendicularVectors(this.opts.width, perpendicular[0], perpendicular[1], this.opts.height, nibMatrix ? this.pen : void 0);
30000
- return { dx, dy, L, R, v: vectors.v, vRaw: vectors.ray, e: squareExtension(this.opts.width, perpendicular[0], perpendicular[1], 1, this.opts.height, nibMatrix, !!this.opts.wholePixelDashVectors), pe: [perpendicular[0], perpendicular[1]], curveEnd: !!drawDir, role: drawDir?.[2] ?? 3, tangentLength: drawDir ? drawDir[0] * drawDir[0] + drawDir[1] * drawDir[1] : 0, curve };
30059
+ return { dx, dy, L, R, v: vectors.v, vRaw: vectors.ray, e: squareExtension(this.opts.capWidth ?? this.opts.width, perpendicular[0], perpendicular[1], 1, this.opts.height ?? this.opts.capWidth, nibMatrix, !!(this.opts.wholePixelDashVectors || this.opts.cutToLogicalUnits), void 0, this.opts.logicalScale), pe: [perpendicular[0], perpendicular[1]], curveEnd: !!drawDir, role: drawDir?.[2] ?? 3, tangentLength: drawDir ? drawDir[0] * drawDir[0] + drawDir[1] * drawDir[1] : 0, curve };
30001
30060
  }
30002
30061
  push(p, v) {
30003
30062
  this.pts.push([p[0] + v[0], p[1] + v[1]]);
@@ -30315,8 +30374,8 @@ function dashPieces(P, tangents, curveFlags, pattern, shorten, wholePixelVectors
30315
30374
  const [x1, y1] = P[i + 1];
30316
30375
  const dir = [x1 - x0, y1 - y0];
30317
30376
  const det = matrix ? matrix[0] * matrix[3] - matrix[1] * matrix[2] : 1;
30318
- const real = matrix ? Math.hypot((matrix[3] * dir[0] - matrix[2] * dir[1]) / det, (-matrix[1] * dir[0] + matrix[0] * dir[1]) / det) : metric ? Math.hypot(dir[0] / metric[0], dir[1] / metric[1]) : Math.hypot(dir[0], dir[1]);
30319
- const len = metric || matrix ? real : wholePixelVectors ? Math.hypot(Math.floor(dir[0] / 16), Math.floor(dir[1] / 16)) * 16 : Math.hypot(dir[0], dir[1]);
30377
+ const real = matrix ? Math.hypot((matrix[3] * dir[0] - matrix[2] * dir[1]) / det, (-matrix[1] * dir[0] + matrix[0] * dir[1]) / det) : metric ? Math.hypot(dir[0] / metric[0], dir[1] / metric[1]) : norm(dir[0], dir[1]);
30378
+ const len = metric || matrix ? real : wholePixelVectors ? norm(Math.floor(dir[0] / 16), Math.floor(dir[1] / 16)) * 16 : norm(dir[0], dir[1]);
30320
30379
  if (len === 0) {
30321
30380
  if (on && cur) {
30322
30381
  cur.pts.push(P[i + 1]);
@@ -30424,26 +30483,16 @@ function widenPath(path, opts) {
30424
30483
  const runTangents = closed ? [...dirs, void 0] : dirs;
30425
30484
  const runCurves = closed ? [...curves, !!fig.roundWiden] : curves;
30426
30485
  for (const piece of dashPieces(run, runTangents, runCurves, opts.dashes, shorten, !!opts.wholePixelDashVectors, opts.dashMetric, opts.deviceNib ? opts.matrix : void 0)) {
30427
- const pts = [piece.pts[0]];
30428
- const pdirs = [];
30429
- const pdraws = [];
30430
- const pcurves = [];
30431
- for (let i = 1; i < piece.pts.length; i++) {
30432
- const q = piece.pts[i];
30433
- const last = pts[pts.length - 1];
30434
- if (q[0] !== last[0] || q[1] !== last[1]) {
30435
- pts.push(q);
30436
- pdirs.push(piece.dirs[i - 1]);
30437
- pdraws.push(piece.draws[i - 1]);
30438
- pcurves.push(piece.curves[i - 1]);
30439
- }
30440
- }
30486
+ const pts = piece.pts;
30487
+ const pdirs = piece.dirs;
30488
+ const pdraws = piece.draws;
30489
+ const pcurves = piece.curves;
30441
30490
  outliner.open(pts, pdirs.length > 0 ? pdirs : [piece.dirs[0]], pdirs.length > 0 ? pdraws : [piece.draws[0]], pdirs.length > 0 ? pcurves : [piece.curves[0]], piece.startScale, piece.endScale);
30442
30491
  }
30443
30492
  } else if (closed && P.length >= 3) {
30444
30493
  outliner.closed(P, dirs, curves);
30445
30494
  } else if (closed && P.length === 2) {
30446
- outliner.open([P[0], P[1], P[0]]);
30495
+ outliner.closed(P, dirs, curves);
30447
30496
  } else {
30448
30497
  outliner.open(P, void 0, dirs, curves);
30449
30498
  }
@@ -30706,11 +30755,17 @@ function penWidenOptions(rCtx, opts = {}) {
30706
30755
  join: opts.roundPen ? "round" : opts.rectangle && !state.penExtended ? "miter" : !state.penExtended || joinBits === 0 ? "round" : joinBits === 4096 ? "bevel" : "miter",
30707
30756
  miterLimit: state.miterLimit ?? 10,
30708
30757
  dashes: dashes ? dashes.map((v) => v * 16) : null,
30709
- shortenDashes: (flags & 15) !== 7
30758
+ shortenDashes: (flags & 15) !== 7,
30759
+ // Under a rotation the device vector is converted back to logical units for a square cap's extension.
30760
+ cutToLogicalUnits: !dashes
30710
30761
  };
30711
30762
  }
30763
+ const unrotated = Math.abs(matrix[1]) < 1e-6 && Math.abs(matrix[2]) < 1e-6;
30764
+ const scaled2 = !rCtx.wholeDevicePixels && unrotated && (Math.abs(Math.abs(matrix[0]) - 1) > 1e-6 || Math.abs(Math.abs(matrix[3]) - 1) > 1e-6);
30765
+ const uniform = scaled2 && !ellipse && state.penExtended && (flags & 15) !== 6;
30766
+ const nibWidth = uniform ? Math.max(1, Math.floor(widthPx + 0.5)) : widthPx;
30712
30767
  return {
30713
- width: Math.round((ellipse ? state.penWidth * Math.abs(matrix[0]) : widthPx) * 16),
30768
+ width: Math.round((ellipse ? state.penWidth * Math.abs(matrix[0]) : nibWidth) * 16),
30714
30769
  ...ellipse ? { height: Math.round(state.penWidth * Math.abs(matrix[3]) * 16) } : {},
30715
30770
  cap: opts.roundPen || !state.penExtended || capBits === 0 ? "round" : capBits === 256 ? "square" : "flat",
30716
30771
  join: opts.roundPen ? "round" : opts.rectangle && !state.penExtended ? "miter" : !state.penExtended || joinBits === 0 ? "round" : joinBits === 4096 ? "bevel" : "miter",
@@ -30720,7 +30775,13 @@ function penWidenOptions(rCtx, opts = {}) {
30720
30775
  shortenDashes: (flags & 15) !== 7,
30721
30776
  // One logical unit per device pixel, unrotated: GDI measures dash
30722
30777
  // segments in whole pixels.
30723
- wholePixelDashVectors: Math.abs(matrix[0] - 1) < 1e-6 && Math.abs(matrix[3] - 1) < 1e-6 && Math.abs(matrix[1]) < 1e-6 && Math.abs(matrix[2]) < 1e-6
30778
+ wholePixelDashVectors: Math.abs(matrix[0] - 1) < 1e-6 && Math.abs(matrix[3] - 1) < 1e-6 && Math.abs(matrix[1]) < 1e-6 && Math.abs(matrix[2]) < 1e-6,
30779
+ ...scaled2 && !dashes ? {
30780
+ cutToLogicalUnits: true,
30781
+ logicalScale: ellipse ? [Math.abs(matrix[0]), Math.abs(matrix[3])] : Math.abs(matrix[0]),
30782
+ // The pen rounds to whole pixels, but a square cap extends by half the unrounded width.
30783
+ ...uniform ? { capWidth: state.penWidth * Math.abs(matrix[0]) * 16 } : {}
30784
+ } : {}
30724
30785
  };
30725
30786
  }
30726
30787
  function hatchBackgroundMask(hatch) {
@@ -31211,6 +31272,21 @@ function foldIntoTile(v, origin, size, mirror2, lag) {
31211
31272
  }
31212
31273
  return origin + local;
31213
31274
  }
31275
+ function pixelAligned(boundary, m) {
31276
+ let x0 = Infinity;
31277
+ let y0 = Infinity;
31278
+ let x1 = -Infinity;
31279
+ let y1 = -Infinity;
31280
+ for (const p of boundary) {
31281
+ const x = m[0] * p.x + m[2] * p.y + m[4];
31282
+ const y = m[1] * p.x + m[3] * p.y + m[5];
31283
+ x0 = Math.min(x0, x);
31284
+ y0 = Math.min(y0, y);
31285
+ x1 = Math.max(x1, x);
31286
+ y1 = Math.max(y1, y);
31287
+ }
31288
+ return [x0, y0, x1, y1].every((v) => Math.abs(v - Math.round(v)) < 1e-6);
31289
+ }
31214
31290
  function pathGradientSampler(shape, wrap, device) {
31215
31291
  const box = boundaryBox2(shape.boundary);
31216
31292
  const full = mulMatrix(device, shape.transform ?? IDENTITY4);
@@ -31218,6 +31294,10 @@ function pathGradientSampler(shape, wrap, device) {
31218
31294
  if (!box || !inv2) {
31219
31295
  return null;
31220
31296
  }
31297
+ const anisotropicFocus = !!shape.focus && Math.abs(shape.focus.x - shape.focus.y) > 1e-9;
31298
+ const uniformSurround = shape.boundaryArgb.every((color) => color === shape.boundaryArgb[0]);
31299
+ const smooth = !uniformSurround && !shape.preset || anisotropicFocus || wrap !== "clamp" && !pixelAligned(shape.boundary, full);
31300
+ const quantum = smooth ? 0 : pathGradientQuantum(shape.boundary, full);
31221
31301
  const pxX = Math.hypot(full[0], full[1]);
31222
31302
  const pxY = Math.hypot(full[2], full[3]);
31223
31303
  const lagX = pxX > 0 ? 1 / pxX : 0;
@@ -31237,8 +31317,8 @@ function pathGradientSampler(shape, wrap, device) {
31237
31317
  by = foldIntoTile(by, box.y, box.h, mirrorY, lagY);
31238
31318
  }
31239
31319
  if (wrap === "clamp" && by >= box.y + box.h) continue;
31240
- const focusPoint = bias && shape.focus?.x === 0 ? { x: bx - inv2[0] * bias, y: by - inv2[1] * bias } : void 0;
31241
- const c = pathGradientColorAt(shape, bx, by, focusPoint);
31320
+ const focusPoint = bias ? { x: bx - inv2[0] * bias, y: by - inv2[1] * bias } : void 0;
31321
+ const c = pathGradientColorAt(shape, bx, by, focusPoint, quantum);
31242
31322
  if (c === null) {
31243
31323
  continue;
31244
31324
  }
@@ -32012,8 +32092,14 @@ function handleRoundRect(rCtx, dataOff, recSize) {
32012
32092
  const cornerFixW = inset ? Math.abs(cornerW * devM[0]) * 16 : fixExtent(rCtx, cornerW, 0);
32013
32093
  const cornerFixH = inset ? Math.abs(cornerH * devM[3]) * 16 : fixExtent(rCtx, cornerH, 1);
32014
32094
  const cornerOnFrame = (fix, size, framedSize) => inset && size !== 0 ? Math.floor(Math.min(fix, size) * framedSize / size) : fix;
32015
- const cw = cornerOnFrame(cornerFixW, Math.abs((r - l) * devM[0]) * 16, Math.abs(framed.exx));
32016
- const ch = cornerOnFrame(cornerFixH, Math.abs((b - t) * devM[3]) * 16, Math.abs(framed.eyy));
32095
+ let cw = cornerOnFrame(cornerFixW, Math.abs((r - l) * devM[0]) * 16, Math.abs(framed.exx));
32096
+ let ch = cornerOnFrame(cornerFixH, Math.abs((b - t) * devM[3]) * 16, Math.abs(framed.eyy));
32097
+ if (!inset && rCtx.state.penStyle !== 6 && cornerW !== 0 && cornerH !== 0 && Math.abs(devM[1]) < 1e-9 && Math.abs(devM[2]) < 1e-9 && rCtx.state.penStyle !== 5 && !penIsCosmetic(rCtx) && rCtx.state.worldTransform.every((v, i) => v === [1, 0, 0, 1, 0, 0][i])) {
32098
+ const fullW = (Math.abs(r - l) + 1) * Math.abs(devM[0]) * 16;
32099
+ const fullH = (Math.abs(b - t) + 1) * Math.abs(devM[3]) * 16;
32100
+ cw = Math.floor(Math.min(cornerFixW, fullW) * Math.abs(framed.exx) / fullW);
32101
+ ch = Math.floor(Math.min(cornerFixH, fullH) * Math.abs(framed.eyy) / fullH);
32102
+ }
32017
32103
  const drawRoundRect = inset ? (c) => appendRoundRectPath(
32018
32104
  c,
32019
32105
  Math.min(framed.ax, framed.ax + framed.exx) / 16,
package/dist/browser.mjs CHANGED
@@ -16511,6 +16511,45 @@ function lerpUniformPathArgb(a, b, t) {
16511
16511
  };
16512
16512
  return (alpha << 24 | ch(16) << 16 | ch(8) << 8 | ch(0)) >>> 0;
16513
16513
  }
16514
+ function pathGradientQuantum(boundary, matrix = IDENTITY) {
16515
+ let x0 = Infinity;
16516
+ let y0 = Infinity;
16517
+ let x1 = -Infinity;
16518
+ let y1 = -Infinity;
16519
+ for (const p of boundary) {
16520
+ const x = matrix[0] * p.x + matrix[2] * p.y + matrix[4];
16521
+ const y = matrix[1] * p.x + matrix[3] * p.y + matrix[5];
16522
+ x0 = Math.min(x0, x);
16523
+ y0 = Math.min(y0, y);
16524
+ x1 = Math.max(x1, x);
16525
+ y1 = Math.max(y1, y);
16526
+ }
16527
+ const steps = Math.ceil(2 * Math.hypot(x1 - x0, y1 - y0));
16528
+ return Number.isFinite(steps) && steps > 0 ? steps : 0;
16529
+ }
16530
+ var TIE_EPSILON = 1e-7;
16531
+ function quantisedPathArgb(surround, centre, t, quantum) {
16532
+ const scaled2 = t * quantum;
16533
+ const tie = Math.abs(scaled2 - Math.floor(scaled2) - 0.5) < TIE_EPSILON;
16534
+ const k = tie ? scaled2 : Math.floor(scaled2 + 0.5);
16535
+ const fromBoundary = t < 0.5;
16536
+ const w = (fromBoundary ? k : quantum - k) / quantum;
16537
+ const aS = surround >>> 24 & 255;
16538
+ const aC = centre >>> 24 & 255;
16539
+ const mix2 = (s, c) => fromBoundary ? s + (c - s) * w : c + (s - c) * w;
16540
+ const alpha = Math.floor(mix2(aS, aC) + 0.5);
16541
+ if (alpha <= 0) return 0;
16542
+ const ch = (shift) => {
16543
+ const p = Math.floor(mix2((surround >>> shift & 255) * aS / 255, (centre >>> shift & 255) * aC / 255) + 0.5);
16544
+ return Math.min(255, Math.max(0, Math.round(p * 255 / alpha)));
16545
+ };
16546
+ return (alpha << 24 | ch(16) << 16 | ch(8) << 8 | ch(0)) >>> 0;
16547
+ }
16548
+ function quantisedPosition(pos, quantum) {
16549
+ const scaled2 = pos * quantum;
16550
+ const tie = Math.abs(scaled2 - Math.floor(scaled2) - 0.5) < TIE_EPSILON;
16551
+ return (tie ? scaled2 : Math.floor(scaled2 + 0.5)) / quantum;
16552
+ }
16514
16553
  function piecewise2(xs, ys, x) {
16515
16554
  const n = Math.min(xs.length, ys.length);
16516
16555
  if (n === 0) {
@@ -16545,11 +16584,13 @@ function stopColorAt(stops, t) {
16545
16584
  }
16546
16585
  return stops[stops.length - 1].argb;
16547
16586
  }
16548
- function pathGradientColorAt(shape, x, y, focusPoint) {
16587
+ function pathGradientColorAt(shape, x, y, focusPoint, quantum) {
16549
16588
  const { center, boundary, boundaryArgb } = shape;
16550
16589
  const n = boundary.length;
16551
16590
  const px = x - center.x;
16552
16591
  const py = y - center.y;
16592
+ const ux = (focusPoint?.x ?? x) - center.x;
16593
+ const uy = (focusPoint?.y ?? y) - center.y;
16553
16594
  let found = -1;
16554
16595
  let bestS = 0;
16555
16596
  let bestU = 0;
@@ -16571,8 +16612,10 @@ function pathGradientColorAt(shape, x, y, focusPoint) {
16571
16612
  continue;
16572
16613
  }
16573
16614
  found = i;
16574
- bestS = Math.max(0, alpha);
16575
- bestU = alpha > 0 ? Math.min(1, Math.max(0, beta / alpha)) : 0;
16615
+ const alphaU = quantum ? (ux * ey - uy * ex) / det : alpha;
16616
+ const betaU = quantum ? (ax * uy - ay * ux) / det : beta;
16617
+ bestS = Math.max(0, alphaU);
16618
+ bestU = alphaU > 0 ? Math.min(1, Math.max(0, betaU / alphaU)) : 0;
16576
16619
  }
16577
16620
  let s = found < 0 ? Infinity : bestS;
16578
16621
  if (shape.focus) {
@@ -16647,7 +16690,8 @@ function pathGradientColorAt(shape, x, y, focusPoint) {
16647
16690
  }
16648
16691
  }
16649
16692
  if (found < 0 || !Number.isFinite(s)) return null;
16650
- const pos = 1 - s;
16693
+ const exactPos = 1 - s;
16694
+ const pos = quantum && quantum > 0 ? quantisedPosition(exactPos, quantum) : exactPos;
16651
16695
  if (shape.preset && shape.preset.positions.length > 0) {
16652
16696
  const { positions, argb } = shape.preset;
16653
16697
  if (pos <= positions[0]) {
@@ -16666,6 +16710,9 @@ function pathGradientColorAt(shape, x, y, focusPoint) {
16666
16710
  const surround = lerpArgb(c0, c1, bestU);
16667
16711
  const factor = shape.blend ? piecewise2(shape.blend.positions, shape.blend.factors, pos) : pos;
16668
16712
  const t = Math.min(1, Math.max(0, factor));
16713
+ if (quantum && quantum > 0 && !shape.blend && boundaryArgb.every((color) => color === boundaryArgb[0])) {
16714
+ return quantisedPathArgb(surround, shape.centerArgb, Math.min(1, Math.max(0, exactPos)), quantum);
16715
+ }
16669
16716
  return surround >>> 24 !== shape.centerArgb >>> 24 && boundaryArgb.every((color) => color === boundaryArgb[0]) ? lerpUniformPathArgb(surround, shape.centerArgb, t) : lerpArgb(surround, shape.centerArgb, t);
16670
16717
  }
16671
16718
  var MAX_TILE = 2048;
@@ -29939,23 +29986,35 @@ function perpendicularVectors(width, dx0, dy0, height = width, matrixPen) {
29939
29986
  const vx = rounded[0], vy = rounded[1];
29940
29987
  return { v: flip ? [-vx, -vy] : [vx, vy], ray: flip ? [-x, -y] : [x, y] };
29941
29988
  }
29942
- function squareExtension(width, dx, dy, scale = 1, height = width, matrix, wholePixelNorm = false, symmetric = wholePixelNorm) {
29989
+ function norm(x, y) {
29990
+ return Math.sqrt(x * x + y * y);
29991
+ }
29992
+ function squareExtension(width, dx, dy, scale = 1, height = width, matrix, wholePixelNorm = false, symmetric = wholePixelNorm, logicalScale = 1) {
29993
+ const slack = 8;
29943
29994
  if (matrix) {
29944
29995
  const det = matrix[0] * matrix[3] - matrix[1] * matrix[2];
29945
29996
  const lx = (matrix[3] * dx - matrix[2] * dy) / det;
29946
29997
  const ly = (-matrix[1] * dx + matrix[0] * dy) / det;
29947
- const l = Math.hypot(lx, ly);
29998
+ let l = Math.hypot(lx, ly);
29999
+ if (wholePixelNorm) {
30000
+ l = norm(Math.floor((lx + slack) / 16) * 16, Math.floor((ly + slack) / 16) * 16);
30001
+ if (l === 0) return [0, 0];
30002
+ }
29948
30003
  const q = width / 2 * scale / l;
29949
30004
  return [Math.floor((matrix[0] * lx + matrix[2] * ly) * q + 0.5), Math.floor((matrix[1] * lx + matrix[3] * ly) * q + 0.5)];
29950
30005
  }
29951
- let len = width === height ? Math.hypot(dx, dy) : Math.hypot(dx, dy * width / height);
29952
- if (wholePixelNorm && width === height) {
29953
- const whole = Math.hypot(Math.floor(dx / 16), Math.floor(dy / 16)) * 16;
29954
- if (whole === 0) return [0, 0];
29955
- len = whole;
29956
- }
30006
+ const [ax, ay] = typeof logicalScale === "number" ? [logicalScale, logicalScale] : logicalScale;
29957
30007
  const r = width / 2 * scale;
29958
30008
  const round = symmetric ? (v) => Math.sign(v) * Math.floor(Math.abs(v) + 0.5) : (v) => Math.floor(v + 0.5);
30009
+ if (wholePixelNorm && (width === height || ax !== ay)) {
30010
+ const lx = dx / ax;
30011
+ const ly = dy / ay;
30012
+ const cut = ax === 1 && ay === 1 ? 0 : slack;
30013
+ const whole = norm(Math.floor((lx + cut) / 16) * 16, Math.floor((ly + cut) / 16) * 16);
30014
+ if (whole === 0) return [0, 0];
30015
+ return [round(lx / whole * r), round(ly * ay / ax / whole * r)];
30016
+ }
30017
+ const len = width === height ? norm(dx, dy) : norm(dx, dy * width / height);
29959
30018
  return [round(dx / len * r), round(dy / len * r)];
29960
30019
  }
29961
30020
  function turnSign(ax, ay, bx, by) {
@@ -29995,7 +30054,7 @@ var Outliner = class {
29995
30054
  const [L, R] = drawVertices(this.pen, d[0], d[1], this.opts.width, this.opts.height, nibMatrix);
29996
30055
  const perpendicular = curve && drawDir ? drawDir : [dx, dy];
29997
30056
  const vectors = perpendicularVectors(this.opts.width, perpendicular[0], perpendicular[1], this.opts.height, nibMatrix ? this.pen : void 0);
29998
- return { dx, dy, L, R, v: vectors.v, vRaw: vectors.ray, e: squareExtension(this.opts.width, perpendicular[0], perpendicular[1], 1, this.opts.height, nibMatrix, !!this.opts.wholePixelDashVectors), pe: [perpendicular[0], perpendicular[1]], curveEnd: !!drawDir, role: drawDir?.[2] ?? 3, tangentLength: drawDir ? drawDir[0] * drawDir[0] + drawDir[1] * drawDir[1] : 0, curve };
30057
+ return { dx, dy, L, R, v: vectors.v, vRaw: vectors.ray, e: squareExtension(this.opts.capWidth ?? this.opts.width, perpendicular[0], perpendicular[1], 1, this.opts.height ?? this.opts.capWidth, nibMatrix, !!(this.opts.wholePixelDashVectors || this.opts.cutToLogicalUnits), void 0, this.opts.logicalScale), pe: [perpendicular[0], perpendicular[1]], curveEnd: !!drawDir, role: drawDir?.[2] ?? 3, tangentLength: drawDir ? drawDir[0] * drawDir[0] + drawDir[1] * drawDir[1] : 0, curve };
29999
30058
  }
30000
30059
  push(p, v) {
30001
30060
  this.pts.push([p[0] + v[0], p[1] + v[1]]);
@@ -30313,8 +30372,8 @@ function dashPieces(P, tangents, curveFlags, pattern, shorten, wholePixelVectors
30313
30372
  const [x1, y1] = P[i + 1];
30314
30373
  const dir = [x1 - x0, y1 - y0];
30315
30374
  const det = matrix ? matrix[0] * matrix[3] - matrix[1] * matrix[2] : 1;
30316
- const real = matrix ? Math.hypot((matrix[3] * dir[0] - matrix[2] * dir[1]) / det, (-matrix[1] * dir[0] + matrix[0] * dir[1]) / det) : metric ? Math.hypot(dir[0] / metric[0], dir[1] / metric[1]) : Math.hypot(dir[0], dir[1]);
30317
- const len = metric || matrix ? real : wholePixelVectors ? Math.hypot(Math.floor(dir[0] / 16), Math.floor(dir[1] / 16)) * 16 : Math.hypot(dir[0], dir[1]);
30375
+ const real = matrix ? Math.hypot((matrix[3] * dir[0] - matrix[2] * dir[1]) / det, (-matrix[1] * dir[0] + matrix[0] * dir[1]) / det) : metric ? Math.hypot(dir[0] / metric[0], dir[1] / metric[1]) : norm(dir[0], dir[1]);
30376
+ const len = metric || matrix ? real : wholePixelVectors ? norm(Math.floor(dir[0] / 16), Math.floor(dir[1] / 16)) * 16 : norm(dir[0], dir[1]);
30318
30377
  if (len === 0) {
30319
30378
  if (on && cur) {
30320
30379
  cur.pts.push(P[i + 1]);
@@ -30422,26 +30481,16 @@ function widenPath(path, opts) {
30422
30481
  const runTangents = closed ? [...dirs, void 0] : dirs;
30423
30482
  const runCurves = closed ? [...curves, !!fig.roundWiden] : curves;
30424
30483
  for (const piece of dashPieces(run, runTangents, runCurves, opts.dashes, shorten, !!opts.wholePixelDashVectors, opts.dashMetric, opts.deviceNib ? opts.matrix : void 0)) {
30425
- const pts = [piece.pts[0]];
30426
- const pdirs = [];
30427
- const pdraws = [];
30428
- const pcurves = [];
30429
- for (let i = 1; i < piece.pts.length; i++) {
30430
- const q = piece.pts[i];
30431
- const last = pts[pts.length - 1];
30432
- if (q[0] !== last[0] || q[1] !== last[1]) {
30433
- pts.push(q);
30434
- pdirs.push(piece.dirs[i - 1]);
30435
- pdraws.push(piece.draws[i - 1]);
30436
- pcurves.push(piece.curves[i - 1]);
30437
- }
30438
- }
30484
+ const pts = piece.pts;
30485
+ const pdirs = piece.dirs;
30486
+ const pdraws = piece.draws;
30487
+ const pcurves = piece.curves;
30439
30488
  outliner.open(pts, pdirs.length > 0 ? pdirs : [piece.dirs[0]], pdirs.length > 0 ? pdraws : [piece.draws[0]], pdirs.length > 0 ? pcurves : [piece.curves[0]], piece.startScale, piece.endScale);
30440
30489
  }
30441
30490
  } else if (closed && P.length >= 3) {
30442
30491
  outliner.closed(P, dirs, curves);
30443
30492
  } else if (closed && P.length === 2) {
30444
- outliner.open([P[0], P[1], P[0]]);
30493
+ outliner.closed(P, dirs, curves);
30445
30494
  } else {
30446
30495
  outliner.open(P, void 0, dirs, curves);
30447
30496
  }
@@ -30704,11 +30753,17 @@ function penWidenOptions(rCtx, opts = {}) {
30704
30753
  join: opts.roundPen ? "round" : opts.rectangle && !state.penExtended ? "miter" : !state.penExtended || joinBits === 0 ? "round" : joinBits === 4096 ? "bevel" : "miter",
30705
30754
  miterLimit: state.miterLimit ?? 10,
30706
30755
  dashes: dashes ? dashes.map((v) => v * 16) : null,
30707
- shortenDashes: (flags & 15) !== 7
30756
+ shortenDashes: (flags & 15) !== 7,
30757
+ // Under a rotation the device vector is converted back to logical units for a square cap's extension.
30758
+ cutToLogicalUnits: !dashes
30708
30759
  };
30709
30760
  }
30761
+ const unrotated = Math.abs(matrix[1]) < 1e-6 && Math.abs(matrix[2]) < 1e-6;
30762
+ const scaled2 = !rCtx.wholeDevicePixels && unrotated && (Math.abs(Math.abs(matrix[0]) - 1) > 1e-6 || Math.abs(Math.abs(matrix[3]) - 1) > 1e-6);
30763
+ const uniform = scaled2 && !ellipse && state.penExtended && (flags & 15) !== 6;
30764
+ const nibWidth = uniform ? Math.max(1, Math.floor(widthPx + 0.5)) : widthPx;
30710
30765
  return {
30711
- width: Math.round((ellipse ? state.penWidth * Math.abs(matrix[0]) : widthPx) * 16),
30766
+ width: Math.round((ellipse ? state.penWidth * Math.abs(matrix[0]) : nibWidth) * 16),
30712
30767
  ...ellipse ? { height: Math.round(state.penWidth * Math.abs(matrix[3]) * 16) } : {},
30713
30768
  cap: opts.roundPen || !state.penExtended || capBits === 0 ? "round" : capBits === 256 ? "square" : "flat",
30714
30769
  join: opts.roundPen ? "round" : opts.rectangle && !state.penExtended ? "miter" : !state.penExtended || joinBits === 0 ? "round" : joinBits === 4096 ? "bevel" : "miter",
@@ -30718,7 +30773,13 @@ function penWidenOptions(rCtx, opts = {}) {
30718
30773
  shortenDashes: (flags & 15) !== 7,
30719
30774
  // One logical unit per device pixel, unrotated: GDI measures dash
30720
30775
  // segments in whole pixels.
30721
- wholePixelDashVectors: Math.abs(matrix[0] - 1) < 1e-6 && Math.abs(matrix[3] - 1) < 1e-6 && Math.abs(matrix[1]) < 1e-6 && Math.abs(matrix[2]) < 1e-6
30776
+ wholePixelDashVectors: Math.abs(matrix[0] - 1) < 1e-6 && Math.abs(matrix[3] - 1) < 1e-6 && Math.abs(matrix[1]) < 1e-6 && Math.abs(matrix[2]) < 1e-6,
30777
+ ...scaled2 && !dashes ? {
30778
+ cutToLogicalUnits: true,
30779
+ logicalScale: ellipse ? [Math.abs(matrix[0]), Math.abs(matrix[3])] : Math.abs(matrix[0]),
30780
+ // The pen rounds to whole pixels, but a square cap extends by half the unrounded width.
30781
+ ...uniform ? { capWidth: state.penWidth * Math.abs(matrix[0]) * 16 } : {}
30782
+ } : {}
30722
30783
  };
30723
30784
  }
30724
30785
  function hatchBackgroundMask(hatch) {
@@ -31209,6 +31270,21 @@ function foldIntoTile(v, origin, size, mirror2, lag) {
31209
31270
  }
31210
31271
  return origin + local;
31211
31272
  }
31273
+ function pixelAligned(boundary, m) {
31274
+ let x0 = Infinity;
31275
+ let y0 = Infinity;
31276
+ let x1 = -Infinity;
31277
+ let y1 = -Infinity;
31278
+ for (const p of boundary) {
31279
+ const x = m[0] * p.x + m[2] * p.y + m[4];
31280
+ const y = m[1] * p.x + m[3] * p.y + m[5];
31281
+ x0 = Math.min(x0, x);
31282
+ y0 = Math.min(y0, y);
31283
+ x1 = Math.max(x1, x);
31284
+ y1 = Math.max(y1, y);
31285
+ }
31286
+ return [x0, y0, x1, y1].every((v) => Math.abs(v - Math.round(v)) < 1e-6);
31287
+ }
31212
31288
  function pathGradientSampler(shape, wrap, device) {
31213
31289
  const box = boundaryBox2(shape.boundary);
31214
31290
  const full = mulMatrix(device, shape.transform ?? IDENTITY4);
@@ -31216,6 +31292,10 @@ function pathGradientSampler(shape, wrap, device) {
31216
31292
  if (!box || !inv2) {
31217
31293
  return null;
31218
31294
  }
31295
+ const anisotropicFocus = !!shape.focus && Math.abs(shape.focus.x - shape.focus.y) > 1e-9;
31296
+ const uniformSurround = shape.boundaryArgb.every((color) => color === shape.boundaryArgb[0]);
31297
+ const smooth = !uniformSurround && !shape.preset || anisotropicFocus || wrap !== "clamp" && !pixelAligned(shape.boundary, full);
31298
+ const quantum = smooth ? 0 : pathGradientQuantum(shape.boundary, full);
31219
31299
  const pxX = Math.hypot(full[0], full[1]);
31220
31300
  const pxY = Math.hypot(full[2], full[3]);
31221
31301
  const lagX = pxX > 0 ? 1 / pxX : 0;
@@ -31235,8 +31315,8 @@ function pathGradientSampler(shape, wrap, device) {
31235
31315
  by = foldIntoTile(by, box.y, box.h, mirrorY, lagY);
31236
31316
  }
31237
31317
  if (wrap === "clamp" && by >= box.y + box.h) continue;
31238
- const focusPoint = bias && shape.focus?.x === 0 ? { x: bx - inv2[0] * bias, y: by - inv2[1] * bias } : void 0;
31239
- const c = pathGradientColorAt(shape, bx, by, focusPoint);
31318
+ const focusPoint = bias ? { x: bx - inv2[0] * bias, y: by - inv2[1] * bias } : void 0;
31319
+ const c = pathGradientColorAt(shape, bx, by, focusPoint, quantum);
31240
31320
  if (c === null) {
31241
31321
  continue;
31242
31322
  }
@@ -32010,8 +32090,14 @@ function handleRoundRect(rCtx, dataOff, recSize) {
32010
32090
  const cornerFixW = inset ? Math.abs(cornerW * devM[0]) * 16 : fixExtent(rCtx, cornerW, 0);
32011
32091
  const cornerFixH = inset ? Math.abs(cornerH * devM[3]) * 16 : fixExtent(rCtx, cornerH, 1);
32012
32092
  const cornerOnFrame = (fix, size, framedSize) => inset && size !== 0 ? Math.floor(Math.min(fix, size) * framedSize / size) : fix;
32013
- const cw = cornerOnFrame(cornerFixW, Math.abs((r - l) * devM[0]) * 16, Math.abs(framed.exx));
32014
- const ch = cornerOnFrame(cornerFixH, Math.abs((b - t) * devM[3]) * 16, Math.abs(framed.eyy));
32093
+ let cw = cornerOnFrame(cornerFixW, Math.abs((r - l) * devM[0]) * 16, Math.abs(framed.exx));
32094
+ let ch = cornerOnFrame(cornerFixH, Math.abs((b - t) * devM[3]) * 16, Math.abs(framed.eyy));
32095
+ if (!inset && rCtx.state.penStyle !== 6 && cornerW !== 0 && cornerH !== 0 && Math.abs(devM[1]) < 1e-9 && Math.abs(devM[2]) < 1e-9 && rCtx.state.penStyle !== 5 && !penIsCosmetic(rCtx) && rCtx.state.worldTransform.every((v, i) => v === [1, 0, 0, 1, 0, 0][i])) {
32096
+ const fullW = (Math.abs(r - l) + 1) * Math.abs(devM[0]) * 16;
32097
+ const fullH = (Math.abs(b - t) + 1) * Math.abs(devM[3]) * 16;
32098
+ cw = Math.floor(Math.min(cornerFixW, fullW) * Math.abs(framed.exx) / fullW);
32099
+ ch = Math.floor(Math.min(cornerFixH, fullH) * Math.abs(framed.eyy) / fullH);
32100
+ }
32015
32101
  const drawRoundRect = inset ? (c) => appendRoundRectPath(
32016
32102
  c,
32017
32103
  Math.min(framed.ax, framed.ax + framed.exx) / 16,
package/dist/index.js CHANGED
@@ -16514,6 +16514,45 @@ function lerpUniformPathArgb(a, b, t) {
16514
16514
  };
16515
16515
  return (alpha << 24 | ch(16) << 16 | ch(8) << 8 | ch(0)) >>> 0;
16516
16516
  }
16517
+ function pathGradientQuantum(boundary, matrix = IDENTITY) {
16518
+ let x0 = Infinity;
16519
+ let y0 = Infinity;
16520
+ let x1 = -Infinity;
16521
+ let y1 = -Infinity;
16522
+ for (const p of boundary) {
16523
+ const x = matrix[0] * p.x + matrix[2] * p.y + matrix[4];
16524
+ const y = matrix[1] * p.x + matrix[3] * p.y + matrix[5];
16525
+ x0 = Math.min(x0, x);
16526
+ y0 = Math.min(y0, y);
16527
+ x1 = Math.max(x1, x);
16528
+ y1 = Math.max(y1, y);
16529
+ }
16530
+ const steps = Math.ceil(2 * Math.hypot(x1 - x0, y1 - y0));
16531
+ return Number.isFinite(steps) && steps > 0 ? steps : 0;
16532
+ }
16533
+ var TIE_EPSILON = 1e-7;
16534
+ function quantisedPathArgb(surround, centre, t, quantum) {
16535
+ const scaled2 = t * quantum;
16536
+ const tie = Math.abs(scaled2 - Math.floor(scaled2) - 0.5) < TIE_EPSILON;
16537
+ const k = tie ? scaled2 : Math.floor(scaled2 + 0.5);
16538
+ const fromBoundary = t < 0.5;
16539
+ const w = (fromBoundary ? k : quantum - k) / quantum;
16540
+ const aS = surround >>> 24 & 255;
16541
+ const aC = centre >>> 24 & 255;
16542
+ const mix2 = (s, c) => fromBoundary ? s + (c - s) * w : c + (s - c) * w;
16543
+ const alpha = Math.floor(mix2(aS, aC) + 0.5);
16544
+ if (alpha <= 0) return 0;
16545
+ const ch = (shift) => {
16546
+ const p = Math.floor(mix2((surround >>> shift & 255) * aS / 255, (centre >>> shift & 255) * aC / 255) + 0.5);
16547
+ return Math.min(255, Math.max(0, Math.round(p * 255 / alpha)));
16548
+ };
16549
+ return (alpha << 24 | ch(16) << 16 | ch(8) << 8 | ch(0)) >>> 0;
16550
+ }
16551
+ function quantisedPosition(pos, quantum) {
16552
+ const scaled2 = pos * quantum;
16553
+ const tie = Math.abs(scaled2 - Math.floor(scaled2) - 0.5) < TIE_EPSILON;
16554
+ return (tie ? scaled2 : Math.floor(scaled2 + 0.5)) / quantum;
16555
+ }
16517
16556
  function piecewise2(xs, ys, x) {
16518
16557
  const n = Math.min(xs.length, ys.length);
16519
16558
  if (n === 0) {
@@ -16548,11 +16587,13 @@ function stopColorAt(stops, t) {
16548
16587
  }
16549
16588
  return stops[stops.length - 1].argb;
16550
16589
  }
16551
- function pathGradientColorAt(shape, x, y, focusPoint) {
16590
+ function pathGradientColorAt(shape, x, y, focusPoint, quantum) {
16552
16591
  const { center, boundary, boundaryArgb } = shape;
16553
16592
  const n = boundary.length;
16554
16593
  const px = x - center.x;
16555
16594
  const py = y - center.y;
16595
+ const ux = (focusPoint?.x ?? x) - center.x;
16596
+ const uy = (focusPoint?.y ?? y) - center.y;
16556
16597
  let found = -1;
16557
16598
  let bestS = 0;
16558
16599
  let bestU = 0;
@@ -16574,8 +16615,10 @@ function pathGradientColorAt(shape, x, y, focusPoint) {
16574
16615
  continue;
16575
16616
  }
16576
16617
  found = i;
16577
- bestS = Math.max(0, alpha);
16578
- bestU = alpha > 0 ? Math.min(1, Math.max(0, beta / alpha)) : 0;
16618
+ const alphaU = quantum ? (ux * ey - uy * ex) / det : alpha;
16619
+ const betaU = quantum ? (ax * uy - ay * ux) / det : beta;
16620
+ bestS = Math.max(0, alphaU);
16621
+ bestU = alphaU > 0 ? Math.min(1, Math.max(0, betaU / alphaU)) : 0;
16579
16622
  }
16580
16623
  let s = found < 0 ? Infinity : bestS;
16581
16624
  if (shape.focus) {
@@ -16650,7 +16693,8 @@ function pathGradientColorAt(shape, x, y, focusPoint) {
16650
16693
  }
16651
16694
  }
16652
16695
  if (found < 0 || !Number.isFinite(s)) return null;
16653
- const pos = 1 - s;
16696
+ const exactPos = 1 - s;
16697
+ const pos = quantum && quantum > 0 ? quantisedPosition(exactPos, quantum) : exactPos;
16654
16698
  if (shape.preset && shape.preset.positions.length > 0) {
16655
16699
  const { positions, argb } = shape.preset;
16656
16700
  if (pos <= positions[0]) {
@@ -16669,6 +16713,9 @@ function pathGradientColorAt(shape, x, y, focusPoint) {
16669
16713
  const surround = lerpArgb(c0, c1, bestU);
16670
16714
  const factor = shape.blend ? piecewise2(shape.blend.positions, shape.blend.factors, pos) : pos;
16671
16715
  const t = Math.min(1, Math.max(0, factor));
16716
+ if (quantum && quantum > 0 && !shape.blend && boundaryArgb.every((color) => color === boundaryArgb[0])) {
16717
+ return quantisedPathArgb(surround, shape.centerArgb, Math.min(1, Math.max(0, exactPos)), quantum);
16718
+ }
16672
16719
  return surround >>> 24 !== shape.centerArgb >>> 24 && boundaryArgb.every((color) => color === boundaryArgb[0]) ? lerpUniformPathArgb(surround, shape.centerArgb, t) : lerpArgb(surround, shape.centerArgb, t);
16673
16720
  }
16674
16721
  var MAX_TILE = 2048;
@@ -29942,23 +29989,35 @@ function perpendicularVectors(width, dx0, dy0, height = width, matrixPen) {
29942
29989
  const vx = rounded[0], vy = rounded[1];
29943
29990
  return { v: flip ? [-vx, -vy] : [vx, vy], ray: flip ? [-x, -y] : [x, y] };
29944
29991
  }
29945
- function squareExtension(width, dx, dy, scale = 1, height = width, matrix, wholePixelNorm = false, symmetric = wholePixelNorm) {
29992
+ function norm(x, y) {
29993
+ return Math.sqrt(x * x + y * y);
29994
+ }
29995
+ function squareExtension(width, dx, dy, scale = 1, height = width, matrix, wholePixelNorm = false, symmetric = wholePixelNorm, logicalScale = 1) {
29996
+ const slack = 8;
29946
29997
  if (matrix) {
29947
29998
  const det = matrix[0] * matrix[3] - matrix[1] * matrix[2];
29948
29999
  const lx = (matrix[3] * dx - matrix[2] * dy) / det;
29949
30000
  const ly = (-matrix[1] * dx + matrix[0] * dy) / det;
29950
- const l = Math.hypot(lx, ly);
30001
+ let l = Math.hypot(lx, ly);
30002
+ if (wholePixelNorm) {
30003
+ l = norm(Math.floor((lx + slack) / 16) * 16, Math.floor((ly + slack) / 16) * 16);
30004
+ if (l === 0) return [0, 0];
30005
+ }
29951
30006
  const q = width / 2 * scale / l;
29952
30007
  return [Math.floor((matrix[0] * lx + matrix[2] * ly) * q + 0.5), Math.floor((matrix[1] * lx + matrix[3] * ly) * q + 0.5)];
29953
30008
  }
29954
- let len = width === height ? Math.hypot(dx, dy) : Math.hypot(dx, dy * width / height);
29955
- if (wholePixelNorm && width === height) {
29956
- const whole = Math.hypot(Math.floor(dx / 16), Math.floor(dy / 16)) * 16;
29957
- if (whole === 0) return [0, 0];
29958
- len = whole;
29959
- }
30009
+ const [ax, ay] = typeof logicalScale === "number" ? [logicalScale, logicalScale] : logicalScale;
29960
30010
  const r = width / 2 * scale;
29961
30011
  const round = symmetric ? (v) => Math.sign(v) * Math.floor(Math.abs(v) + 0.5) : (v) => Math.floor(v + 0.5);
30012
+ if (wholePixelNorm && (width === height || ax !== ay)) {
30013
+ const lx = dx / ax;
30014
+ const ly = dy / ay;
30015
+ const cut = ax === 1 && ay === 1 ? 0 : slack;
30016
+ const whole = norm(Math.floor((lx + cut) / 16) * 16, Math.floor((ly + cut) / 16) * 16);
30017
+ if (whole === 0) return [0, 0];
30018
+ return [round(lx / whole * r), round(ly * ay / ax / whole * r)];
30019
+ }
30020
+ const len = width === height ? norm(dx, dy) : norm(dx, dy * width / height);
29962
30021
  return [round(dx / len * r), round(dy / len * r)];
29963
30022
  }
29964
30023
  function turnSign(ax, ay, bx, by) {
@@ -29998,7 +30057,7 @@ var Outliner = class {
29998
30057
  const [L, R] = drawVertices(this.pen, d[0], d[1], this.opts.width, this.opts.height, nibMatrix);
29999
30058
  const perpendicular = curve && drawDir ? drawDir : [dx, dy];
30000
30059
  const vectors = perpendicularVectors(this.opts.width, perpendicular[0], perpendicular[1], this.opts.height, nibMatrix ? this.pen : void 0);
30001
- return { dx, dy, L, R, v: vectors.v, vRaw: vectors.ray, e: squareExtension(this.opts.width, perpendicular[0], perpendicular[1], 1, this.opts.height, nibMatrix, !!this.opts.wholePixelDashVectors), pe: [perpendicular[0], perpendicular[1]], curveEnd: !!drawDir, role: drawDir?.[2] ?? 3, tangentLength: drawDir ? drawDir[0] * drawDir[0] + drawDir[1] * drawDir[1] : 0, curve };
30060
+ return { dx, dy, L, R, v: vectors.v, vRaw: vectors.ray, e: squareExtension(this.opts.capWidth ?? this.opts.width, perpendicular[0], perpendicular[1], 1, this.opts.height ?? this.opts.capWidth, nibMatrix, !!(this.opts.wholePixelDashVectors || this.opts.cutToLogicalUnits), void 0, this.opts.logicalScale), pe: [perpendicular[0], perpendicular[1]], curveEnd: !!drawDir, role: drawDir?.[2] ?? 3, tangentLength: drawDir ? drawDir[0] * drawDir[0] + drawDir[1] * drawDir[1] : 0, curve };
30002
30061
  }
30003
30062
  push(p, v) {
30004
30063
  this.pts.push([p[0] + v[0], p[1] + v[1]]);
@@ -30316,8 +30375,8 @@ function dashPieces(P, tangents, curveFlags, pattern, shorten, wholePixelVectors
30316
30375
  const [x1, y1] = P[i + 1];
30317
30376
  const dir = [x1 - x0, y1 - y0];
30318
30377
  const det = matrix ? matrix[0] * matrix[3] - matrix[1] * matrix[2] : 1;
30319
- const real = matrix ? Math.hypot((matrix[3] * dir[0] - matrix[2] * dir[1]) / det, (-matrix[1] * dir[0] + matrix[0] * dir[1]) / det) : metric ? Math.hypot(dir[0] / metric[0], dir[1] / metric[1]) : Math.hypot(dir[0], dir[1]);
30320
- const len = metric || matrix ? real : wholePixelVectors ? Math.hypot(Math.floor(dir[0] / 16), Math.floor(dir[1] / 16)) * 16 : Math.hypot(dir[0], dir[1]);
30378
+ const real = matrix ? Math.hypot((matrix[3] * dir[0] - matrix[2] * dir[1]) / det, (-matrix[1] * dir[0] + matrix[0] * dir[1]) / det) : metric ? Math.hypot(dir[0] / metric[0], dir[1] / metric[1]) : norm(dir[0], dir[1]);
30379
+ const len = metric || matrix ? real : wholePixelVectors ? norm(Math.floor(dir[0] / 16), Math.floor(dir[1] / 16)) * 16 : norm(dir[0], dir[1]);
30321
30380
  if (len === 0) {
30322
30381
  if (on && cur) {
30323
30382
  cur.pts.push(P[i + 1]);
@@ -30425,26 +30484,16 @@ function widenPath(path, opts) {
30425
30484
  const runTangents = closed ? [...dirs, void 0] : dirs;
30426
30485
  const runCurves = closed ? [...curves, !!fig.roundWiden] : curves;
30427
30486
  for (const piece of dashPieces(run, runTangents, runCurves, opts.dashes, shorten, !!opts.wholePixelDashVectors, opts.dashMetric, opts.deviceNib ? opts.matrix : void 0)) {
30428
- const pts = [piece.pts[0]];
30429
- const pdirs = [];
30430
- const pdraws = [];
30431
- const pcurves = [];
30432
- for (let i = 1; i < piece.pts.length; i++) {
30433
- const q = piece.pts[i];
30434
- const last = pts[pts.length - 1];
30435
- if (q[0] !== last[0] || q[1] !== last[1]) {
30436
- pts.push(q);
30437
- pdirs.push(piece.dirs[i - 1]);
30438
- pdraws.push(piece.draws[i - 1]);
30439
- pcurves.push(piece.curves[i - 1]);
30440
- }
30441
- }
30487
+ const pts = piece.pts;
30488
+ const pdirs = piece.dirs;
30489
+ const pdraws = piece.draws;
30490
+ const pcurves = piece.curves;
30442
30491
  outliner.open(pts, pdirs.length > 0 ? pdirs : [piece.dirs[0]], pdirs.length > 0 ? pdraws : [piece.draws[0]], pdirs.length > 0 ? pcurves : [piece.curves[0]], piece.startScale, piece.endScale);
30443
30492
  }
30444
30493
  } else if (closed && P.length >= 3) {
30445
30494
  outliner.closed(P, dirs, curves);
30446
30495
  } else if (closed && P.length === 2) {
30447
- outliner.open([P[0], P[1], P[0]]);
30496
+ outliner.closed(P, dirs, curves);
30448
30497
  } else {
30449
30498
  outliner.open(P, void 0, dirs, curves);
30450
30499
  }
@@ -30707,11 +30756,17 @@ function penWidenOptions(rCtx, opts = {}) {
30707
30756
  join: opts.roundPen ? "round" : opts.rectangle && !state.penExtended ? "miter" : !state.penExtended || joinBits === 0 ? "round" : joinBits === 4096 ? "bevel" : "miter",
30708
30757
  miterLimit: state.miterLimit ?? 10,
30709
30758
  dashes: dashes ? dashes.map((v) => v * 16) : null,
30710
- shortenDashes: (flags & 15) !== 7
30759
+ shortenDashes: (flags & 15) !== 7,
30760
+ // Under a rotation the device vector is converted back to logical units for a square cap's extension.
30761
+ cutToLogicalUnits: !dashes
30711
30762
  };
30712
30763
  }
30764
+ const unrotated = Math.abs(matrix[1]) < 1e-6 && Math.abs(matrix[2]) < 1e-6;
30765
+ const scaled2 = !rCtx.wholeDevicePixels && unrotated && (Math.abs(Math.abs(matrix[0]) - 1) > 1e-6 || Math.abs(Math.abs(matrix[3]) - 1) > 1e-6);
30766
+ const uniform = scaled2 && !ellipse && state.penExtended && (flags & 15) !== 6;
30767
+ const nibWidth = uniform ? Math.max(1, Math.floor(widthPx + 0.5)) : widthPx;
30713
30768
  return {
30714
- width: Math.round((ellipse ? state.penWidth * Math.abs(matrix[0]) : widthPx) * 16),
30769
+ width: Math.round((ellipse ? state.penWidth * Math.abs(matrix[0]) : nibWidth) * 16),
30715
30770
  ...ellipse ? { height: Math.round(state.penWidth * Math.abs(matrix[3]) * 16) } : {},
30716
30771
  cap: opts.roundPen || !state.penExtended || capBits === 0 ? "round" : capBits === 256 ? "square" : "flat",
30717
30772
  join: opts.roundPen ? "round" : opts.rectangle && !state.penExtended ? "miter" : !state.penExtended || joinBits === 0 ? "round" : joinBits === 4096 ? "bevel" : "miter",
@@ -30721,7 +30776,13 @@ function penWidenOptions(rCtx, opts = {}) {
30721
30776
  shortenDashes: (flags & 15) !== 7,
30722
30777
  // One logical unit per device pixel, unrotated: GDI measures dash
30723
30778
  // segments in whole pixels.
30724
- wholePixelDashVectors: Math.abs(matrix[0] - 1) < 1e-6 && Math.abs(matrix[3] - 1) < 1e-6 && Math.abs(matrix[1]) < 1e-6 && Math.abs(matrix[2]) < 1e-6
30779
+ wholePixelDashVectors: Math.abs(matrix[0] - 1) < 1e-6 && Math.abs(matrix[3] - 1) < 1e-6 && Math.abs(matrix[1]) < 1e-6 && Math.abs(matrix[2]) < 1e-6,
30780
+ ...scaled2 && !dashes ? {
30781
+ cutToLogicalUnits: true,
30782
+ logicalScale: ellipse ? [Math.abs(matrix[0]), Math.abs(matrix[3])] : Math.abs(matrix[0]),
30783
+ // The pen rounds to whole pixels, but a square cap extends by half the unrounded width.
30784
+ ...uniform ? { capWidth: state.penWidth * Math.abs(matrix[0]) * 16 } : {}
30785
+ } : {}
30725
30786
  };
30726
30787
  }
30727
30788
  function hatchBackgroundMask(hatch) {
@@ -31212,6 +31273,21 @@ function foldIntoTile(v, origin, size, mirror2, lag) {
31212
31273
  }
31213
31274
  return origin + local;
31214
31275
  }
31276
+ function pixelAligned(boundary, m) {
31277
+ let x0 = Infinity;
31278
+ let y0 = Infinity;
31279
+ let x1 = -Infinity;
31280
+ let y1 = -Infinity;
31281
+ for (const p of boundary) {
31282
+ const x = m[0] * p.x + m[2] * p.y + m[4];
31283
+ const y = m[1] * p.x + m[3] * p.y + m[5];
31284
+ x0 = Math.min(x0, x);
31285
+ y0 = Math.min(y0, y);
31286
+ x1 = Math.max(x1, x);
31287
+ y1 = Math.max(y1, y);
31288
+ }
31289
+ return [x0, y0, x1, y1].every((v) => Math.abs(v - Math.round(v)) < 1e-6);
31290
+ }
31215
31291
  function pathGradientSampler(shape, wrap, device) {
31216
31292
  const box = boundaryBox2(shape.boundary);
31217
31293
  const full = mulMatrix(device, shape.transform ?? IDENTITY4);
@@ -31219,6 +31295,10 @@ function pathGradientSampler(shape, wrap, device) {
31219
31295
  if (!box || !inv2) {
31220
31296
  return null;
31221
31297
  }
31298
+ const anisotropicFocus = !!shape.focus && Math.abs(shape.focus.x - shape.focus.y) > 1e-9;
31299
+ const uniformSurround = shape.boundaryArgb.every((color) => color === shape.boundaryArgb[0]);
31300
+ const smooth = !uniformSurround && !shape.preset || anisotropicFocus || wrap !== "clamp" && !pixelAligned(shape.boundary, full);
31301
+ const quantum = smooth ? 0 : pathGradientQuantum(shape.boundary, full);
31222
31302
  const pxX = Math.hypot(full[0], full[1]);
31223
31303
  const pxY = Math.hypot(full[2], full[3]);
31224
31304
  const lagX = pxX > 0 ? 1 / pxX : 0;
@@ -31238,8 +31318,8 @@ function pathGradientSampler(shape, wrap, device) {
31238
31318
  by = foldIntoTile(by, box.y, box.h, mirrorY, lagY);
31239
31319
  }
31240
31320
  if (wrap === "clamp" && by >= box.y + box.h) continue;
31241
- const focusPoint = bias && shape.focus?.x === 0 ? { x: bx - inv2[0] * bias, y: by - inv2[1] * bias } : void 0;
31242
- const c = pathGradientColorAt(shape, bx, by, focusPoint);
31321
+ const focusPoint = bias ? { x: bx - inv2[0] * bias, y: by - inv2[1] * bias } : void 0;
31322
+ const c = pathGradientColorAt(shape, bx, by, focusPoint, quantum);
31243
31323
  if (c === null) {
31244
31324
  continue;
31245
31325
  }
@@ -32013,8 +32093,14 @@ function handleRoundRect(rCtx, dataOff, recSize) {
32013
32093
  const cornerFixW = inset ? Math.abs(cornerW * devM[0]) * 16 : fixExtent(rCtx, cornerW, 0);
32014
32094
  const cornerFixH = inset ? Math.abs(cornerH * devM[3]) * 16 : fixExtent(rCtx, cornerH, 1);
32015
32095
  const cornerOnFrame = (fix, size, framedSize) => inset && size !== 0 ? Math.floor(Math.min(fix, size) * framedSize / size) : fix;
32016
- const cw = cornerOnFrame(cornerFixW, Math.abs((r - l) * devM[0]) * 16, Math.abs(framed.exx));
32017
- const ch = cornerOnFrame(cornerFixH, Math.abs((b - t) * devM[3]) * 16, Math.abs(framed.eyy));
32096
+ let cw = cornerOnFrame(cornerFixW, Math.abs((r - l) * devM[0]) * 16, Math.abs(framed.exx));
32097
+ let ch = cornerOnFrame(cornerFixH, Math.abs((b - t) * devM[3]) * 16, Math.abs(framed.eyy));
32098
+ if (!inset && rCtx.state.penStyle !== 6 && cornerW !== 0 && cornerH !== 0 && Math.abs(devM[1]) < 1e-9 && Math.abs(devM[2]) < 1e-9 && rCtx.state.penStyle !== 5 && !penIsCosmetic(rCtx) && rCtx.state.worldTransform.every((v, i) => v === [1, 0, 0, 1, 0, 0][i])) {
32099
+ const fullW = (Math.abs(r - l) + 1) * Math.abs(devM[0]) * 16;
32100
+ const fullH = (Math.abs(b - t) + 1) * Math.abs(devM[3]) * 16;
32101
+ cw = Math.floor(Math.min(cornerFixW, fullW) * Math.abs(framed.exx) / fullW);
32102
+ ch = Math.floor(Math.min(cornerFixH, fullH) * Math.abs(framed.eyy) / fullH);
32103
+ }
32018
32104
  const drawRoundRect = inset ? (c) => appendRoundRectPath(
32019
32105
  c,
32020
32106
  Math.min(framed.ax, framed.ax + framed.exx) / 16,
package/dist/index.mjs CHANGED
@@ -16512,6 +16512,45 @@ function lerpUniformPathArgb(a, b, t) {
16512
16512
  };
16513
16513
  return (alpha << 24 | ch(16) << 16 | ch(8) << 8 | ch(0)) >>> 0;
16514
16514
  }
16515
+ function pathGradientQuantum(boundary, matrix = IDENTITY) {
16516
+ let x0 = Infinity;
16517
+ let y0 = Infinity;
16518
+ let x1 = -Infinity;
16519
+ let y1 = -Infinity;
16520
+ for (const p of boundary) {
16521
+ const x = matrix[0] * p.x + matrix[2] * p.y + matrix[4];
16522
+ const y = matrix[1] * p.x + matrix[3] * p.y + matrix[5];
16523
+ x0 = Math.min(x0, x);
16524
+ y0 = Math.min(y0, y);
16525
+ x1 = Math.max(x1, x);
16526
+ y1 = Math.max(y1, y);
16527
+ }
16528
+ const steps = Math.ceil(2 * Math.hypot(x1 - x0, y1 - y0));
16529
+ return Number.isFinite(steps) && steps > 0 ? steps : 0;
16530
+ }
16531
+ var TIE_EPSILON = 1e-7;
16532
+ function quantisedPathArgb(surround, centre, t, quantum) {
16533
+ const scaled2 = t * quantum;
16534
+ const tie = Math.abs(scaled2 - Math.floor(scaled2) - 0.5) < TIE_EPSILON;
16535
+ const k = tie ? scaled2 : Math.floor(scaled2 + 0.5);
16536
+ const fromBoundary = t < 0.5;
16537
+ const w = (fromBoundary ? k : quantum - k) / quantum;
16538
+ const aS = surround >>> 24 & 255;
16539
+ const aC = centre >>> 24 & 255;
16540
+ const mix2 = (s, c) => fromBoundary ? s + (c - s) * w : c + (s - c) * w;
16541
+ const alpha = Math.floor(mix2(aS, aC) + 0.5);
16542
+ if (alpha <= 0) return 0;
16543
+ const ch = (shift) => {
16544
+ const p = Math.floor(mix2((surround >>> shift & 255) * aS / 255, (centre >>> shift & 255) * aC / 255) + 0.5);
16545
+ return Math.min(255, Math.max(0, Math.round(p * 255 / alpha)));
16546
+ };
16547
+ return (alpha << 24 | ch(16) << 16 | ch(8) << 8 | ch(0)) >>> 0;
16548
+ }
16549
+ function quantisedPosition(pos, quantum) {
16550
+ const scaled2 = pos * quantum;
16551
+ const tie = Math.abs(scaled2 - Math.floor(scaled2) - 0.5) < TIE_EPSILON;
16552
+ return (tie ? scaled2 : Math.floor(scaled2 + 0.5)) / quantum;
16553
+ }
16515
16554
  function piecewise2(xs, ys, x) {
16516
16555
  const n = Math.min(xs.length, ys.length);
16517
16556
  if (n === 0) {
@@ -16546,11 +16585,13 @@ function stopColorAt(stops, t) {
16546
16585
  }
16547
16586
  return stops[stops.length - 1].argb;
16548
16587
  }
16549
- function pathGradientColorAt(shape, x, y, focusPoint) {
16588
+ function pathGradientColorAt(shape, x, y, focusPoint, quantum) {
16550
16589
  const { center, boundary, boundaryArgb } = shape;
16551
16590
  const n = boundary.length;
16552
16591
  const px = x - center.x;
16553
16592
  const py = y - center.y;
16593
+ const ux = (focusPoint?.x ?? x) - center.x;
16594
+ const uy = (focusPoint?.y ?? y) - center.y;
16554
16595
  let found = -1;
16555
16596
  let bestS = 0;
16556
16597
  let bestU = 0;
@@ -16572,8 +16613,10 @@ function pathGradientColorAt(shape, x, y, focusPoint) {
16572
16613
  continue;
16573
16614
  }
16574
16615
  found = i;
16575
- bestS = Math.max(0, alpha);
16576
- bestU = alpha > 0 ? Math.min(1, Math.max(0, beta / alpha)) : 0;
16616
+ const alphaU = quantum ? (ux * ey - uy * ex) / det : alpha;
16617
+ const betaU = quantum ? (ax * uy - ay * ux) / det : beta;
16618
+ bestS = Math.max(0, alphaU);
16619
+ bestU = alphaU > 0 ? Math.min(1, Math.max(0, betaU / alphaU)) : 0;
16577
16620
  }
16578
16621
  let s = found < 0 ? Infinity : bestS;
16579
16622
  if (shape.focus) {
@@ -16648,7 +16691,8 @@ function pathGradientColorAt(shape, x, y, focusPoint) {
16648
16691
  }
16649
16692
  }
16650
16693
  if (found < 0 || !Number.isFinite(s)) return null;
16651
- const pos = 1 - s;
16694
+ const exactPos = 1 - s;
16695
+ const pos = quantum && quantum > 0 ? quantisedPosition(exactPos, quantum) : exactPos;
16652
16696
  if (shape.preset && shape.preset.positions.length > 0) {
16653
16697
  const { positions, argb } = shape.preset;
16654
16698
  if (pos <= positions[0]) {
@@ -16667,6 +16711,9 @@ function pathGradientColorAt(shape, x, y, focusPoint) {
16667
16711
  const surround = lerpArgb(c0, c1, bestU);
16668
16712
  const factor = shape.blend ? piecewise2(shape.blend.positions, shape.blend.factors, pos) : pos;
16669
16713
  const t = Math.min(1, Math.max(0, factor));
16714
+ if (quantum && quantum > 0 && !shape.blend && boundaryArgb.every((color) => color === boundaryArgb[0])) {
16715
+ return quantisedPathArgb(surround, shape.centerArgb, Math.min(1, Math.max(0, exactPos)), quantum);
16716
+ }
16670
16717
  return surround >>> 24 !== shape.centerArgb >>> 24 && boundaryArgb.every((color) => color === boundaryArgb[0]) ? lerpUniformPathArgb(surround, shape.centerArgb, t) : lerpArgb(surround, shape.centerArgb, t);
16671
16718
  }
16672
16719
  var MAX_TILE = 2048;
@@ -29940,23 +29987,35 @@ function perpendicularVectors(width, dx0, dy0, height = width, matrixPen) {
29940
29987
  const vx = rounded[0], vy = rounded[1];
29941
29988
  return { v: flip ? [-vx, -vy] : [vx, vy], ray: flip ? [-x, -y] : [x, y] };
29942
29989
  }
29943
- function squareExtension(width, dx, dy, scale = 1, height = width, matrix, wholePixelNorm = false, symmetric = wholePixelNorm) {
29990
+ function norm(x, y) {
29991
+ return Math.sqrt(x * x + y * y);
29992
+ }
29993
+ function squareExtension(width, dx, dy, scale = 1, height = width, matrix, wholePixelNorm = false, symmetric = wholePixelNorm, logicalScale = 1) {
29994
+ const slack = 8;
29944
29995
  if (matrix) {
29945
29996
  const det = matrix[0] * matrix[3] - matrix[1] * matrix[2];
29946
29997
  const lx = (matrix[3] * dx - matrix[2] * dy) / det;
29947
29998
  const ly = (-matrix[1] * dx + matrix[0] * dy) / det;
29948
- const l = Math.hypot(lx, ly);
29999
+ let l = Math.hypot(lx, ly);
30000
+ if (wholePixelNorm) {
30001
+ l = norm(Math.floor((lx + slack) / 16) * 16, Math.floor((ly + slack) / 16) * 16);
30002
+ if (l === 0) return [0, 0];
30003
+ }
29949
30004
  const q = width / 2 * scale / l;
29950
30005
  return [Math.floor((matrix[0] * lx + matrix[2] * ly) * q + 0.5), Math.floor((matrix[1] * lx + matrix[3] * ly) * q + 0.5)];
29951
30006
  }
29952
- let len = width === height ? Math.hypot(dx, dy) : Math.hypot(dx, dy * width / height);
29953
- if (wholePixelNorm && width === height) {
29954
- const whole = Math.hypot(Math.floor(dx / 16), Math.floor(dy / 16)) * 16;
29955
- if (whole === 0) return [0, 0];
29956
- len = whole;
29957
- }
30007
+ const [ax, ay] = typeof logicalScale === "number" ? [logicalScale, logicalScale] : logicalScale;
29958
30008
  const r = width / 2 * scale;
29959
30009
  const round = symmetric ? (v) => Math.sign(v) * Math.floor(Math.abs(v) + 0.5) : (v) => Math.floor(v + 0.5);
30010
+ if (wholePixelNorm && (width === height || ax !== ay)) {
30011
+ const lx = dx / ax;
30012
+ const ly = dy / ay;
30013
+ const cut = ax === 1 && ay === 1 ? 0 : slack;
30014
+ const whole = norm(Math.floor((lx + cut) / 16) * 16, Math.floor((ly + cut) / 16) * 16);
30015
+ if (whole === 0) return [0, 0];
30016
+ return [round(lx / whole * r), round(ly * ay / ax / whole * r)];
30017
+ }
30018
+ const len = width === height ? norm(dx, dy) : norm(dx, dy * width / height);
29960
30019
  return [round(dx / len * r), round(dy / len * r)];
29961
30020
  }
29962
30021
  function turnSign(ax, ay, bx, by) {
@@ -29996,7 +30055,7 @@ var Outliner = class {
29996
30055
  const [L, R] = drawVertices(this.pen, d[0], d[1], this.opts.width, this.opts.height, nibMatrix);
29997
30056
  const perpendicular = curve && drawDir ? drawDir : [dx, dy];
29998
30057
  const vectors = perpendicularVectors(this.opts.width, perpendicular[0], perpendicular[1], this.opts.height, nibMatrix ? this.pen : void 0);
29999
- return { dx, dy, L, R, v: vectors.v, vRaw: vectors.ray, e: squareExtension(this.opts.width, perpendicular[0], perpendicular[1], 1, this.opts.height, nibMatrix, !!this.opts.wholePixelDashVectors), pe: [perpendicular[0], perpendicular[1]], curveEnd: !!drawDir, role: drawDir?.[2] ?? 3, tangentLength: drawDir ? drawDir[0] * drawDir[0] + drawDir[1] * drawDir[1] : 0, curve };
30058
+ return { dx, dy, L, R, v: vectors.v, vRaw: vectors.ray, e: squareExtension(this.opts.capWidth ?? this.opts.width, perpendicular[0], perpendicular[1], 1, this.opts.height ?? this.opts.capWidth, nibMatrix, !!(this.opts.wholePixelDashVectors || this.opts.cutToLogicalUnits), void 0, this.opts.logicalScale), pe: [perpendicular[0], perpendicular[1]], curveEnd: !!drawDir, role: drawDir?.[2] ?? 3, tangentLength: drawDir ? drawDir[0] * drawDir[0] + drawDir[1] * drawDir[1] : 0, curve };
30000
30059
  }
30001
30060
  push(p, v) {
30002
30061
  this.pts.push([p[0] + v[0], p[1] + v[1]]);
@@ -30314,8 +30373,8 @@ function dashPieces(P, tangents, curveFlags, pattern, shorten, wholePixelVectors
30314
30373
  const [x1, y1] = P[i + 1];
30315
30374
  const dir = [x1 - x0, y1 - y0];
30316
30375
  const det = matrix ? matrix[0] * matrix[3] - matrix[1] * matrix[2] : 1;
30317
- const real = matrix ? Math.hypot((matrix[3] * dir[0] - matrix[2] * dir[1]) / det, (-matrix[1] * dir[0] + matrix[0] * dir[1]) / det) : metric ? Math.hypot(dir[0] / metric[0], dir[1] / metric[1]) : Math.hypot(dir[0], dir[1]);
30318
- const len = metric || matrix ? real : wholePixelVectors ? Math.hypot(Math.floor(dir[0] / 16), Math.floor(dir[1] / 16)) * 16 : Math.hypot(dir[0], dir[1]);
30376
+ const real = matrix ? Math.hypot((matrix[3] * dir[0] - matrix[2] * dir[1]) / det, (-matrix[1] * dir[0] + matrix[0] * dir[1]) / det) : metric ? Math.hypot(dir[0] / metric[0], dir[1] / metric[1]) : norm(dir[0], dir[1]);
30377
+ const len = metric || matrix ? real : wholePixelVectors ? norm(Math.floor(dir[0] / 16), Math.floor(dir[1] / 16)) * 16 : norm(dir[0], dir[1]);
30319
30378
  if (len === 0) {
30320
30379
  if (on && cur) {
30321
30380
  cur.pts.push(P[i + 1]);
@@ -30423,26 +30482,16 @@ function widenPath(path, opts) {
30423
30482
  const runTangents = closed ? [...dirs, void 0] : dirs;
30424
30483
  const runCurves = closed ? [...curves, !!fig.roundWiden] : curves;
30425
30484
  for (const piece of dashPieces(run, runTangents, runCurves, opts.dashes, shorten, !!opts.wholePixelDashVectors, opts.dashMetric, opts.deviceNib ? opts.matrix : void 0)) {
30426
- const pts = [piece.pts[0]];
30427
- const pdirs = [];
30428
- const pdraws = [];
30429
- const pcurves = [];
30430
- for (let i = 1; i < piece.pts.length; i++) {
30431
- const q = piece.pts[i];
30432
- const last = pts[pts.length - 1];
30433
- if (q[0] !== last[0] || q[1] !== last[1]) {
30434
- pts.push(q);
30435
- pdirs.push(piece.dirs[i - 1]);
30436
- pdraws.push(piece.draws[i - 1]);
30437
- pcurves.push(piece.curves[i - 1]);
30438
- }
30439
- }
30485
+ const pts = piece.pts;
30486
+ const pdirs = piece.dirs;
30487
+ const pdraws = piece.draws;
30488
+ const pcurves = piece.curves;
30440
30489
  outliner.open(pts, pdirs.length > 0 ? pdirs : [piece.dirs[0]], pdirs.length > 0 ? pdraws : [piece.draws[0]], pdirs.length > 0 ? pcurves : [piece.curves[0]], piece.startScale, piece.endScale);
30441
30490
  }
30442
30491
  } else if (closed && P.length >= 3) {
30443
30492
  outliner.closed(P, dirs, curves);
30444
30493
  } else if (closed && P.length === 2) {
30445
- outliner.open([P[0], P[1], P[0]]);
30494
+ outliner.closed(P, dirs, curves);
30446
30495
  } else {
30447
30496
  outliner.open(P, void 0, dirs, curves);
30448
30497
  }
@@ -30705,11 +30754,17 @@ function penWidenOptions(rCtx, opts = {}) {
30705
30754
  join: opts.roundPen ? "round" : opts.rectangle && !state.penExtended ? "miter" : !state.penExtended || joinBits === 0 ? "round" : joinBits === 4096 ? "bevel" : "miter",
30706
30755
  miterLimit: state.miterLimit ?? 10,
30707
30756
  dashes: dashes ? dashes.map((v) => v * 16) : null,
30708
- shortenDashes: (flags & 15) !== 7
30757
+ shortenDashes: (flags & 15) !== 7,
30758
+ // Under a rotation the device vector is converted back to logical units for a square cap's extension.
30759
+ cutToLogicalUnits: !dashes
30709
30760
  };
30710
30761
  }
30762
+ const unrotated = Math.abs(matrix[1]) < 1e-6 && Math.abs(matrix[2]) < 1e-6;
30763
+ const scaled2 = !rCtx.wholeDevicePixels && unrotated && (Math.abs(Math.abs(matrix[0]) - 1) > 1e-6 || Math.abs(Math.abs(matrix[3]) - 1) > 1e-6);
30764
+ const uniform = scaled2 && !ellipse && state.penExtended && (flags & 15) !== 6;
30765
+ const nibWidth = uniform ? Math.max(1, Math.floor(widthPx + 0.5)) : widthPx;
30711
30766
  return {
30712
- width: Math.round((ellipse ? state.penWidth * Math.abs(matrix[0]) : widthPx) * 16),
30767
+ width: Math.round((ellipse ? state.penWidth * Math.abs(matrix[0]) : nibWidth) * 16),
30713
30768
  ...ellipse ? { height: Math.round(state.penWidth * Math.abs(matrix[3]) * 16) } : {},
30714
30769
  cap: opts.roundPen || !state.penExtended || capBits === 0 ? "round" : capBits === 256 ? "square" : "flat",
30715
30770
  join: opts.roundPen ? "round" : opts.rectangle && !state.penExtended ? "miter" : !state.penExtended || joinBits === 0 ? "round" : joinBits === 4096 ? "bevel" : "miter",
@@ -30719,7 +30774,13 @@ function penWidenOptions(rCtx, opts = {}) {
30719
30774
  shortenDashes: (flags & 15) !== 7,
30720
30775
  // One logical unit per device pixel, unrotated: GDI measures dash
30721
30776
  // segments in whole pixels.
30722
- wholePixelDashVectors: Math.abs(matrix[0] - 1) < 1e-6 && Math.abs(matrix[3] - 1) < 1e-6 && Math.abs(matrix[1]) < 1e-6 && Math.abs(matrix[2]) < 1e-6
30777
+ wholePixelDashVectors: Math.abs(matrix[0] - 1) < 1e-6 && Math.abs(matrix[3] - 1) < 1e-6 && Math.abs(matrix[1]) < 1e-6 && Math.abs(matrix[2]) < 1e-6,
30778
+ ...scaled2 && !dashes ? {
30779
+ cutToLogicalUnits: true,
30780
+ logicalScale: ellipse ? [Math.abs(matrix[0]), Math.abs(matrix[3])] : Math.abs(matrix[0]),
30781
+ // The pen rounds to whole pixels, but a square cap extends by half the unrounded width.
30782
+ ...uniform ? { capWidth: state.penWidth * Math.abs(matrix[0]) * 16 } : {}
30783
+ } : {}
30723
30784
  };
30724
30785
  }
30725
30786
  function hatchBackgroundMask(hatch) {
@@ -31210,6 +31271,21 @@ function foldIntoTile(v, origin, size, mirror2, lag) {
31210
31271
  }
31211
31272
  return origin + local;
31212
31273
  }
31274
+ function pixelAligned(boundary, m) {
31275
+ let x0 = Infinity;
31276
+ let y0 = Infinity;
31277
+ let x1 = -Infinity;
31278
+ let y1 = -Infinity;
31279
+ for (const p of boundary) {
31280
+ const x = m[0] * p.x + m[2] * p.y + m[4];
31281
+ const y = m[1] * p.x + m[3] * p.y + m[5];
31282
+ x0 = Math.min(x0, x);
31283
+ y0 = Math.min(y0, y);
31284
+ x1 = Math.max(x1, x);
31285
+ y1 = Math.max(y1, y);
31286
+ }
31287
+ return [x0, y0, x1, y1].every((v) => Math.abs(v - Math.round(v)) < 1e-6);
31288
+ }
31213
31289
  function pathGradientSampler(shape, wrap, device) {
31214
31290
  const box = boundaryBox2(shape.boundary);
31215
31291
  const full = mulMatrix(device, shape.transform ?? IDENTITY4);
@@ -31217,6 +31293,10 @@ function pathGradientSampler(shape, wrap, device) {
31217
31293
  if (!box || !inv2) {
31218
31294
  return null;
31219
31295
  }
31296
+ const anisotropicFocus = !!shape.focus && Math.abs(shape.focus.x - shape.focus.y) > 1e-9;
31297
+ const uniformSurround = shape.boundaryArgb.every((color) => color === shape.boundaryArgb[0]);
31298
+ const smooth = !uniformSurround && !shape.preset || anisotropicFocus || wrap !== "clamp" && !pixelAligned(shape.boundary, full);
31299
+ const quantum = smooth ? 0 : pathGradientQuantum(shape.boundary, full);
31220
31300
  const pxX = Math.hypot(full[0], full[1]);
31221
31301
  const pxY = Math.hypot(full[2], full[3]);
31222
31302
  const lagX = pxX > 0 ? 1 / pxX : 0;
@@ -31236,8 +31316,8 @@ function pathGradientSampler(shape, wrap, device) {
31236
31316
  by = foldIntoTile(by, box.y, box.h, mirrorY, lagY);
31237
31317
  }
31238
31318
  if (wrap === "clamp" && by >= box.y + box.h) continue;
31239
- const focusPoint = bias && shape.focus?.x === 0 ? { x: bx - inv2[0] * bias, y: by - inv2[1] * bias } : void 0;
31240
- const c = pathGradientColorAt(shape, bx, by, focusPoint);
31319
+ const focusPoint = bias ? { x: bx - inv2[0] * bias, y: by - inv2[1] * bias } : void 0;
31320
+ const c = pathGradientColorAt(shape, bx, by, focusPoint, quantum);
31241
31321
  if (c === null) {
31242
31322
  continue;
31243
31323
  }
@@ -32011,8 +32091,14 @@ function handleRoundRect(rCtx, dataOff, recSize) {
32011
32091
  const cornerFixW = inset ? Math.abs(cornerW * devM[0]) * 16 : fixExtent(rCtx, cornerW, 0);
32012
32092
  const cornerFixH = inset ? Math.abs(cornerH * devM[3]) * 16 : fixExtent(rCtx, cornerH, 1);
32013
32093
  const cornerOnFrame = (fix, size, framedSize) => inset && size !== 0 ? Math.floor(Math.min(fix, size) * framedSize / size) : fix;
32014
- const cw = cornerOnFrame(cornerFixW, Math.abs((r - l) * devM[0]) * 16, Math.abs(framed.exx));
32015
- const ch = cornerOnFrame(cornerFixH, Math.abs((b - t) * devM[3]) * 16, Math.abs(framed.eyy));
32094
+ let cw = cornerOnFrame(cornerFixW, Math.abs((r - l) * devM[0]) * 16, Math.abs(framed.exx));
32095
+ let ch = cornerOnFrame(cornerFixH, Math.abs((b - t) * devM[3]) * 16, Math.abs(framed.eyy));
32096
+ if (!inset && rCtx.state.penStyle !== 6 && cornerW !== 0 && cornerH !== 0 && Math.abs(devM[1]) < 1e-9 && Math.abs(devM[2]) < 1e-9 && rCtx.state.penStyle !== 5 && !penIsCosmetic(rCtx) && rCtx.state.worldTransform.every((v, i) => v === [1, 0, 0, 1, 0, 0][i])) {
32097
+ const fullW = (Math.abs(r - l) + 1) * Math.abs(devM[0]) * 16;
32098
+ const fullH = (Math.abs(b - t) + 1) * Math.abs(devM[3]) * 16;
32099
+ cw = Math.floor(Math.min(cornerFixW, fullW) * Math.abs(framed.exx) / fullW);
32100
+ ch = Math.floor(Math.min(cornerFixH, fullH) * Math.abs(framed.eyy) / fullH);
32101
+ }
32016
32102
  const drawRoundRect = inset ? (c) => appendRoundRectPath(
32017
32103
  c,
32018
32104
  Math.min(framed.ax, framed.ax + framed.exx) / 16,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emf-converter",
3
- "version": "4.13.0",
3
+ "version": "4.15.0",
4
4
  "description": "Convert EMF/WMF metafile binaries to PNG data URLs or SVG (markup, data URL, React/JSX)",
5
5
  "keywords": [
6
6
  "canvas",