@react-x11/components 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/code-editor/node.d.ts +13 -0
  2. package/dist/code-editor/node.d.ts.map +1 -1
  3. package/dist/code-editor/node.js +23 -5
  4. package/dist/code-editor/node.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +209 -23
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +873 -175
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/color.d.ts +25 -0
  10. package/dist/html/css/color.d.ts.map +1 -1
  11. package/dist/html/css/color.js +307 -19
  12. package/dist/html/css/color.js.map +1 -1
  13. package/dist/html/css/content.d.ts +11 -6
  14. package/dist/html/css/content.d.ts.map +1 -1
  15. package/dist/html/css/content.js +171 -12
  16. package/dist/html/css/content.js.map +1 -1
  17. package/dist/html/css/parse.d.ts.map +1 -1
  18. package/dist/html/css/parse.js +62 -9
  19. package/dist/html/css/parse.js.map +1 -1
  20. package/dist/html/css/shapes.d.ts +62 -0
  21. package/dist/html/css/shapes.d.ts.map +1 -0
  22. package/dist/html/css/shapes.js +214 -0
  23. package/dist/html/css/shapes.js.map +1 -0
  24. package/dist/html/css/style.d.ts +42 -5
  25. package/dist/html/css/style.d.ts.map +1 -1
  26. package/dist/html/css/style.js +75 -27
  27. package/dist/html/css/style.js.map +1 -1
  28. package/dist/html/css/ua.js +15 -0
  29. package/dist/html/css/ua.js.map +1 -1
  30. package/dist/html/css/values.d.ts +5 -0
  31. package/dist/html/css/values.d.ts.map +1 -1
  32. package/dist/html/css/values.js +18 -9
  33. package/dist/html/css/values.js.map +1 -1
  34. package/dist/html/index.d.ts.map +1 -1
  35. package/dist/html/index.js +7 -0
  36. package/dist/html/index.js.map +1 -1
  37. package/dist/html/layout/block.d.ts +38 -7
  38. package/dist/html/layout/block.d.ts.map +1 -1
  39. package/dist/html/layout/block.js +160 -50
  40. package/dist/html/layout/block.js.map +1 -1
  41. package/dist/html/layout/boxes.d.ts +29 -2
  42. package/dist/html/layout/boxes.d.ts.map +1 -1
  43. package/dist/html/layout/boxes.js +159 -20
  44. package/dist/html/layout/boxes.js.map +1 -1
  45. package/dist/html/layout/css-grid.d.ts.map +1 -1
  46. package/dist/html/layout/css-grid.js +69 -19
  47. package/dist/html/layout/css-grid.js.map +1 -1
  48. package/dist/html/layout/flex.d.ts.map +1 -1
  49. package/dist/html/layout/flex.js +1648 -225
  50. package/dist/html/layout/flex.js.map +1 -1
  51. package/dist/html/layout/inline.d.ts +15 -3
  52. package/dist/html/layout/inline.d.ts.map +1 -1
  53. package/dist/html/layout/inline.js +23 -8
  54. package/dist/html/layout/inline.js.map +1 -1
  55. package/dist/html/layout/table.d.ts +27 -1
  56. package/dist/html/layout/table.d.ts.map +1 -1
  57. package/dist/html/layout/table.js +907 -323
  58. package/dist/html/layout/table.js.map +1 -1
  59. package/dist/html/node.d.ts +36 -4
  60. package/dist/html/node.d.ts.map +1 -1
  61. package/dist/html/node.js +174 -37
  62. package/dist/html/node.js.map +1 -1
  63. package/dist/html/paint.d.ts +4 -1
  64. package/dist/html/paint.d.ts.map +1 -1
  65. package/dist/html/paint.js +769 -204
  66. package/dist/html/paint.js.map +1 -1
  67. package/dist/html/resources.js +14 -1
  68. package/dist/html/resources.js.map +1 -1
  69. package/dist/html/svg.d.ts +50 -17
  70. package/dist/html/svg.d.ts.map +1 -1
  71. package/dist/html/svg.js +501 -83
  72. package/dist/html/svg.js.map +1 -1
  73. package/dist/html/widgets.d.ts.map +1 -1
  74. package/dist/html/widgets.js +57 -4
  75. package/dist/html/widgets.js.map +1 -1
  76. package/package.json +3 -3
  77. package/src/code-editor/node.ts +24 -5
  78. package/src/html/css/cascade.ts +987 -176
  79. package/src/html/css/color.ts +345 -18
  80. package/src/html/css/content.ts +173 -15
  81. package/src/html/css/parse.ts +60 -8
  82. package/src/html/css/shapes.ts +273 -0
  83. package/src/html/css/style.ts +123 -30
  84. package/src/html/css/ua.ts +15 -0
  85. package/src/html/css/values.ts +23 -9
  86. package/src/html/index.ts +7 -0
  87. package/src/html/layout/block.ts +169 -56
  88. package/src/html/layout/boxes.ts +198 -24
  89. package/src/html/layout/css-grid.ts +89 -22
  90. package/src/html/layout/flex.ts +1863 -219
  91. package/src/html/layout/inline.ts +27 -7
  92. package/src/html/layout/table.ts +989 -333
  93. package/src/html/node.ts +204 -37
  94. package/src/html/paint.ts +999 -252
  95. package/src/html/resources.ts +13 -1
  96. package/src/html/svg.ts +600 -78
  97. package/src/html/widgets.ts +61 -4
@@ -29,7 +29,7 @@ import { contained, placedMatrix } from './layout/block.js';
29
29
  import { BOX_RAISES, LINE_BOX_RAISES, PADDED_FACES, Box, CLAMPED, CLIPPED_CELLS, COLLAPSED_CELLS, COLUMN_LINES, COLUMN_PIECES, COLUMN_ROWS, columned, FADED_BLOCKS, PAINT_ORDER, INLINE_OFFSETS, MOVED_OFF_LINES, SHADOWED_TEXT, SHIFTED_LINES, TEXT_RAISES, TEXT_SHIFTS, } from './layout/boxes.js';
30
30
  import { CLIPPED_TEXT, clipBoxOf, depthOf, inklessLayout, layoutOffsetOf, layoutOffsets, } from './layout/inline.js';
31
31
  import { tableGrid } from './layout/grid.js';
32
- import { SvgDrawing, inlineDrawing } from './svg.js';
32
+ import { SvgDrawing, concreteSize, inlineDrawing } from './svg.js';
33
33
  /**
34
34
  * The bounds of everything a box and its descendants draw, in document
35
35
  * coordinates, into `boundsX`…, with the text `position: relative` moved off
@@ -494,6 +494,7 @@ export function paintDocument(ctx, tree, options) {
494
494
  canvasSource: canvas?.source,
495
495
  negative: tree.negative,
496
496
  selectionStyler: options.selection ? tree.selectionStyler : null,
497
+ shapeStyler: tree.shapeStyler,
497
498
  });
498
499
  ctx.restore();
499
500
  }
@@ -695,7 +696,8 @@ function originBox(frame, options, style) {
695
696
  * it falls nearest, as browsers snap a box — boxes that meet share the
696
697
  * column their edge is in, and a rule 1.33px wide is one pixel, not two —
697
698
  * with the border box's corners less the widths between (5.3), where
698
- * `rounded` asks for them. Null where it has no area.
699
+ * `rounded` asks for them — cut to what the paint reaches (`clampArea`).
700
+ * Null where it has no area there.
699
701
  */
700
702
  function clipArea(box, options, style, rounded) {
701
703
  const left = box.x + options.originX;
@@ -715,7 +717,7 @@ function clipArea(box, options, style, rounded) {
715
717
  if (corners && (t || r || b || l)) {
716
718
  corners = spreadCorners(corners, box.width, frameHeight(box), -t, -r, -b, -l);
717
719
  }
718
- return { x, y, w, h, corners };
720
+ return clampArea(options, x, y, w, h, corners);
719
721
  }
720
722
  /** Where a box's background and border go: its border box, which for a
721
723
  * table leaves out the captions around it (CSS 2.1 17.4). */
@@ -1175,8 +1177,9 @@ function paintContent(ctx, box, options) {
1175
1177
  paintStacked(ctx, child, options);
1176
1178
  }
1177
1179
  if (visible) {
1178
- if (box.marker)
1179
- paintMarker(ctx, box.marker, options);
1180
+ if (box.marker) {
1181
+ paintMarker(ctx, box.marker, options, box.style.colorScheme);
1182
+ }
1180
1183
  if (box.replaced === 'image')
1181
1184
  paintImage(ctx, box, options);
1182
1185
  else if (box.replaced === 'svg')
@@ -1362,10 +1365,9 @@ const ROW_REACH = 2048;
1362
1365
  */
1363
1366
  function frameImages(ctx, frame, options) {
1364
1367
  return (layer) => {
1365
- const painted = clipArea(frame, options, layer, true);
1366
- const area = painted && clampRect(options, painted.x, painted.y, painted.w, painted.h);
1368
+ const area = clipArea(frame, options, layer, true);
1367
1369
  if (area) {
1368
- paintBackgroundImage(ctx, layer, area, originBox(frame, options, layer), options, painted.corners);
1370
+ paintBackgroundImage(ctx, layer, area, originBox(frame, options, layer), options, area.corners);
1369
1371
  }
1370
1372
  };
1371
1373
  }
@@ -1375,6 +1377,8 @@ function shadowReach(blur) {
1375
1377
  return blur > 0 ? Math.ceil(blur * 1.5) : 0;
1376
1378
  }
1377
1379
  const SQUARE = { x: [0, 0, 0, 0], y: [0, 0, 0, 0] };
1380
+ /** A rectangle with no area, where a ring has no inside. */
1381
+ const NOWHERE = { x: 0, y: 0, width: 0, height: 0 };
1378
1382
  /** Corners shrunk by `by` — an inner shadow's spread — or grown where it
1379
1383
  * is negative; a square corner stays square. */
1380
1384
  function grownCorners(c, by) {
@@ -1442,28 +1446,28 @@ function paintShadows(ctx, box, options, inset) {
1442
1446
  if (!clampRect(options, shape.x - reach, shape.y - reach, shape.width + 2 * reach, shape.height + 2 * reach)) {
1443
1447
  continue;
1444
1448
  }
1445
- // Cut, as a background is (`clampRect`), to what the paint reaches
1449
+ const around = spreadCorners(corners, rect.width, rect.height, s.spread, s.spread, s.spread, s.spread) ?? SQUARE;
1450
+ // Cut, as a background is (`clampArea`), to what the paint reaches
1446
1451
  // and as far again as the blur does, so the cut edges cast nothing
1447
- // on it. A shadow down a box thousands of pixels tall went to the
1448
- // server whole, and its outline, sent in 16.16 fixed point, threw.
1449
- const within = reach + CLAMP_PAD;
1450
- const cut = clampAround(options, shape, within);
1452
+ // on it, and the box with it, which a ring is the band outside and
1453
+ // the shadow is clipped out of. A shadow down a box thousands of
1454
+ // pixels tall went to the server whole, and its outline, sent in
1455
+ // 16.16 fixed point, threw.
1456
+ const [cut, own] = clampPair(options, reach + CLAMP_PAD, { rect: shape, corners: around }, { rect, corners });
1451
1457
  if (!cut)
1452
1458
  continue;
1453
- Object.assign(shape, cut);
1454
- const own = clampAround(options, rect, within) ?? rect;
1455
- const around = spreadCorners(corners, rect.width, rect.height, s.spread, s.spread, s.spread, s.spread) ?? SQUARE;
1459
+ const { x, y, width, height } = cut.rect;
1456
1460
  const color = inkColor(s.color, style.color);
1457
1461
  if (!(s.blur > 0) && !s.x && !s.y && s.spread >= 0 && !covered) {
1458
1462
  // a ring about the box: the band between it and the spread
1459
1463
  ctx.fillStyle = color;
1460
- fillRing(ctx, shape, around, own, corners);
1464
+ fillRing(ctx, cut.rect, cut.corners, own ? own.rect : NOWHERE, own ? own.corners : SQUARE);
1461
1465
  continue;
1462
1466
  }
1463
1467
  if (!(s.blur > 0) && covered) {
1464
1468
  // a hard shadow, under a box that hides what falls under it
1465
1469
  fillShadow(ctx, s, color, (dx) => {
1466
- roundedRect(ctx, shape.x - dx, shape.y, shape.width, shape.height, around);
1470
+ roundedRect(ctx, x - dx, y, width, height, cut.corners);
1467
1471
  }, 0);
1468
1472
  continue;
1469
1473
  }
@@ -1472,13 +1476,16 @@ function paintShadows(ctx, box, options, inset) {
1472
1476
  if (clipped) {
1473
1477
  ctx.save();
1474
1478
  ctx.beginPath();
1475
- ctx.rect(shape.x - reach - 1, shape.y - reach - 1, shape.width + 2 * reach + 2, shape.height + 2 * reach + 2);
1476
- roundedRect(ctx, own.x, own.y, own.width, own.height, corners, true, true);
1479
+ ctx.rect(x - reach - 1, y - reach - 1, width + 2 * reach + 2, height + 2 * reach + 2);
1480
+ if (own) {
1481
+ const { rect: r } = own;
1482
+ roundedRect(ctx, r.x, r.y, r.width, r.height, own.corners, true, true);
1483
+ }
1477
1484
  ctx.clip();
1478
1485
  }
1479
1486
  fillShadow(ctx, s, color, (dx) => {
1480
- shadowShape(ctx, shape.x - dx, shape.y, shape.width, shape.height, around);
1481
- }, shape.x + shape.width + reach);
1487
+ shadowShape(ctx, x - dx, y, width, height, cut.corners);
1488
+ }, x + width + reach);
1482
1489
  if (clipped)
1483
1490
  ctx.restore();
1484
1491
  }
@@ -1503,48 +1510,53 @@ function paintShadows(ctx, box, options, inset) {
1503
1510
  furthest = Math.max(furthest, shadowReach(s.blur) + Math.abs(s.x) + Math.abs(s.y) + 1);
1504
1511
  }
1505
1512
  }
1506
- const padCut = clampAround(options, pad, furthest + CLAMP_PAD);
1507
- if (!padCut)
1508
- return;
1509
- Object.assign(pad, padCut);
1510
1513
  const inner = insetCorners(corners, box.borderTop, box.borderRight, box.borderBottom, box.borderLeft);
1511
1514
  for (let i = shadows.length - 1; i >= 0; i -= 1) {
1512
1515
  const s = shadows[i];
1513
1516
  if (!s.inset || (s.blur > 0 && !canShadow))
1514
1517
  continue;
1515
1518
  const color = inkColor(s.color, style.color);
1516
- // the hole the shadow is cast around, moved and shrunk by the spread
1517
- const hole = {
1518
- x: pad.x + s.x + s.spread,
1519
- y: pad.y + s.y + s.spread,
1520
- width: Math.max(0, pad.width - 2 * s.spread),
1521
- height: Math.max(0, pad.height - 2 * s.spread),
1522
- };
1523
- const within = grownCorners(inner, -s.spread);
1519
+ // The hole the shadow is cast around, moved and shrunk by the spread,
1520
+ // cut with the padding box: made from the cut one, it was a spread in
1521
+ // from the cut rather than from the box's edge, and a spread wider
1522
+ // than the margin left none.
1523
+ const [area, hole] = clampPair(options, furthest + CLAMP_PAD, { rect: pad, corners: inner }, {
1524
+ rect: {
1525
+ x: pad.x + s.x + s.spread,
1526
+ y: pad.y + s.y + s.spread,
1527
+ width: Math.max(0, pad.width - 2 * s.spread),
1528
+ height: Math.max(0, pad.height - 2 * s.spread),
1529
+ },
1530
+ corners: grownCorners(inner, -s.spread),
1531
+ });
1532
+ if (!area)
1533
+ continue;
1524
1534
  if (!(s.blur > 0) && !s.x && !s.y && s.spread >= 0) {
1525
1535
  // Tailwind's `ring-inset`: a band inside the padding edge
1526
1536
  ctx.fillStyle = color;
1527
- fillRing(ctx, pad, inner, hole, within);
1537
+ fillRing(ctx, area.rect, area.corners, hole ? hole.rect : NOWHERE, hole ? hole.corners : SQUARE);
1528
1538
  continue;
1529
1539
  }
1530
1540
  if (!ctx.clip || !ctx.save || !ctx.rect)
1531
1541
  continue;
1542
+ const { x, y, width, height } = area.rect;
1532
1543
  ctx.save();
1533
1544
  ctx.beginPath();
1534
- roundedRect(ctx, pad.x, pad.y, pad.width, pad.height, inner);
1545
+ roundedRect(ctx, x, y, width, height, area.corners);
1535
1546
  ctx.clip();
1536
1547
  // a frame around the hole, as wide as the blur and the offset reach
1537
1548
  const reach = shadowReach(s.blur) + Math.abs(s.x) + Math.abs(s.y) + 1;
1538
1549
  const frame = {
1539
- x: pad.x - reach,
1540
- y: pad.y - reach,
1541
- width: pad.width + 2 * reach,
1542
- height: pad.height + 2 * reach,
1550
+ x: x - reach,
1551
+ y: y - reach,
1552
+ width: width + 2 * reach,
1553
+ height: height + 2 * reach,
1543
1554
  };
1544
1555
  fillShadow(ctx, s, color, (dx) => {
1545
1556
  ctx.rect(frame.x - dx, frame.y, frame.width, frame.height);
1546
- if (hole.width > 0 && hole.height > 0) {
1547
- shadowShape(ctx, hole.x - dx, hole.y, hole.width, hole.height, within);
1557
+ if (hole) {
1558
+ const { rect: r } = hole;
1559
+ shadowShape(ctx, r.x - dx, r.y, r.width, r.height, hole.corners);
1548
1560
  }
1549
1561
  }, frame.x + frame.width + shadowReach(s.blur), 'evenodd');
1550
1562
  ctx.restore();
@@ -1742,7 +1754,7 @@ function paintFlowLines(ctx, box, options, outlines) {
1742
1754
  if (!intersects(child, options))
1743
1755
  continue;
1744
1756
  if (child.marker && child.style.visibility === 'visible') {
1745
- paintMarker(ctx, child.marker, options);
1757
+ paintMarker(ctx, child.marker, options, child.style.colorScheme);
1746
1758
  }
1747
1759
  if (child.lines)
1748
1760
  paintLines(ctx, child, options);
@@ -2776,22 +2788,78 @@ function clampRect(options, x, y, w, h) {
2776
2788
  return null;
2777
2789
  return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
2778
2790
  }
2791
+ /**
2792
+ * `clampRect` for a rectangle that may be rounded, with the corners it is
2793
+ * left with. Where no radius is larger than `CLAMP_PAD` they are its own,
2794
+ * and it is cut as a square one is: a corner the cut leaves on a cut edge
2795
+ * curves no further in from it than the margin, which keeps it out of what
2796
+ * is painted. A larger radius, a circle's or a pill's, would bend into it
2797
+ * — a circle 400 across repainted a strip at a time was another shape in
2798
+ * every strip across its curve — so there the window is moved out past
2799
+ * any curve it would cross, and a corner on a side it cut is square
2800
+ * (`cutRounded`, as a rounded border's ring is cut).
2801
+ */
2802
+ function clampArea(options, x, y, w, h, corners) {
2803
+ if (corners && curvesPast(corners, CLAMP_PAD)) {
2804
+ const shape = { rect: { x, y, width: w, height: h }, corners };
2805
+ const cut = cutRounded(roundedWindow(options, [shape]), shape);
2806
+ if (!cut)
2807
+ return null;
2808
+ const { rect } = cut;
2809
+ return {
2810
+ x: rect.x,
2811
+ y: rect.y,
2812
+ w: rect.width,
2813
+ h: rect.height,
2814
+ corners: cut.corners,
2815
+ };
2816
+ }
2817
+ const rect = clampRect(options, x, y, w, h);
2818
+ return rect && { x: rect.x, y: rect.y, w: rect.w, h: rect.h, corners };
2819
+ }
2820
+ /**
2821
+ * Two rounded rectangles drawn against each other — a shadow's shape and
2822
+ * the box it is clipped out of, a hole and what it is cut in — cut to one
2823
+ * window, `pad` round what the paint reaches, as `clampArea` cuts one:
2824
+ * where neither has a corner that curves further than `CLAMP_PAD`, as
2825
+ * square ones are, keeping their corners, and otherwise to a window moved
2826
+ * out past every curve of either it would cross, each square on the sides
2827
+ * it cut. `CLAMP_PAD` and not `pad`, because what a shadow's margin has
2828
+ * past it is what the blur casts back across the cut, and a corner bent
2829
+ * into the margin is cast that far further in. Null for one with nothing
2830
+ * in the window.
2831
+ */
2832
+ function clampPair(options, pad, a, b) {
2833
+ if (!curvesPast(a.corners, CLAMP_PAD) && !curvesPast(b.corners, CLAMP_PAD)) {
2834
+ return [clampKeeping(options, a, pad), clampKeeping(options, b, pad)];
2835
+ }
2836
+ const cut = roundedWindow(options, [a, b], pad);
2837
+ return [cutRounded(cut, a), cutRounded(cut, b)];
2838
+ }
2839
+ /** `clampAround` for a rounded rectangle, keeping its corners. */
2840
+ function clampKeeping(options, { rect, corners }, pad) {
2841
+ const cut = clampAround(options, rect, pad);
2842
+ return cut && { rect: cut, corners };
2843
+ }
2844
+ /** Whether a corner curves further than `by` along either of its sides. */
2845
+ function curvesPast({ x, y }, by) {
2846
+ for (let i = 0; i < 4; i += 1)
2847
+ if (x[i] > by || y[i] > by)
2848
+ return true;
2849
+ return false;
2850
+ }
2779
2851
  function paintBackground(ctx, box, options, style) {
2780
2852
  const color = style.backgroundColor;
2781
2853
  const gradient = style.backgroundGradient;
2782
2854
  const solid = !isTransparent(color);
2783
2855
  if (!solid && !gradient)
2784
2856
  return;
2785
- const area = clipArea(box, options, style, !!(ctx.roundRect && ctx.fill && ctx.beginPath));
2786
- const rect = area && clampRect(options, area.x, area.y, area.w, area.h);
2857
+ const rect = clipArea(box, options, style, !!(ctx.roundRect && ctx.fill && ctx.beginPath));
2787
2858
  if (!rect)
2788
2859
  return;
2789
- const rounded = area.corners;
2860
+ const rounded = rect.corners;
2790
2861
  const fill = () => {
2791
2862
  if (rounded) {
2792
- // The clamp can only have cut edges further than CLAMP_PAD outside
2793
- // the damage, and a sane radius is smaller than that — so a corner
2794
- // that survives the cut is whole, and a cut edge is offscreen.
2795
2863
  ctx.beginPath();
2796
2864
  roundedRect(ctx, rect.x, rect.y, rect.w, rect.h, rounded);
2797
2865
  ctx.fill();
@@ -3426,12 +3494,13 @@ function paintBackgroundImage(ctx, style, area, at, options, corners = null) {
3426
3494
  // a drawing is drawn a tile at a time, at the size it was given
3427
3495
  if (tiles <= MAX_TILES) {
3428
3496
  for (let y = fromY; y < toY; y += stepY) {
3429
- for (let x = fromX; x < toX; x += stepX)
3430
- svg.draw(ctx, Math.round(x), Math.round(y), iw, ih, scale);
3497
+ for (let x = fromX; x < toX; x += stepX) {
3498
+ svg.drawImage(ctx, Math.round(x), Math.round(y), iw, ih, scale, style.colorScheme);
3499
+ }
3431
3500
  }
3432
3501
  }
3433
3502
  else {
3434
- svg.draw(ctx, x0, y0, iw, ih, scale);
3503
+ svg.drawImage(ctx, x0, y0, iw, ih, scale, style.colorScheme);
3435
3504
  }
3436
3505
  }
3437
3506
  else if (tiles > 1 &&
@@ -3518,7 +3587,7 @@ function roundedTile(size, [modeX, modeY], [w, h], at) {
3518
3587
  }
3519
3588
  /**
3520
3589
  * A background tile's size (CSS Backgrounds 3, 3.9, over CSS Images' sizing
3521
- * of an object): `auto` is the image's own, as `tileSize` has it; `cover`
3590
+ * of an object): `auto` is the image's own, as `concreteSize` has it; `cover`
3522
3591
  * and `contain` scale it to fill the positioning area or to fit inside it,
3523
3592
  * keeping its ratio, and one with no ratio is the size of the area; and a
3524
3593
  * width or a height alone takes the other from the ratio, or else from the
@@ -3527,7 +3596,7 @@ function roundedTile(size, [modeX, modeY], [w, h], at) {
3527
3596
  */
3528
3597
  function sizedTile(size, image, area, scale) {
3529
3598
  if (size === 'auto')
3530
- return tileSize(image, area, scale);
3599
+ return concreteSize(image, area.width, area.height, scale);
3531
3600
  const { ratio } = image;
3532
3601
  if (size === 'cover' || size === 'contain') {
3533
3602
  if (!(ratio > 0 && area.height > 0))
@@ -3552,32 +3621,7 @@ function sizedTile(size, image, area, scale) {
3552
3621
  return [h * ratio, h];
3553
3622
  return [image.width === null ? area.width : image.width * scale, h];
3554
3623
  }
3555
- return tileSize(image, area, scale);
3556
- }
3557
- /**
3558
- * A background image's size, in device pixels, where nothing sets it — CSS
3559
- * 2.1 has no `background-size` — which is CSS Images' default sizing: an
3560
- * image's own size where it has one; the dimension it lacks from its ratio,
3561
- * or else from the positioning area; and one with a ratio alone as large as
3562
- * fits in the area. An SVG may be any of these, and a raster image is the
3563
- * first.
3564
- */
3565
- function tileSize(size, area, scale) {
3566
- const { ratio } = size;
3567
- const width = size.width === null ? null : size.width * scale;
3568
- const height = size.height === null ? null : size.height * scale;
3569
- if (width !== null && height !== null)
3570
- return [width, height];
3571
- if (width !== null)
3572
- return [width, ratio > 0 ? width / ratio : area.height];
3573
- if (height !== null)
3574
- return [ratio > 0 ? height * ratio : area.width, height];
3575
- if (ratio > 0) {
3576
- return area.width / area.height > ratio
3577
- ? [area.height * ratio, area.height]
3578
- : [area.width, area.width / ratio];
3579
- }
3580
- return [area.width, area.height];
3624
+ return concreteSize(image, area.width, area.height, scale);
3581
3625
  }
3582
3626
  /**
3583
3627
  * A box's outline (CSS 2.1 18.4, CSS UI 4 5): a border of its own width,
@@ -3642,12 +3686,13 @@ function paintOutline(ctx, box, options) {
3642
3686
  *
3643
3687
  * Where two sides that meet differ in colour — one of them transparent
3644
3688
  * included — the corner is cut on the diagonal from its outer point to its
3645
- * inner one and each side takes its half (CSS Backgrounds 3, 4.3), so a side
3689
+ * inner one and each side takes its half (CSS Backgrounds 3, 4.4), so a side
3646
3690
  * is a trapezoid, and a triangle where the box inside the borders has no
3647
3691
  * width: the CSS triangle a dropdown's caret and a tooltip's arrow are
3648
3692
  * drawn with, which came out as the rectangle around it. Only a solid side
3649
3693
  * is cut, and only against a side that is solid or paints nothing; dots,
3650
- * dashes and a double border's two lines keep their rectangles.
3694
+ * dashes and a double border's two lines keep their rectangles, the lines
3695
+ * joined where two double sides of one colour meet (`fillSide`).
3651
3696
  */
3652
3697
  function paintBorders(ctx, box, options) {
3653
3698
  // most boxes have none, and are asked on every paint
@@ -3687,14 +3732,12 @@ function paintBorders(ctx, box, options) {
3687
3732
  const tr = paths && mitred(t, r, topInk, rightInk, topStyle, rightStyle);
3688
3733
  const br = paths && mitred(b, r, bottomInk, rightInk, bottomStyle, rightStyle);
3689
3734
  const bl = paths && mitred(b, l, bottomInk, leftInk, bottomStyle, leftStyle);
3690
- const edge = (ex, ey, ew, eh, style, color, horizontal) => {
3691
- const rect = clampRect(options, ex, ey, ew, eh);
3692
- if (!rect)
3693
- return;
3735
+ const widths = [t, r, b, l];
3736
+ const joins = doubleJoins(box);
3737
+ /** A side with square corners, the top first and clockwise. */
3738
+ const edge = (at, style, color) => {
3694
3739
  ctx.fillStyle = color;
3695
- // The un-clamped start is the dash phase's origin, so the pattern does
3696
- // not crawl as the viewport moves along a long edge.
3697
- fillEdge(ctx, rect, horizontal ? ex : ey, style, horizontal);
3740
+ fillSide(ctx, options, at, x, y, w, h, widths, style, joins);
3698
3741
  };
3699
3742
  /** A side with a cut corner: its outer edge and then its inner one. */
3700
3743
  const side = (points, color) => {
@@ -3724,7 +3767,7 @@ function paintBorders(ctx, box, options) {
3724
3767
  side([x, y, x1, y, cutRight ? px1 : x1, py0, cutLeft ? px0 : x, py0], topInk);
3725
3768
  }
3726
3769
  else
3727
- edge(x, y, w, t, topStyle, topInk, true);
3770
+ edge(0, topStyle, topInk);
3728
3771
  }
3729
3772
  if (bottomInk !== null) {
3730
3773
  const cutLeft = bl && !over(leftInk);
@@ -3733,26 +3776,26 @@ function paintBorders(ctx, box, options) {
3733
3776
  side([x1, y1, x, y1, cutLeft ? px0 : x, py1, cutRight ? px1 : x1, py1], bottomInk);
3734
3777
  }
3735
3778
  else
3736
- edge(x, y1 - b, w, b, bottomStyle, bottomInk, true);
3779
+ edge(2, bottomStyle, bottomInk);
3737
3780
  }
3738
3781
  if (leftInk !== null) {
3739
3782
  if (tl || bl) {
3740
3783
  side([x, bl ? y1 : py1, x, tl ? y : py0, px0, py0, px0, py1], leftInk);
3741
3784
  }
3742
3785
  else
3743
- edge(x, y + t, l, h - t - b, leftStyle, leftInk, false);
3786
+ edge(3, leftStyle, leftInk);
3744
3787
  }
3745
3788
  if (rightInk !== null) {
3746
3789
  if (tr || br) {
3747
3790
  side([x1, tr ? y : py0, x1, br ? y1 : py1, px1, py1, px1, py0], rightInk);
3748
3791
  }
3749
3792
  else
3750
- edge(x1 - r, y + t, r, h - t - b, rightStyle, rightInk, false);
3793
+ edge(1, rightStyle, rightInk);
3751
3794
  }
3752
3795
  }
3753
3796
  /**
3754
3797
  * Whether the corner two sides meet at is cut on its diagonal, each side
3755
- * taking its half (CSS Backgrounds 3, 4.3): where both have a width, they
3798
+ * taking its half (CSS Backgrounds 3, 4.4): where both have a width, they
3756
3799
  * are painted in different colours or only one is painted at all, and each
3757
3800
  * is solid or unpainted. Sides of one colour make the same corner either
3758
3801
  * way, and so does a corner of a single pixel, which is left to the side
@@ -3765,12 +3808,14 @@ function mitred(a, b, inkA, inkB, styleA, styleB) {
3765
3808
  (inkB === null || styleB === 'solid'));
3766
3809
  }
3767
3810
  /**
3768
- * Fill a convex polygon, its corners as `x, y` pairs, cut to `area`: the
3769
- * part of it near what is painted, which is what keeps a side thousands of
3770
- * pixels long inside X's coordinates. Cut edge by edge rather than by
3771
- * moving its corners in, which would turn a diagonal that crosses the
3772
- * painted area: a slanted divider, `border-left: 100vw solid transparent`,
3773
- * has a corner far outside it.
3811
+ * Fill a polygon, its corners as `x, y` pairs, cut to `area`: the part of
3812
+ * it near what is painted, which is what keeps a side thousands of pixels
3813
+ * long inside X's coordinates. Cut edge by edge rather than by moving its
3814
+ * corners in, which would turn a diagonal that crosses the painted area: a
3815
+ * slanted divider, `border-left: 100vw solid transparent`, has a corner far
3816
+ * outside it. A polygon that is not convex, a rounded ring's share, may
3817
+ * come out of the cut with an edge run there and back along the area's
3818
+ * border, which fills nothing, and that border is outside the paint.
3774
3819
  */
3775
3820
  function fillPolygon(ctx, points, area) {
3776
3821
  const bounds = [area.x, area.y, area.x + area.w, area.y + area.h];
@@ -3841,9 +3886,7 @@ function paintBorderImage(ctx, box, options) {
3841
3886
  // the image's size, CSS Images' default sizing in the area: a drawing
3842
3887
  // with no size of its own is as large as it fits there, and a gradient,
3843
3888
  // which has none, is the area's
3844
- const [cw, ch] = loaded
3845
- ? tileSize(loaded, { x: 0, y: 0, width: aw, height: ah }, scale)
3846
- : [aw, ah];
3889
+ const [cw, ch] = loaded ? concreteSize(loaded, aw, ah, scale) : [aw, ah];
3847
3890
  const iw = cw / scale;
3848
3891
  const ih = ch / scale;
3849
3892
  if (!(iw > 0 && ih > 0))
@@ -3858,9 +3901,10 @@ function paintBorderImage(ctx, box, options) {
3858
3901
  const svg = image instanceof SvgDrawing ? image : null;
3859
3902
  const w = Math.max(1, Math.round(cw));
3860
3903
  const h = Math.max(1, Math.round(ch));
3861
- const drawn = options.cached?.(`border-image|${key}|${w}x${h}`, w, h, (sctx) => {
3904
+ const scheme = box.style.colorScheme;
3905
+ const drawn = options.cached?.(`border-image|${key}|${w}x${h}|${scheme}`, w, h, (sctx) => {
3862
3906
  if (svg)
3863
- svg.draw(sctx, 0, 0, w, h, scale);
3907
+ svg.drawImage(sctx, 0, 0, w, h, scale, scheme);
3864
3908
  else if (typeof source !== 'string') {
3865
3909
  const paint = gradientFill(sctx, source, 0, 0, w, h, box.style.color);
3866
3910
  if (paint)
@@ -4029,17 +4073,18 @@ function edgeRun(mode, start, length, tile) {
4029
4073
  }
4030
4074
  /**
4031
4075
  * A rounded box's border as the ring between its border edge and its
4032
- * padding edge, each rounded, where every side that has a border has one
4033
- * of the same colour and a solid rule: a card's or a button's. Drawn
4034
- * straight, its corners were square over the background's rounded ones,
4035
- * and an accent border down one side did not follow the corner. The inner
4036
- * radius is the outer less the wider border at that corner (CSS
4037
- * Backgrounds 3, 5.2). False where the border is not such a one, and it is
4038
- * drawn a side at a time.
4076
+ * padding edge, each rounded, where every side that has a border has a
4077
+ * solid rule: a card's or a button's. Drawn straight, its corners were
4078
+ * square over the background's rounded ones, and an accent border down one
4079
+ * side did not follow the corner. The inner radius is the outer less the
4080
+ * wider border at that corner (CSS Backgrounds 3, 5.2). Sides of one colour
4081
+ * are one fill; sides of more are each given their share of the ring
4082
+ * (`ringSides`). False where the border is not such a one, and it is drawn
4083
+ * a side at a time.
4039
4084
  */
4040
4085
  function roundedRing(ctx, box, x, y, w, h, options) {
4041
4086
  const s = box.style;
4042
- if (!ctx.roundRect || !ctx.fill || !ctx.beginPath)
4087
+ if (!ctx.fill || !ctx.beginPath)
4043
4088
  return false;
4044
4089
  const corners = cornersOf(s, w, h);
4045
4090
  if (!corners)
@@ -4051,33 +4096,444 @@ function roundedRing(ctx, box, x, y, w, h, options) {
4051
4096
  [box.borderLeft, s.borderLeftColor, s.borderLeftStyle],
4052
4097
  ];
4053
4098
  let color = null;
4099
+ let mixed = false;
4054
4100
  for (const [width, ink, style] of sides) {
4055
4101
  if (!width)
4056
4102
  continue;
4057
- if (style !== 'solid' || (color !== null && ink !== color))
4103
+ if (style !== 'solid')
4058
4104
  return false;
4105
+ if (color !== null && ink !== color)
4106
+ mixed = true;
4059
4107
  color = ink;
4060
4108
  }
4109
+ if (mixed)
4110
+ return ringSides(ctx, box, x, y, w, h, corners, options);
4111
+ if (!ctx.roundRect)
4112
+ return false;
4061
4113
  if (color === null)
4062
4114
  return true;
4063
4115
  if (isTransparent(color))
4064
4116
  return true;
4065
- const rect = clampRect(options, x, y, w, h);
4066
- if (!rect)
4117
+ wholeRing(ctx, box, inkColor(color, s.color), x, y, w, h, corners, options);
4118
+ return true;
4119
+ }
4120
+ /** The ring of a rounded border in one colour, cut to what the paint
4121
+ * reaches (`cutRing`). */
4122
+ function wholeRing(ctx, box, color, x, y, w, h, corners, options) {
4123
+ const ring = cutRing(options, x, y, w, h, corners, box.borderTop, box.borderRight, box.borderBottom, box.borderLeft);
4124
+ if (!ring)
4125
+ return;
4126
+ const { outer, inner } = ring;
4127
+ ctx.fillStyle = color;
4128
+ fillRing(ctx, outer.rect, outer.corners, inner ? inner.rect : NOWHERE, inner ? inner.corners : outer.corners);
4129
+ }
4130
+ /** How far a chord of a flattened corner may stray from its curve, in
4131
+ * device pixels: a sixteenth, well inside what antialiasing shows. */
4132
+ const CURVE_TOLERANCE = 1 / 16;
4133
+ /** How far past a cut a share reaches under the one drawn over it, in
4134
+ * device pixels: past the antialiased edge of either. */
4135
+ const SEAM = 1.5;
4136
+ /** Which way a border's sides run at each corner, clockwise from the top
4137
+ * left: the side that ends at corner i arrives along `ALONG[i]`, and the
4138
+ * side that begins there leaves along `ALONG[i + 1]`. */
4139
+ const ALONG = [
4140
+ [0, -1],
4141
+ [1, 0],
4142
+ [0, 1],
4143
+ [-1, 0],
4144
+ ];
4145
+ /**
4146
+ * A rounded border whose solid sides are not all one colour, each side's
4147
+ * share of the ring filled in its own. Where two sides of different colours
4148
+ * meet, the corner is cut on the line from the border box's corner through
4149
+ * the padding box's, as Blink cuts it: CSS Backgrounds 3, 4.4 puts the
4150
+ * transition on the curve at a point that follows the ratio of the two
4151
+ * widths and leaves the rest to the UA. A corner between a side and one
4152
+ * with no width is all the side's (4.4 again), and sides of one colour that
4153
+ * meet are one fill, so nothing is cut where the colour does not change.
4154
+ * Drawn a side at a time, the spinner the web draws — `border-radius: 50%`
4155
+ * with one side of another colour — was a square frame.
4156
+ *
4157
+ * A share is a polygon: the curves flattened to chords no further than
4158
+ * `CURVE_TOLERANCE` from them, so that it is cut to what the paint reaches
4159
+ * as the square corners are (`fillPolygon`) and a large ring repainted a
4160
+ * strip at a time is the same ring. False where the context draws no
4161
+ * lines, and the sides are drawn straight.
4162
+ */
4163
+ function ringSides(ctx, box, x, y, w, h, corners, options) {
4164
+ if (!ctx.moveTo || !ctx.lineTo)
4165
+ return false;
4166
+ const area = clampRect(options, x, y, w, h);
4167
+ if (!area)
4067
4168
  return true;
4068
- const top = box.borderTop;
4069
- const right = box.borderRight;
4070
- const bottom = box.borderBottom;
4071
- const left = box.borderLeft;
4072
- ctx.fillStyle = inkColor(color, s.color);
4073
- fillRing(ctx, { x: rect.x, y: rect.y, width: rect.w, height: rect.h }, corners, {
4074
- x: rect.x + left,
4075
- y: rect.y + top,
4076
- width: Math.max(0, rect.w - left - right),
4077
- height: Math.max(0, rect.h - top - bottom),
4078
- }, insetCorners(corners, top, right, bottom, left));
4169
+ const s = box.style;
4170
+ const widths = [
4171
+ box.borderTop,
4172
+ box.borderRight,
4173
+ box.borderBottom,
4174
+ box.borderLeft,
4175
+ ];
4176
+ const colors = [
4177
+ s.borderTopColor,
4178
+ s.borderRightColor,
4179
+ s.borderBottomColor,
4180
+ s.borderLeftColor,
4181
+ ];
4182
+ // what each side is painted in, null where it paints nothing
4183
+ const inks = widths.map((width, i) => width > 0 && !isTransparent(colors[i])
4184
+ ? inkColor(colors[i], s.color)
4185
+ : null);
4186
+ // Corners from the top left, clockwise. Corner i is where side i - 1
4187
+ // ends and side i begins: the top left ends the left and begins the top.
4188
+ // Two sides of one colour are joined across their corner.
4189
+ const joined = [0, 1, 2, 3].map((i) => {
4190
+ const before = (i + 3) % 4;
4191
+ return widths[before] > 0 && widths[i] > 0 && inks[before] === inks[i];
4192
+ });
4193
+ if (!joined.includes(false)) {
4194
+ // one colour after all, `currentColor` and the colour it is
4195
+ if (!ctx.roundRect)
4196
+ return false;
4197
+ if (inks[0] !== null) {
4198
+ wholeRing(ctx, box, inks[0], x, y, w, h, corners, options);
4199
+ }
4200
+ return true;
4201
+ }
4202
+ const [t, r, b, l] = widths;
4203
+ const x1 = x + w;
4204
+ const y1 = y + h;
4205
+ // the padding box, inside the borders
4206
+ const ix0 = x + l;
4207
+ const iy0 = y + t;
4208
+ const ix1 = Math.max(ix0, x1 - r);
4209
+ const iy1 = Math.max(iy0, y1 - b);
4210
+ const inside = insetCorners(corners, t, r, b, l);
4211
+ const hollow = ix1 > ix0 && iy1 > iy0;
4212
+ // each corner's point on the border box and on the padding box, which
4213
+ // way it points, and the quarter ellipses that round the two there
4214
+ const sx = [-1, 1, 1, -1];
4215
+ const sy = [-1, -1, 1, 1];
4216
+ const ox = [x, x1, x1, x];
4217
+ const oy = [y, y, y1, y1];
4218
+ const px = [ix0, ix1, ix1, ix0];
4219
+ const py = [iy0, iy0, iy1, iy1];
4220
+ const quarter = (i, cornerX, cornerY, rx, ry) => rx > 0 && ry > 0
4221
+ ? { cx: cornerX - sx[i] * rx, cy: cornerY - sy[i] * ry, rx, ry }
4222
+ : null;
4223
+ const outer = [];
4224
+ const inner = [];
4225
+ for (let i = 0; i < 4; i += 1) {
4226
+ outer.push(quarter(i, ox[i], oy[i], corners.x[i], corners.y[i]));
4227
+ inner.push(hollow ? quarter(i, px[i], py[i], inside.x[i], inside.y[i]) : null);
4228
+ }
4229
+ // A corner's quarter turns clockwise from the angle it starts at: the top
4230
+ // left's from pointing left to pointing up.
4231
+ const QUARTER = Math.PI / 2;
4232
+ const start = (i) => Math.PI + i * QUARTER;
4233
+ /** Where a line from (fromX, fromY) along (dx, dy) first meets a quarter
4234
+ * ellipse, as the angle it is at; the line starts outside it. */
4235
+ const crossing = (i, q, fromX, fromY, dx, dy) => {
4236
+ const a0 = start(i);
4237
+ if (!q)
4238
+ return a0;
4239
+ const ux = (fromX - q.cx) / q.rx;
4240
+ const uy = (fromY - q.cy) / q.ry;
4241
+ const vx = dx / q.rx;
4242
+ const vy = dy / q.ry;
4243
+ const qa = vx * vx + vy * vy;
4244
+ const qb = ux * vx + uy * vy;
4245
+ const disc = qb * qb - qa * (ux * ux + uy * uy - 1);
4246
+ const k = qa > 0 && disc > 0 ? (-qb - Math.sqrt(disc)) / qa : 0;
4247
+ let a = Math.atan2(uy + k * vy, ux + k * vx);
4248
+ // into the quarter's own turn
4249
+ a += 2 * Math.PI * Math.round((a0 + QUARTER / 2 - a) / (2 * Math.PI));
4250
+ return Math.min(a0 + QUARTER, Math.max(a0, a));
4251
+ };
4252
+ // Where the shares meet at each corner, as angles on its two quarters: on
4253
+ // the cut, or at the end of the corner where one side has no width and
4254
+ // the other has all of it — the side that has none paints nothing.
4255
+ const cutOuter = [];
4256
+ const cutInner = [];
4257
+ for (let i = 0; i < 4; i += 1) {
4258
+ const before = widths[(i + 3) % 4] > 0;
4259
+ const after = widths[i] > 0;
4260
+ if (before && after) {
4261
+ const dx = px[i] - ox[i];
4262
+ const dy = py[i] - oy[i];
4263
+ cutOuter.push(crossing(i, outer[i], ox[i], oy[i], dx, dy));
4264
+ cutInner.push(crossing(i, inner[i], px[i], py[i], dx, dy));
4265
+ }
4266
+ else {
4267
+ const end = before ? start(i) + QUARTER : start(i);
4268
+ cutOuter.push(end);
4269
+ cutInner.push(end);
4270
+ }
4271
+ }
4272
+ // The shares, a run of sides of one colour each, from the corner the
4273
+ // colour changes at to the next one it changes at. Those that show what
4274
+ // is under them are drawn first.
4275
+ const runs = [];
4276
+ for (let i = 0; i < 4; i += 1) {
4277
+ if (joined[i])
4278
+ continue;
4279
+ let j = (i + 1) % 4;
4280
+ while (joined[j])
4281
+ j = (j + 1) % 4;
4282
+ runs.push({ from: i, to: j, ink: inks[i] });
4283
+ }
4284
+ const opaque = (ink) => ink !== null && alphaOf(ink) === 1;
4285
+ runs.sort((a, b) => Number(opaque(a.ink)) - Number(opaque(b.ink)));
4286
+ /** Whether a share reaches past its cut at a corner, under the one on
4287
+ * the other side of it: where that one is drawn after it and covers it. */
4288
+ const under = (k, corner) => {
4289
+ const before = widths[(corner + 3) % 4] > 0;
4290
+ if (!(before && widths[corner] > 0))
4291
+ return false;
4292
+ const other = runs.findIndex((run) => run !== runs[k] && (run.from === corner || run.to === corner));
4293
+ return other > k && opaque(runs[other].ink);
4294
+ };
4295
+ // The straight part of each side's edge, between the quarters at its
4296
+ // ends, on the border edge and the padding edge: as far as a share may
4297
+ // reach along it past a corner.
4298
+ const straight = (q, across, down) => {
4299
+ const rx = (i) => q[i]?.rx ?? 0;
4300
+ const ry = (i) => q[i]?.ry ?? 0;
4301
+ return [
4302
+ across - rx(0) - rx(1),
4303
+ down - ry(1) - ry(2),
4304
+ across - rx(2) - rx(3),
4305
+ down - ry(3) - ry(0),
4306
+ ].map((v) => Math.max(0, v));
4307
+ };
4308
+ const outerRoom = straight(outer, w, h);
4309
+ const innerRoom = straight(inner, ix1 - ix0, iy1 - iy0);
4310
+ /**
4311
+ * A share's end moved past the cut at a corner, onto the other share,
4312
+ * along an edge from the angle `a` it is at on a quarter: `forward` the
4313
+ * way the corner turns, onto the side that begins there, or back onto
4314
+ * the side that ends there. Far enough that the line it ends on is
4315
+ * `SEAM` from the cut, whatever angle the two make; as the angle it
4316
+ * reaches on the quarter and how far it goes on along the straight edge
4317
+ * past it, no further than `room`.
4318
+ */
4319
+ const past = (q, i, a, forward, room) => {
4320
+ const end = forward ? start(i) + QUARTER : start(i);
4321
+ const sign = forward ? 1 : -1;
4322
+ const [ex, ey] = forward ? ALONG[(i + 1) % 4] : ALONG[i];
4323
+ // the way it goes, and the sine of its angle with the cut
4324
+ let tx = forward ? ex : -ex;
4325
+ let ty = forward ? ey : -ey;
4326
+ if (q && a !== end) {
4327
+ const speed = Math.hypot(q.rx * Math.sin(a), q.ry * Math.cos(a));
4328
+ tx = (sign * -q.rx * Math.sin(a)) / speed;
4329
+ ty = (sign * q.ry * Math.cos(a)) / speed;
4330
+ }
4331
+ const dx = px[i] - ox[i];
4332
+ const dy = py[i] - oy[i];
4333
+ const sine = Math.abs(tx * dy - ty * dx) / Math.hypot(dx, dy);
4334
+ let left = SEAM / Math.max(0.25, sine);
4335
+ let at = a;
4336
+ for (let n = 0; q && n < 8 && left > 0 && at !== end; n += 1) {
4337
+ const speed = Math.hypot(q.rx * Math.sin(at), q.ry * Math.cos(at));
4338
+ const turn = Math.abs(end - at);
4339
+ if (left >= turn * speed) {
4340
+ left -= turn * speed;
4341
+ at = end;
4342
+ }
4343
+ else {
4344
+ at += (sign * left) / speed;
4345
+ left = 0;
4346
+ }
4347
+ }
4348
+ return [at, at === end ? Math.min(left, room) : 0];
4349
+ };
4350
+ const points = [];
4351
+ /** The point at an angle on a quarter, or the corner where it is square,
4352
+ * and `over` on past the quarter's end along the edge `along`. */
4353
+ const point = (q, a, cx, cy, over = 0, along = ALONG[0]) => {
4354
+ const x = (q ? q.cx + q.rx * Math.cos(a) : cx) + over * along[0];
4355
+ const y = (q ? q.cy + q.ry * Math.sin(a) : cy) + over * along[1];
4356
+ const n = points.length;
4357
+ if (n && points[n - 2] === x && points[n - 1] === y)
4358
+ return;
4359
+ points.push(x, y);
4360
+ };
4361
+ /** A quarter's curve from one angle to another, as chords, after the
4362
+ * point it starts at. */
4363
+ const sweep = (q, from, to) => {
4364
+ if (!q || from === to)
4365
+ return;
4366
+ const radius = Math.max(q.rx, q.ry);
4367
+ const step = radius > CURVE_TOLERANCE
4368
+ ? 2 * Math.acos(1 - CURVE_TOLERANCE / radius)
4369
+ : QUARTER;
4370
+ const n = Math.min(64, Math.ceil(Math.abs(to - from) / step));
4371
+ for (let k = 1; k <= n; k += 1) {
4372
+ point(q, from + ((to - from) * k) / n, 0, 0);
4373
+ }
4374
+ };
4375
+ for (let k = 0; k < runs.length; k += 1) {
4376
+ const { from: i, to: j, ink } = runs[k];
4377
+ if (ink === null)
4378
+ continue;
4379
+ // Where the share starts and ends on each edge. Two antialiased fills
4380
+ // that meet on a line each cover half of the pixels along it, and what
4381
+ // is under the border shows through between them. So a share reaches
4382
+ // on under the one drawn after it and covering it, and that one's
4383
+ // edge is the cut, over it — as Blink overdraws a corner, and as the
4384
+ // square corners are drawn.
4385
+ let [so, soOver] = [cutOuter[i], 0];
4386
+ let [si, siOver] = [cutInner[i], 0];
4387
+ let [eo, eoOver] = [cutOuter[j], 0];
4388
+ let [ei, eiOver] = [cutInner[j], 0];
4389
+ const back = (i + 3) % 4;
4390
+ if (under(k, i)) {
4391
+ [so, soOver] = past(outer[i], i, so, false, outerRoom[back]);
4392
+ [si, siOver] = past(inner[i], i, si, false, innerRoom[back]);
4393
+ }
4394
+ if (under(k, j)) {
4395
+ [eo, eoOver] = past(outer[j], j, eo, true, outerRoom[j]);
4396
+ [ei, eiOver] = past(inner[j], j, ei, true, innerRoom[j]);
4397
+ }
4398
+ const onward = ALONG[(j + 1) % 4];
4399
+ const backward = [-ALONG[i][0], -ALONG[i][1]];
4400
+ points.length = 0;
4401
+ // along the border edge, clockwise
4402
+ point(outer[i], so, ox[i], oy[i], soOver, backward);
4403
+ point(outer[i], so, ox[i], oy[i]);
4404
+ sweep(outer[i], so, start(i) + QUARTER);
4405
+ for (let c = (i + 1) % 4;; c = (c + 1) % 4) {
4406
+ point(outer[c], start(c), ox[c], oy[c]);
4407
+ if (c === j)
4408
+ break;
4409
+ sweep(outer[c], start(c), start(c) + QUARTER);
4410
+ }
4411
+ sweep(outer[j], start(j), eo);
4412
+ point(outer[j], eo, ox[j], oy[j], eoOver, onward);
4413
+ // and back along the padding edge
4414
+ point(inner[j], ei, px[j], py[j], eiOver, onward);
4415
+ point(inner[j], ei, px[j], py[j]);
4416
+ sweep(inner[j], ei, start(j));
4417
+ for (let c = (j + 3) % 4;; c = (c + 3) % 4) {
4418
+ point(inner[c], start(c) + QUARTER, px[c], py[c]);
4419
+ if (c === i)
4420
+ break;
4421
+ sweep(inner[c], start(c) + QUARTER, start(c));
4422
+ }
4423
+ sweep(inner[i], start(i) + QUARTER, si);
4424
+ point(inner[i], si, px[i], py[i], siOver, backward);
4425
+ ctx.fillStyle = ink;
4426
+ fillPolygon(ctx, points, area);
4427
+ }
4079
4428
  return true;
4080
4429
  }
4430
+ /**
4431
+ * A rounded box's ring — its border edge and its padding edge, the box
4432
+ * inside its borders — cut to the neighbourhood of what is being painted,
4433
+ * as `clampRect` cuts a rectangle. Both edges are the box's own, cut to
4434
+ * one window, so what is left is the part of the true ring in it. Cutting
4435
+ * the box first and insetting the cut by the borders put the hole a
4436
+ * border's width in from the cut rather than from the box's edge, and left
4437
+ * none where a border was wider than `CLAMP_PAD`: the ring filled whatever
4438
+ * of the box a paint reached. The window is the border edge's
4439
+ * (`roundedWindow`), whose curves the padding edge's run inside, and each
4440
+ * edge is cut to it as `cutRounded` cuts one. Null where the ring has
4441
+ * nothing in the window.
4442
+ */
4443
+ function cutRing(options, x, y, w, h, corners, top, right, bottom, left) {
4444
+ const edge = { rect: { x, y, width: w, height: h }, corners };
4445
+ const cut = roundedWindow(options, [edge]);
4446
+ const outer = cutRounded(cut, edge);
4447
+ if (!outer)
4448
+ return null;
4449
+ const iw = w - left - right;
4450
+ const ih = h - top - bottom;
4451
+ const inner = iw > 0 && ih > 0
4452
+ ? cutRounded(cut, {
4453
+ rect: { x: x + left, y: y + top, width: iw, height: ih },
4454
+ corners: insetCorners(corners, top, right, bottom, left),
4455
+ })
4456
+ : null;
4457
+ // a window inside the padding edge, where the ring has nothing
4458
+ if (inner && sameRounded(outer, inner))
4459
+ return null;
4460
+ return { outer, inner };
4461
+ }
4462
+ /**
4463
+ * The window `clampRect` cuts to, `pad` round what the paint reaches, for
4464
+ * rectangles with rounded corners: a cut is straight, so a side of it is
4465
+ * moved out past any corner's curve it would cross, in any of them.
4466
+ */
4467
+ function roundedWindow(options, shapes, pad = CLAMP_PAD) {
4468
+ // where each corner's curve runs, across and down
4469
+ const across = [];
4470
+ const down = [];
4471
+ for (const { rect, corners } of shapes) {
4472
+ const { x, y, width: w, height: h } = rect;
4473
+ const { x: cx, y: cy } = corners;
4474
+ across.push([x, x + cx[0]], [x + w - cx[1], x + w], [x + w - cx[2], x + w], [x, x + cx[3]]);
4475
+ down.push([y, y + cy[0]], [y, y + cy[1]], [y + h - cy[2], y + h], [y + h - cy[3], y + h]);
4476
+ }
4477
+ const d = options.damage;
4478
+ const side = (at, spans, towards) => Math.max(-COORD_LIMIT, Math.min(COORD_LIMIT, outOfSpans(at, spans, towards)));
4479
+ return [
4480
+ side(d ? d.x - pad : -COORD_LIMIT, across, -1),
4481
+ side(d ? d.y - pad : -COORD_LIMIT, down, -1),
4482
+ side(d ? d.x + d.width + pad : COORD_LIMIT, across, 1),
4483
+ side(d ? d.y + d.height + pad : COORD_LIMIT, down, 1),
4484
+ ];
4485
+ }
4486
+ /**
4487
+ * A rounded rectangle cut to a window (`roundedWindow`), as `clampRect`
4488
+ * cuts a rectangle: a corner on a side the window cut is square, its
4489
+ * curve being outside, and the rest are whole. Null where none of it is in
4490
+ * the window.
4491
+ */
4492
+ function cutRounded([x0, y0, x1, y1], { rect: { x, y, width: w, height: h }, corners: c }) {
4493
+ const l = Math.max(x, x0);
4494
+ const t = Math.max(y, y0);
4495
+ const r = Math.min(x + w, x1);
4496
+ const b = Math.min(y + h, y1);
4497
+ if (r <= l || b <= t)
4498
+ return null;
4499
+ // the corners of the sides the window left where they were
4500
+ const kept = [
4501
+ t === y && l === x,
4502
+ t === y && r === x + w,
4503
+ b === y + h && r === x + w,
4504
+ b === y + h && l === x,
4505
+ ];
4506
+ return {
4507
+ rect: { x: l, y: t, width: r - l, height: b - t },
4508
+ corners: {
4509
+ x: c.x.map((v, i) => (kept[i] ? v : 0)),
4510
+ y: c.y.map((v, i) => (kept[i] ? v : 0)),
4511
+ },
4512
+ };
4513
+ }
4514
+ /** A position moved out of any of `spans` it is inside, towards their
4515
+ * starts where `towards` is negative and their ends where it is not. */
4516
+ function outOfSpans(at, spans, towards) {
4517
+ for (let moved = true; moved;) {
4518
+ moved = false;
4519
+ for (const [from, to] of spans) {
4520
+ if (at > from && at < to) {
4521
+ at = towards < 0 ? from : to;
4522
+ moved = true;
4523
+ }
4524
+ }
4525
+ }
4526
+ return at;
4527
+ }
4528
+ /** Whether two rounded rectangles are one shape. */
4529
+ function sameRounded(a, b) {
4530
+ return (a.rect.x === b.rect.x &&
4531
+ a.rect.y === b.rect.y &&
4532
+ a.rect.width === b.rect.width &&
4533
+ a.rect.height === b.rect.height &&
4534
+ a.corners.x.every((v, i) => v === b.corners.x[i]) &&
4535
+ a.corners.y.every((v, i) => v === b.corners.y[i]));
4536
+ }
4081
4537
  /**
4082
4538
  * Clip to what a box's border paints, for a layer of `background-clip:
4083
4539
  * border-area` (CSS Backgrounds 4, 2.1): each side's width and style, and
@@ -4085,12 +4541,17 @@ function roundedRing(ctx, box, x, y, w, h, options) {
4085
4541
  * the value is for. The shapes are the border painter's own, so that the
4086
4542
  * background shows exactly where that border would: the ring `roundedRing`
4087
4543
  * fills where a rounded border is solid, the ring the trapezoids of a 3D
4088
- * style make, and otherwise the rectangles `fillEdge` fills a side at a
4089
- * time — the dots, the dashes, the two lines of a double border. All of
4544
+ * style make, and otherwise the rectangles `fillSide` fills a side at a
4545
+ * time — the dots, the dashes, the two lines of a double border joined as
4546
+ * they are painted, by the sides' colours though neither is drawn. All of
4090
4547
  * them run clockwise and a ring's inside the other way, so the clip is the
4091
- * union by the non-zero rule, the one every context's `clip` takes. False
4092
- * where the border paints nothing or the context cannot clip, and nothing
4093
- * is pushed.
4548
+ * union by the non-zero rule, the one every context's `clip` takes. Each
4549
+ * is placed from the box's edges and then cut to the painted area: placed
4550
+ * from the cut, a border wider than `CLAMP_PAD` put its bands and its
4551
+ * ring's hole a border's width in from wherever the paint was cut, and
4552
+ * the background was painted over the content of a box repainted in part.
4553
+ * False where the border paints nothing in the painted area or the context
4554
+ * cannot clip, and nothing is pushed.
4094
4555
  */
4095
4556
  function pushBorderArea(ctx, box, options) {
4096
4557
  const t = box.borderTop;
@@ -4109,44 +4570,53 @@ function pushBorderArea(ctx, box, options) {
4109
4570
  const h = Math.round(top + frameHeight(box)) - y;
4110
4571
  if (w <= 0 || h <= 0)
4111
4572
  return false;
4112
- const area = clampRect(options, x, y, w, h);
4113
- if (!area)
4573
+ if (!clampRect(options, x, y, w, h))
4114
4574
  return false;
4115
4575
  const corners = cornersOf(s, w, h);
4116
4576
  ctx.save();
4117
4577
  ctx.beginPath();
4578
+ // how many shapes the path has: none, and the border paints nothing here
4579
+ let shapes = 0;
4118
4580
  if (corners && canCurve(ctx) && solidBorder(box)) {
4119
- roundedRect(ctx, area.x, area.y, area.w, area.h, corners, true);
4120
- const iw = area.w - l - r;
4121
- const ih = area.h - t - b;
4122
- if (iw > 0 && ih > 0) {
4123
- const inner = insetCorners(corners, t, r, b, l);
4124
- roundedRect(ctx, area.x + l, area.y + t, iw, ih, inner, true, true);
4581
+ const ring = cutRing(options, x, y, w, h, corners, t, r, b, l);
4582
+ if (ring) {
4583
+ const add = ({ rect, corners: c }, hole) => roundedRect(ctx, rect.x, rect.y, rect.width, rect.height, c, true, hole);
4584
+ add(ring.outer, false);
4585
+ // the padding edge run the other way, which cuts it out
4586
+ if (ring.inner)
4587
+ add(ring.inner, true);
4588
+ shapes = 1;
4125
4589
  }
4126
4590
  }
4127
- else if (sculpted(s)) {
4128
- // a band a side, which the trapezoids cover between them
4129
- ctx.rect(area.x, area.y, area.w, t);
4130
- ctx.rect(area.x, area.y + area.h - b, area.w, b);
4131
- ctx.rect(area.x, area.y + t, l, area.h - t - b);
4132
- ctx.rect(area.x + area.w - r, area.y + t, r, area.h - t - b);
4133
- }
4134
4591
  else {
4135
- const path = { fillRect: ctx.rect.bind(ctx) };
4136
- const edge = (ex, ey, ew, eh, style, horizontal) => {
4137
- const rect = clampRect(options, ex, ey, ew, eh);
4138
- if (rect)
4139
- fillEdge(path, rect, horizontal ? ex : ey, style, horizontal);
4592
+ // A side at a time, from the box's edges and cut to the painted area
4593
+ // after: a 3D side as a band, which the trapezoids cover between them,
4594
+ // and any other as `fillSide` fills it.
4595
+ const path = {
4596
+ fillRect(ex, ey, ew, eh) {
4597
+ ctx.rect(ex, ey, ew, eh);
4598
+ shapes += 1;
4599
+ },
4140
4600
  };
4141
- const between = h - t - b;
4142
- if (t > 0)
4143
- edge(x, y, w, t, s.borderTopStyle, true);
4144
- if (b > 0)
4145
- edge(x, y + h - b, w, b, s.borderBottomStyle, true);
4146
- if (l > 0)
4147
- edge(x, y + t, l, between, s.borderLeftStyle, false);
4148
- if (r > 0)
4149
- edge(x + w - r, y + t, r, between, s.borderRightStyle, false);
4601
+ const widths = [t, r, b, l];
4602
+ const joins = doubleJoins(box);
4603
+ const styles = sculpted(s)
4604
+ ? ['solid', 'solid', 'solid', 'solid']
4605
+ : [
4606
+ s.borderTopStyle,
4607
+ s.borderRightStyle,
4608
+ s.borderBottomStyle,
4609
+ s.borderLeftStyle,
4610
+ ];
4611
+ for (let at = 0; at < 4; at += 1) {
4612
+ if (widths[at] > 0) {
4613
+ fillSide(path, options, at, x, y, w, h, widths, styles[at], joins);
4614
+ }
4615
+ }
4616
+ }
4617
+ if (!shapes) {
4618
+ ctx.restore();
4619
+ return false;
4150
4620
  }
4151
4621
  ctx.clip();
4152
4622
  return true;
@@ -4175,10 +4645,12 @@ function sculpted(s) {
4175
4645
  * colour's shadow and `outset` its bottom and right; `groove` is `inset`
4176
4646
  * outside `outset`, a band each, and `ridge` the other way round
4177
4647
  * (`borderShades`). A side of any other style takes its trapezoid in its
4178
- * colour. The trapezoids are clamped to the painted area, which leaves a
4179
- * cut end square and out of sight and keeps a long box's far corners out
4180
- * of X's coordinates. False where the context cannot draw a path, and the
4181
- * sides are drawn straight.
4648
+ * colour. The trapezoids are cut to the painted area (`fillPolygon`), which
4649
+ * keeps a long box's far corners out of X's coordinates and a join on its
4650
+ * diagonal: its corners clamped in, a border wider than `CLAMP_PAD` turned
4651
+ * the join to cross the damage at 45° wherever it was, and a scrolled
4652
+ * strip smeared the corners of a thick one. False where the context cannot
4653
+ * draw a path, and the sides are drawn straight.
4182
4654
  */
4183
4655
  function paintSculpted(ctx, box, x, y, w, h, options) {
4184
4656
  if (!ctx.beginPath || !ctx.moveTo || !ctx.lineTo || !ctx.fill)
@@ -4191,8 +4663,6 @@ function paintSculpted(ctx, box, x, y, w, h, options) {
4191
4663
  const r = box.borderRight;
4192
4664
  const b = box.borderBottom;
4193
4665
  const l = box.borderLeft;
4194
- const cx = (v) => Math.max(area.x - 1, Math.min(v, area.x + area.w + 1));
4195
- const cy = (v) => Math.max(area.y - 1, Math.min(v, area.y + area.h + 1));
4196
4666
  /** The band between the edge `outer` of the way in and `inner`, for one
4197
4667
  * side: 0 is the border box, 1 the padding box. */
4198
4668
  const band = (side, outer, inner, color) => {
@@ -4212,12 +4682,7 @@ function paintSculpted(ctx, box, x, y, w, h, options) {
4212
4682
  ? [o.x1, o.y1, o.x0, o.y1, i.x0, i.y1, i.x1, i.y1]
4213
4683
  : [o.x0, o.y1, o.x0, o.y0, i.x0, i.y0, i.x0, i.y1];
4214
4684
  ctx.fillStyle = color;
4215
- ctx.beginPath();
4216
- ctx.moveTo(cx(points[0]), cy(points[1]));
4217
- for (let k = 2; k < 8; k += 2)
4218
- ctx.lineTo(cx(points[k]), cy(points[k + 1]));
4219
- ctx.closePath?.();
4220
- ctx.fill();
4685
+ fillPolygon(ctx, points, area);
4221
4686
  };
4222
4687
  const sides = [
4223
4688
  [t, s.borderTopStyle, s.borderTopColor],
@@ -4322,54 +4787,153 @@ function paintCollapsedBorders(ctx, table, options) {
4322
4787
  for (const s of segments) {
4323
4788
  if (isTransparent(s.border.color))
4324
4789
  continue;
4325
- const rect = clampRect(options, s.x, s.y, s.w, s.h);
4326
- if (!rect)
4790
+ if (!clampRect(options, s.x, s.y, s.w, s.h))
4327
4791
  continue;
4328
4792
  ctx.fillStyle = s.border.color;
4329
- fillEdge(ctx, rect, s.horizontal ? s.x : s.y, s.border.style, s.horizontal);
4793
+ fillEdge(ctx, options, s.x, s.y, s.w, s.h, s.border.style, s.horizontal);
4794
+ }
4795
+ }
4796
+ /**
4797
+ * One side of a border box at `x, y`, `w` by `h` — `at` counts the top
4798
+ * first and clockwise — as rectangles cut to what the paint reaches: the
4799
+ * top and the bottom the width of the box and the left and the right
4800
+ * between them, so a corner is a square of one side or the other.
4801
+ *
4802
+ * A double side is two lines, a band each along its outer and its inner
4803
+ * edge. Where it meets another double side of its colour (`joins`, the
4804
+ * top left corner first) the lines of the two meet as two frames do, one
4805
+ * inside the other (CSS Backgrounds 3, 4.2 and 4.3): the outer line of the
4806
+ * left or the right runs on to the outer line of the top or the bottom,
4807
+ * and the inner line of the top or the bottom stops at the inner line of
4808
+ * the side. With square corners the outer frame was open at each of them
4809
+ * and the inner line of the top ran on past the side's to the border edge.
4810
+ * Each line is cut to the paint on its own, so a border wider than the
4811
+ * reach around the paint keeps its lines at its edges, and not at the
4812
+ * edges of the part of it cut out.
4813
+ */
4814
+ function fillSide(ctx, options, at, x, y, w, h, widths, style, joins) {
4815
+ const [t, r, b, l] = widths;
4816
+ const width = widths[at];
4817
+ const horizontal = (at & 1) === 0;
4818
+ if (style === 'double' && width >= 3) {
4819
+ const band = doubleBand(width);
4820
+ const line = (lx, ly, lw, lh) => {
4821
+ const rect = clampRect(options, lx, ly, lw, lh);
4822
+ if (rect)
4823
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
4824
+ };
4825
+ if (horizontal) {
4826
+ // the outer line the width of the box, and the inner one from the
4827
+ // inner line of a side it joins, or from the edge of the box
4828
+ const from = joins[at === 0 ? 0 : 3] ? x + l - doubleBand(l) : x;
4829
+ const to = joins[at === 0 ? 1 : 2] ? x + w - r + doubleBand(r) : x + w;
4830
+ line(x, at === 0 ? y : y + h - band, w, band);
4831
+ line(from, at === 0 ? y + t - band : y + h - b, to - from, band);
4832
+ }
4833
+ else {
4834
+ // the outer line from the outer line of a side it joins, or from
4835
+ // that side's inner edge, and the inner one between the two sides
4836
+ const from = joins[at === 3 ? 0 : 1] ? y + doubleBand(t) : y + t;
4837
+ const to = joins[at === 3 ? 3 : 2] ? y + h - doubleBand(b) : y + h - b;
4838
+ line(at === 3 ? x : x + w - band, from, band, to - from);
4839
+ line(at === 3 ? x + l - band : x + w - r, y + t, band, h - t - b);
4840
+ }
4841
+ return;
4330
4842
  }
4843
+ const ex = at === 1 ? x + w - r : x;
4844
+ const ey = at === 0 ? y : at === 2 ? y + h - b : y + t;
4845
+ if (horizontal)
4846
+ fillEdge(ctx, options, ex, ey, w, width, style, true);
4847
+ else
4848
+ fillEdge(ctx, options, ex, ey, width, h - t - b, style, false);
4331
4849
  }
4332
- function fillEdge(ctx, rect, phaseOrigin, style, horizontal) {
4333
- const { x, y, w, h } = rect;
4850
+ /**
4851
+ * The corners of a box where two double sides of one colour meet, the top
4852
+ * left first and clockwise, and whose lines `fillSide` joins. By the
4853
+ * colours as written, a transparent one included, which is how a clip to
4854
+ * the border's area draws the border it does not paint.
4855
+ */
4856
+ function doubleJoins(box) {
4857
+ const s = box.style;
4858
+ const color = (width, style, value) => style === 'double' && width >= 3 ? inkColor(value, s.color) : null;
4859
+ const top = color(box.borderTop, s.borderTopStyle, s.borderTopColor);
4860
+ const bottom = color(box.borderBottom, s.borderBottomStyle, s.borderBottomColor);
4861
+ if (top === null && bottom === null)
4862
+ return UNJOINED;
4863
+ const right = color(box.borderRight, s.borderRightStyle, s.borderRightColor);
4864
+ const left = color(box.borderLeft, s.borderLeftStyle, s.borderLeftColor);
4865
+ return [
4866
+ top !== null && top === left,
4867
+ top !== null && top === right,
4868
+ bottom !== null && bottom === right,
4869
+ bottom !== null && bottom === left,
4870
+ ];
4871
+ }
4872
+ const UNJOINED = [false, false, false, false];
4873
+ /** How wide each of a double border's two lines is, the gap between them
4874
+ * what is left of the side. */
4875
+ function doubleBand(thickness) {
4876
+ return Math.max(1, Math.floor(thickness / 3));
4877
+ }
4878
+ /**
4879
+ * A side at `x, y`, `w` by `h`, running across when `horizontal`, as the
4880
+ * rectangles its style makes, each cut to what the paint reaches. The
4881
+ * dashes, the dots and a double side's lines are measured from the side
4882
+ * and cut after: measured from the cut, a side wider than `CLAMP_PAD` had
4883
+ * dashes as long and as far apart as the cut was wide, starting wherever
4884
+ * it started, and a strip repainted down one dash was striped across it.
4885
+ */
4886
+ function fillEdge(ctx, options, x, y, w, h, style, horizontal) {
4334
4887
  const length = horizontal ? w : h;
4335
4888
  const thickness = horizontal ? h : w;
4336
- if (length <= 0 || thickness <= 0)
4889
+ if (!(length > 0 && thickness > 0))
4337
4890
  return;
4891
+ const fill = (fx, fy, fw, fh) => {
4892
+ const rect = clampRect(options, fx, fy, fw, fh);
4893
+ if (rect)
4894
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
4895
+ };
4338
4896
  if (style === 'dashed' || style === 'dotted') {
4897
+ // what of the side the paint reaches, so that a side far longer than
4898
+ // that is not walked from its start
4899
+ const cut = clampRect(options, x, y, w, h);
4900
+ if (!cut)
4901
+ return;
4339
4902
  const period = style === 'dotted' ? thickness * 2 : thickness * 3;
4340
4903
  const on = style === 'dotted' ? thickness : thickness * 2;
4341
- const from = horizontal ? x : y;
4342
- // Start on the pattern boundary at or before the clamped start, so the
4343
- // dash the viewport cuts into is the same dash it always was.
4344
- let i = Math.floor((from - phaseOrigin) / period) * period + phaseOrigin;
4345
- for (; i < from + length; i += period) {
4346
- const start = Math.max(i, from);
4347
- const run = Math.min(i + on, from + length) - start;
4348
- if (run <= 0)
4349
- continue;
4904
+ const start = horizontal ? x : y;
4905
+ const end = start + length;
4906
+ const from = horizontal ? cut.x : cut.y;
4907
+ const to = from + (horizontal ? cut.w : cut.h);
4908
+ // from the side's start, so the dash the paint cuts into is the one it
4909
+ // always was
4910
+ for (let i = start + Math.floor((from - start) / period) * period; i < to; i += period) {
4911
+ const run = Math.min(i + on, end) - i;
4350
4912
  if (horizontal)
4351
- ctx.fillRect(start, y, run, thickness);
4913
+ fill(i, y, run, thickness);
4352
4914
  else
4353
- ctx.fillRect(x, start, thickness, run);
4915
+ fill(x, i, thickness, run);
4354
4916
  }
4355
4917
  return;
4356
4918
  }
4357
4919
  if (style === 'double' && thickness >= 3) {
4358
- const band = Math.max(1, Math.floor(thickness / 3));
4920
+ const band = doubleBand(thickness);
4359
4921
  if (horizontal) {
4360
- ctx.fillRect(x, y, length, band);
4361
- ctx.fillRect(x, y + thickness - band, length, band);
4922
+ fill(x, y, length, band);
4923
+ fill(x, y + thickness - band, length, band);
4362
4924
  }
4363
4925
  else {
4364
- ctx.fillRect(x, y, band, length);
4365
- ctx.fillRect(x + thickness - band, y, band, length);
4926
+ fill(x, y, band, length);
4927
+ fill(x + thickness - band, y, band, length);
4366
4928
  }
4367
4929
  return;
4368
4930
  }
4369
- ctx.fillRect(x, y, w, h);
4931
+ fill(x, y, w, h);
4370
4932
  }
4371
4933
  /** A list item's bullet or number, in the margin. */
4372
- function paintMarker(ctx, marker, options) {
4934
+ function paintMarker(ctx, marker, options,
4935
+ /** The item's colour scheme, which an SVG image in it is drawn in. */
4936
+ scheme) {
4373
4937
  const x = marker.x + options.originX;
4374
4938
  const y = marker.y + options.originY;
4375
4939
  if (marker.image) {
@@ -4379,7 +4943,7 @@ function paintMarker(ctx, marker, options) {
4379
4943
  if (!loaded || !(width > 0 && height > 0))
4380
4944
  return;
4381
4945
  if (loaded.image instanceof SvgDrawing) {
4382
- loaded.image.draw(ctx, x, y, width, height, options.scale ?? 1);
4946
+ loaded.image.drawImage(ctx, x, y, width, height, options.scale ?? 1, scheme);
4383
4947
  }
4384
4948
  else
4385
4949
  ctx.drawImage?.(loaded.image, x, y, width, height);
@@ -4411,7 +4975,7 @@ function paintImage(ctx, box, options) {
4411
4975
  const past = at.x < x || at.y < y || at.x + at.w > x + w || at.y + at.h > y + h;
4412
4976
  const clipped = (!!corners || past) && pushClip(ctx, { x, y, w, h }, corners);
4413
4977
  if (image instanceof SvgDrawing) {
4414
- image.draw(ctx, at.x, at.y, at.w, at.h, options.scale ?? 1);
4978
+ image.drawImage(ctx, at.x, at.y, at.w, at.h, options.scale ?? 1, box.style.colorScheme);
4415
4979
  }
4416
4980
  else
4417
4981
  ctx.drawImage(image, at.x, at.y, at.w, at.h);
@@ -4499,7 +5063,8 @@ function fitted(box, x, y, w, h) {
4499
5063
  /** An inline `<svg>`, drawn in its content box. Its `currentColor` is the
4500
5064
  * box's `color`, as an icon's is the text's around it, and what it is
4501
5065
  * painted with the box's `fill` and `stroke`, where the document's style
4502
- * sheets set them: an icon set's `.icon { fill: currentColor }`. */
5066
+ * sheets set them: an icon set's `.icon { fill: currentColor }`. What
5067
+ * they give a shape inside it is asked for here (`BoxTree.shapeStyler`). */
4503
5068
  function paintSvg(ctx, box, options) {
4504
5069
  if (!box.el)
4505
5070
  return;
@@ -4511,7 +5076,7 @@ function paintSvg(ctx, box, options) {
4511
5076
  const paint = (value) => value === null || value === 'currentColor' || value === 'none'
4512
5077
  ? value
4513
5078
  : inkColor(value, style.color);
4514
- inlineDrawing(box.el).draw(ctx, x, y, w, h, options.scale ?? 1, style.color, paint(style.fill), paint(style.stroke));
5079
+ inlineDrawing(box.el).draw(ctx, x, y, w, h, options.scale ?? 1, style.color, paint(style.fill), paint(style.stroke), options.shapeStyler?.(box) ?? null);
4515
5080
  }
4516
5081
  /**
4517
5082
  * The X protocol carries glyph positions as Int16, so anything drawn past
@@ -4758,7 +5323,7 @@ function paintClippedText(ctx, block, lines, options) {
4758
5323
  Math.max(natural.y + natural.height, natural.baseline + (natural.descent ?? 0)) +
4759
5324
  2;
4760
5325
  for (const run of natural.runs) {
4761
- const clip = clipBoxOf(text.spans.boxAt?.(run.start) ?? null, block);
5326
+ const clip = clipBoxOf(text.spans.boxAt?.(run.start) ?? null);
4762
5327
  if (!clip)
4763
5328
  continue;
4764
5329
  let byLayout = parts.get(clip);
@@ -4782,8 +5347,8 @@ function paintClippedText(ctx, block, lines, options) {
4782
5347
  const list = [...byLayout.values()];
4783
5348
  const style = clip.style;
4784
5349
  let area;
4785
- if (clip === block)
4786
- area = paddingBox(block, options);
5350
+ if (clip.kind !== 'inline')
5351
+ area = paddingBox(clip, options);
4787
5352
  else {
4788
5353
  // an inline box: its runs, and its padding round them
4789
5354
  let x0 = Infinity;