@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
package/src/html/paint.ts CHANGED
@@ -88,6 +88,7 @@ import type {
88
88
  LineText,
89
89
  Marker,
90
90
  SelectionStyler,
91
+ ShapeStyler,
91
92
  } from './layout/boxes.js';
92
93
  import type { SelectionStyle } from './css/cascade.js';
93
94
  import {
@@ -100,7 +101,7 @@ import {
100
101
  } from './layout/inline.js';
101
102
  import { tableGrid } from './layout/grid.js';
102
103
  import type { Cell } from './layout/grid.js';
103
- import { SvgDrawing, inlineDrawing } from './svg.js';
104
+ import { SvgDrawing, concreteSize, inlineDrawing } from './svg.js';
104
105
  import type { IntrinsicSize } from './svg.js';
105
106
  import type { CollapsedBorder } from './layout/collapse.js';
106
107
 
@@ -247,6 +248,9 @@ export interface PaintOptions {
247
248
  /** @internal Each box's `::selection`, where a rule styles one
248
249
  * (`BoxTree.selectionStyler`). */
249
250
  selectionStyler?: SelectionStyler | null;
251
+ /** @internal What the rules give the shapes in each drawing, where one
252
+ * could reach any (`BoxTree.shapeStyler`). */
253
+ shapeStyler?: ShapeStyler | null;
250
254
  }
251
255
 
252
256
  /**
@@ -705,6 +709,7 @@ export function paintDocument(
705
709
  canvasSource: canvas?.source,
706
710
  negative: tree.negative,
707
711
  selectionStyler: options.selection ? tree.selectionStyler : null,
712
+ shapeStyler: tree.shapeStyler,
708
713
  });
709
714
  ctx.restore();
710
715
  }
@@ -971,20 +976,15 @@ function originBox(
971
976
  * it falls nearest, as browsers snap a box — boxes that meet share the
972
977
  * column their edge is in, and a rule 1.33px wide is one pixel, not two —
973
978
  * with the border box's corners less the widths between (5.3), where
974
- * `rounded` asks for them. Null where it has no area.
979
+ * `rounded` asks for them — cut to what the paint reaches (`clampArea`).
980
+ * Null where it has no area there.
975
981
  */
976
982
  function clipArea(
977
983
  box: Frame,
978
984
  options: PaintOptions,
979
985
  style: ComputedStyle,
980
986
  rounded: boolean,
981
- ): {
982
- x: number;
983
- y: number;
984
- w: number;
985
- h: number;
986
- corners: Corners | null;
987
- } | null {
987
+ ): Area | null {
988
988
  const left = box.x + options.originX;
989
989
  const top = frameY(box) + options.originY;
990
990
  const right = left + box.width;
@@ -1013,7 +1013,7 @@ function clipArea(
1013
1013
  -l,
1014
1014
  );
1015
1015
  }
1016
- return { x, y, w, h, corners };
1016
+ return clampArea(options, x, y, w, h, corners);
1017
1017
  }
1018
1018
 
1019
1019
  /** What the background and border painters read of a box — which an
@@ -1535,7 +1535,9 @@ function paintContent(
1535
1535
  for (const child of below) paintStacked(ctx, child, options);
1536
1536
  }
1537
1537
  if (visible) {
1538
- if (box.marker) paintMarker(ctx, box.marker, options);
1538
+ if (box.marker) {
1539
+ paintMarker(ctx, box.marker, options, box.style.colorScheme);
1540
+ }
1539
1541
  if (box.replaced === 'image') paintImage(ctx, box, options);
1540
1542
  else if (box.replaced === 'svg') paintSvg(ctx, box, options);
1541
1543
  }
@@ -1737,9 +1739,7 @@ function frameImages(
1737
1739
  options: PaintOptions,
1738
1740
  ): (layer: ComputedStyle) => void {
1739
1741
  return (layer) => {
1740
- const painted = clipArea(frame, options, layer, true);
1741
- const area =
1742
- painted && clampRect(options, painted.x, painted.y, painted.w, painted.h);
1742
+ const area = clipArea(frame, options, layer, true);
1743
1743
  if (area) {
1744
1744
  paintBackgroundImage(
1745
1745
  ctx,
@@ -1747,7 +1747,7 @@ function frameImages(
1747
1747
  area,
1748
1748
  originBox(frame, options, layer),
1749
1749
  options,
1750
- painted.corners,
1750
+ area.corners,
1751
1751
  );
1752
1752
  }
1753
1753
  };
@@ -1761,6 +1761,9 @@ function shadowReach(blur: number): number {
1761
1761
 
1762
1762
  const SQUARE: Corners = { x: [0, 0, 0, 0], y: [0, 0, 0, 0] };
1763
1763
 
1764
+ /** A rectangle with no area, where a ring has no inside. */
1765
+ const NOWHERE: Rect = { x: 0, y: 0, width: 0, height: 0 };
1766
+
1764
1767
  /** Corners shrunk by `by` — an inner shadow's spread — or grown where it
1765
1768
  * is negative; a square corner stays square. */
1766
1769
  function grownCorners(c: Corners, by: number): Corners {
@@ -1838,15 +1841,6 @@ function paintShadows(
1838
1841
  ) {
1839
1842
  continue;
1840
1843
  }
1841
- // Cut, as a background is (`clampRect`), to what the paint reaches
1842
- // and as far again as the blur does, so the cut edges cast nothing
1843
- // on it. A shadow down a box thousands of pixels tall went to the
1844
- // server whole, and its outline, sent in 16.16 fixed point, threw.
1845
- const within = reach + CLAMP_PAD;
1846
- const cut = clampAround(options, shape, within);
1847
- if (!cut) continue;
1848
- Object.assign(shape, cut);
1849
- const own = clampAround(options, rect, within) ?? rect;
1850
1844
  const around =
1851
1845
  spreadCorners(
1852
1846
  corners,
@@ -1857,11 +1851,31 @@ function paintShadows(
1857
1851
  s.spread,
1858
1852
  s.spread,
1859
1853
  ) ?? SQUARE;
1854
+ // Cut, as a background is (`clampArea`), to what the paint reaches
1855
+ // and as far again as the blur does, so the cut edges cast nothing
1856
+ // on it, and the box with it, which a ring is the band outside and
1857
+ // the shadow is clipped out of. A shadow down a box thousands of
1858
+ // pixels tall went to the server whole, and its outline, sent in
1859
+ // 16.16 fixed point, threw.
1860
+ const [cut, own] = clampPair(
1861
+ options,
1862
+ reach + CLAMP_PAD,
1863
+ { rect: shape, corners: around },
1864
+ { rect, corners },
1865
+ );
1866
+ if (!cut) continue;
1867
+ const { x, y, width, height } = cut.rect;
1860
1868
  const color = inkColor(s.color, style.color);
1861
1869
  if (!(s.blur > 0) && !s.x && !s.y && s.spread >= 0 && !covered) {
1862
1870
  // a ring about the box: the band between it and the spread
1863
1871
  ctx.fillStyle = color;
1864
- fillRing(ctx, shape, around, own, corners);
1872
+ fillRing(
1873
+ ctx,
1874
+ cut.rect,
1875
+ cut.corners,
1876
+ own ? own.rect : NOWHERE,
1877
+ own ? own.corners : SQUARE,
1878
+ );
1865
1879
  continue;
1866
1880
  }
1867
1881
  if (!(s.blur > 0) && covered) {
@@ -1871,14 +1885,7 @@ function paintShadows(
1871
1885
  s,
1872
1886
  color,
1873
1887
  (dx) => {
1874
- roundedRect(
1875
- ctx,
1876
- shape.x - dx,
1877
- shape.y,
1878
- shape.width,
1879
- shape.height,
1880
- around,
1881
- );
1888
+ roundedRect(ctx, x - dx, y, width, height, cut.corners);
1882
1889
  },
1883
1890
  0,
1884
1891
  );
@@ -1890,21 +1897,24 @@ function paintShadows(
1890
1897
  ctx.save();
1891
1898
  ctx.beginPath();
1892
1899
  ctx.rect!(
1893
- shape.x - reach - 1,
1894
- shape.y - reach - 1,
1895
- shape.width + 2 * reach + 2,
1896
- shape.height + 2 * reach + 2,
1897
- );
1898
- roundedRect(
1899
- ctx,
1900
- own.x,
1901
- own.y,
1902
- own.width,
1903
- own.height,
1904
- corners,
1905
- true,
1906
- true,
1900
+ x - reach - 1,
1901
+ y - reach - 1,
1902
+ width + 2 * reach + 2,
1903
+ height + 2 * reach + 2,
1907
1904
  );
1905
+ if (own) {
1906
+ const { rect: r } = own;
1907
+ roundedRect(
1908
+ ctx,
1909
+ r.x,
1910
+ r.y,
1911
+ r.width,
1912
+ r.height,
1913
+ own.corners,
1914
+ true,
1915
+ true,
1916
+ );
1917
+ }
1908
1918
  ctx.clip!();
1909
1919
  }
1910
1920
  fillShadow(
@@ -1912,16 +1922,9 @@ function paintShadows(
1912
1922
  s,
1913
1923
  color,
1914
1924
  (dx) => {
1915
- shadowShape(
1916
- ctx,
1917
- shape.x - dx,
1918
- shape.y,
1919
- shape.width,
1920
- shape.height,
1921
- around,
1922
- );
1925
+ shadowShape(ctx, x - dx, y, width, height, cut.corners);
1923
1926
  },
1924
- shape.x + shape.width + reach,
1927
+ x + width + reach,
1925
1928
  );
1926
1929
  if (clipped) ctx.restore();
1927
1930
  }
@@ -1947,9 +1950,6 @@ function paintShadows(
1947
1950
  );
1948
1951
  }
1949
1952
  }
1950
- const padCut = clampAround(options, pad, furthest + CLAMP_PAD);
1951
- if (!padCut) return;
1952
- Object.assign(pad, padCut);
1953
1953
  const inner = insetCorners(
1954
1954
  corners,
1955
1955
  box.borderTop,
@@ -1961,32 +1961,50 @@ function paintShadows(
1961
1961
  const s = shadows[i];
1962
1962
  if (!s.inset || (s.blur > 0 && !canShadow)) continue;
1963
1963
  const color = inkColor(s.color, style.color);
1964
- // the hole the shadow is cast around, moved and shrunk by the spread
1965
- const hole = {
1966
- x: pad.x + s.x + s.spread,
1967
- y: pad.y + s.y + s.spread,
1968
- width: Math.max(0, pad.width - 2 * s.spread),
1969
- height: Math.max(0, pad.height - 2 * s.spread),
1970
- };
1971
- const within = grownCorners(inner, -s.spread);
1964
+ // The hole the shadow is cast around, moved and shrunk by the spread,
1965
+ // cut with the padding box: made from the cut one, it was a spread in
1966
+ // from the cut rather than from the box's edge, and a spread wider
1967
+ // than the margin left none.
1968
+ const [area, hole] = clampPair(
1969
+ options,
1970
+ furthest + CLAMP_PAD,
1971
+ { rect: pad, corners: inner },
1972
+ {
1973
+ rect: {
1974
+ x: pad.x + s.x + s.spread,
1975
+ y: pad.y + s.y + s.spread,
1976
+ width: Math.max(0, pad.width - 2 * s.spread),
1977
+ height: Math.max(0, pad.height - 2 * s.spread),
1978
+ },
1979
+ corners: grownCorners(inner, -s.spread),
1980
+ },
1981
+ );
1982
+ if (!area) continue;
1972
1983
  if (!(s.blur > 0) && !s.x && !s.y && s.spread >= 0) {
1973
1984
  // Tailwind's `ring-inset`: a band inside the padding edge
1974
1985
  ctx.fillStyle = color;
1975
- fillRing(ctx, pad, inner, hole, within);
1986
+ fillRing(
1987
+ ctx,
1988
+ area.rect,
1989
+ area.corners,
1990
+ hole ? hole.rect : NOWHERE,
1991
+ hole ? hole.corners : SQUARE,
1992
+ );
1976
1993
  continue;
1977
1994
  }
1978
1995
  if (!ctx.clip || !ctx.save || !ctx.rect) continue;
1996
+ const { x, y, width, height } = area.rect;
1979
1997
  ctx.save();
1980
1998
  ctx.beginPath();
1981
- roundedRect(ctx, pad.x, pad.y, pad.width, pad.height, inner);
1999
+ roundedRect(ctx, x, y, width, height, area.corners);
1982
2000
  ctx.clip();
1983
2001
  // a frame around the hole, as wide as the blur and the offset reach
1984
2002
  const reach = shadowReach(s.blur) + Math.abs(s.x) + Math.abs(s.y) + 1;
1985
2003
  const frame = {
1986
- x: pad.x - reach,
1987
- y: pad.y - reach,
1988
- width: pad.width + 2 * reach,
1989
- height: pad.height + 2 * reach,
2004
+ x: x - reach,
2005
+ y: y - reach,
2006
+ width: width + 2 * reach,
2007
+ height: height + 2 * reach,
1990
2008
  };
1991
2009
  fillShadow(
1992
2010
  ctx,
@@ -1994,15 +2012,9 @@ function paintShadows(
1994
2012
  color,
1995
2013
  (dx) => {
1996
2014
  ctx.rect!(frame.x - dx, frame.y, frame.width, frame.height);
1997
- if (hole.width > 0 && hole.height > 0) {
1998
- shadowShape(
1999
- ctx,
2000
- hole.x - dx,
2001
- hole.y,
2002
- hole.width,
2003
- hole.height,
2004
- within,
2005
- );
2015
+ if (hole) {
2016
+ const { rect: r } = hole;
2017
+ shadowShape(ctx, r.x - dx, r.y, r.width, r.height, hole.corners);
2006
2018
  }
2007
2019
  },
2008
2020
  frame.x + frame.width + shadowReach(s.blur),
@@ -2250,7 +2262,7 @@ function paintFlowLines(
2250
2262
  }
2251
2263
  if (!intersects(child, options)) continue;
2252
2264
  if (child.marker && child.style.visibility === 'visible') {
2253
- paintMarker(ctx, child.marker, options);
2265
+ paintMarker(ctx, child.marker, options, child.style.colorScheme);
2254
2266
  }
2255
2267
  if (child.lines) paintLines(ctx, child, options);
2256
2268
  paintFlowLines(ctx, child, options, outlines);
@@ -3410,6 +3422,92 @@ function clampRect(
3410
3422
  return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
3411
3423
  }
3412
3424
 
3425
+ /** A rectangle with its corners, square where they are null. */
3426
+ interface Area {
3427
+ x: number;
3428
+ y: number;
3429
+ w: number;
3430
+ h: number;
3431
+ corners: Corners | null;
3432
+ }
3433
+
3434
+ /**
3435
+ * `clampRect` for a rectangle that may be rounded, with the corners it is
3436
+ * left with. Where no radius is larger than `CLAMP_PAD` they are its own,
3437
+ * and it is cut as a square one is: a corner the cut leaves on a cut edge
3438
+ * curves no further in from it than the margin, which keeps it out of what
3439
+ * is painted. A larger radius, a circle's or a pill's, would bend into it
3440
+ * — a circle 400 across repainted a strip at a time was another shape in
3441
+ * every strip across its curve — so there the window is moved out past
3442
+ * any curve it would cross, and a corner on a side it cut is square
3443
+ * (`cutRounded`, as a rounded border's ring is cut).
3444
+ */
3445
+ function clampArea(
3446
+ options: PaintOptions,
3447
+ x: number,
3448
+ y: number,
3449
+ w: number,
3450
+ h: number,
3451
+ corners: Corners | null,
3452
+ ): Area | null {
3453
+ if (corners && curvesPast(corners, CLAMP_PAD)) {
3454
+ const shape = { rect: { x, y, width: w, height: h }, corners };
3455
+ const cut = cutRounded(roundedWindow(options, [shape]), shape);
3456
+ if (!cut) return null;
3457
+ const { rect } = cut;
3458
+ return {
3459
+ x: rect.x,
3460
+ y: rect.y,
3461
+ w: rect.width,
3462
+ h: rect.height,
3463
+ corners: cut.corners,
3464
+ };
3465
+ }
3466
+ const rect = clampRect(options, x, y, w, h);
3467
+ return rect && { x: rect.x, y: rect.y, w: rect.w, h: rect.h, corners };
3468
+ }
3469
+
3470
+ /**
3471
+ * Two rounded rectangles drawn against each other — a shadow's shape and
3472
+ * the box it is clipped out of, a hole and what it is cut in — cut to one
3473
+ * window, `pad` round what the paint reaches, as `clampArea` cuts one:
3474
+ * where neither has a corner that curves further than `CLAMP_PAD`, as
3475
+ * square ones are, keeping their corners, and otherwise to a window moved
3476
+ * out past every curve of either it would cross, each square on the sides
3477
+ * it cut. `CLAMP_PAD` and not `pad`, because what a shadow's margin has
3478
+ * past it is what the blur casts back across the cut, and a corner bent
3479
+ * into the margin is cast that far further in. Null for one with nothing
3480
+ * in the window.
3481
+ */
3482
+ function clampPair(
3483
+ options: PaintOptions,
3484
+ pad: number,
3485
+ a: Rounded,
3486
+ b: Rounded,
3487
+ ): [Rounded | null, Rounded | null] {
3488
+ if (!curvesPast(a.corners, CLAMP_PAD) && !curvesPast(b.corners, CLAMP_PAD)) {
3489
+ return [clampKeeping(options, a, pad), clampKeeping(options, b, pad)];
3490
+ }
3491
+ const cut = roundedWindow(options, [a, b], pad);
3492
+ return [cutRounded(cut, a), cutRounded(cut, b)];
3493
+ }
3494
+
3495
+ /** `clampAround` for a rounded rectangle, keeping its corners. */
3496
+ function clampKeeping(
3497
+ options: PaintOptions,
3498
+ { rect, corners }: Rounded,
3499
+ pad: number,
3500
+ ): Rounded | null {
3501
+ const cut = clampAround(options, rect, pad);
3502
+ return cut && { rect: cut, corners };
3503
+ }
3504
+
3505
+ /** Whether a corner curves further than `by` along either of its sides. */
3506
+ function curvesPast({ x, y }: Corners, by: number): boolean {
3507
+ for (let i = 0; i < 4; i += 1) if (x[i] > by || y[i] > by) return true;
3508
+ return false;
3509
+ }
3510
+
3413
3511
  function paintBackground(
3414
3512
  ctx: PaintContext,
3415
3513
  box: Frame,
@@ -3420,20 +3518,16 @@ function paintBackground(
3420
3518
  const gradient = style.backgroundGradient;
3421
3519
  const solid = !isTransparent(color);
3422
3520
  if (!solid && !gradient) return;
3423
- const area = clipArea(
3521
+ const rect = clipArea(
3424
3522
  box,
3425
3523
  options,
3426
3524
  style,
3427
3525
  !!(ctx.roundRect && ctx.fill && ctx.beginPath),
3428
3526
  );
3429
- const rect = area && clampRect(options, area.x, area.y, area.w, area.h);
3430
3527
  if (!rect) return;
3431
- const rounded = area.corners;
3528
+ const rounded = rect.corners;
3432
3529
  const fill = (): void => {
3433
3530
  if (rounded) {
3434
- // The clamp can only have cut edges further than CLAMP_PAD outside
3435
- // the damage, and a sane radius is smaller than that — so a corner
3436
- // that survives the cut is whole, and a cut edge is offscreen.
3437
3531
  ctx.beginPath!();
3438
3532
  roundedRect(ctx, rect.x, rect.y, rect.w, rect.h, rounded);
3439
3533
  ctx.fill!();
@@ -4230,11 +4324,20 @@ function paintBackgroundImage(
4230
4324
  // a drawing is drawn a tile at a time, at the size it was given
4231
4325
  if (tiles <= MAX_TILES) {
4232
4326
  for (let y = fromY; y < toY; y += stepY) {
4233
- for (let x = fromX; x < toX; x += stepX)
4234
- svg.draw(ctx, Math.round(x), Math.round(y), iw, ih, scale);
4327
+ for (let x = fromX; x < toX; x += stepX) {
4328
+ svg.drawImage(
4329
+ ctx,
4330
+ Math.round(x),
4331
+ Math.round(y),
4332
+ iw,
4333
+ ih,
4334
+ scale,
4335
+ style.colorScheme,
4336
+ );
4337
+ }
4235
4338
  }
4236
4339
  } else {
4237
- svg.draw(ctx, x0, y0, iw, ih, scale);
4340
+ svg.drawImage(ctx, x0, y0, iw, ih, scale, style.colorScheme);
4238
4341
  }
4239
4342
  } else if (
4240
4343
  tiles > 1 &&
@@ -4339,7 +4442,7 @@ function roundedTile(
4339
4442
 
4340
4443
  /**
4341
4444
  * A background tile's size (CSS Backgrounds 3, 3.9, over CSS Images' sizing
4342
- * of an object): `auto` is the image's own, as `tileSize` has it; `cover`
4445
+ * of an object): `auto` is the image's own, as `concreteSize` has it; `cover`
4343
4446
  * and `contain` scale it to fill the positioning area or to fit inside it,
4344
4447
  * keeping its ratio, and one with no ratio is the size of the area; and a
4345
4448
  * width or a height alone takes the other from the ratio, or else from the
@@ -4352,7 +4455,8 @@ function sizedTile(
4352
4455
  area: Rect,
4353
4456
  scale: number,
4354
4457
  ): [number, number] {
4355
- if (size === 'auto') return tileSize(image, area, scale);
4458
+ if (size === 'auto')
4459
+ return concreteSize(image, area.width, area.height, scale);
4356
4460
  const { ratio } = image;
4357
4461
  if (size === 'cover' || size === 'contain') {
4358
4462
  if (!(ratio > 0 && area.height > 0)) return [area.width, area.height];
@@ -4373,34 +4477,7 @@ function sizedTile(
4373
4477
  if (ratio > 0) return [h * ratio, h];
4374
4478
  return [image.width === null ? area.width : image.width * scale, h];
4375
4479
  }
4376
- return tileSize(image, area, scale);
4377
- }
4378
-
4379
- /**
4380
- * A background image's size, in device pixels, where nothing sets it — CSS
4381
- * 2.1 has no `background-size` — which is CSS Images' default sizing: an
4382
- * image's own size where it has one; the dimension it lacks from its ratio,
4383
- * or else from the positioning area; and one with a ratio alone as large as
4384
- * fits in the area. An SVG may be any of these, and a raster image is the
4385
- * first.
4386
- */
4387
- function tileSize(
4388
- size: IntrinsicSize,
4389
- area: Rect,
4390
- scale: number,
4391
- ): [number, number] {
4392
- const { ratio } = size;
4393
- const width = size.width === null ? null : size.width * scale;
4394
- const height = size.height === null ? null : size.height * scale;
4395
- if (width !== null && height !== null) return [width, height];
4396
- if (width !== null) return [width, ratio > 0 ? width / ratio : area.height];
4397
- if (height !== null) return [ratio > 0 ? height * ratio : area.width, height];
4398
- if (ratio > 0) {
4399
- return area.width / area.height > ratio
4400
- ? [area.height * ratio, area.height]
4401
- : [area.width, area.width / ratio];
4402
- }
4403
- return [area.width, area.height];
4480
+ return concreteSize(image, area.width, area.height, scale);
4404
4481
  }
4405
4482
 
4406
4483
  /**
@@ -4473,12 +4550,13 @@ function paintOutline(
4473
4550
  *
4474
4551
  * Where two sides that meet differ in colour — one of them transparent
4475
4552
  * included — the corner is cut on the diagonal from its outer point to its
4476
- * inner one and each side takes its half (CSS Backgrounds 3, 4.3), so a side
4553
+ * inner one and each side takes its half (CSS Backgrounds 3, 4.4), so a side
4477
4554
  * is a trapezoid, and a triangle where the box inside the borders has no
4478
4555
  * width: the CSS triangle a dropdown's caret and a tooltip's arrow are
4479
4556
  * drawn with, which came out as the rectangle around it. Only a solid side
4480
4557
  * is cut, and only against a side that is solid or paints nothing; dots,
4481
- * dashes and a double border's two lines keep their rectangles.
4558
+ * dashes and a double border's two lines keep their rectangles, the lines
4559
+ * joined where two double sides of one colour meet (`fillSide`).
4482
4560
  */
4483
4561
  function paintBorders(
4484
4562
  ctx: PaintContext,
@@ -4523,21 +4601,16 @@ function paintBorders(
4523
4601
  paths && mitred(b, r, bottomInk, rightInk, bottomStyle, rightStyle);
4524
4602
  const bl = paths && mitred(b, l, bottomInk, leftInk, bottomStyle, leftStyle);
4525
4603
 
4604
+ const widths = [t, r, b, l];
4605
+ const joins = doubleJoins(box);
4606
+ /** A side with square corners, the top first and clockwise. */
4526
4607
  const edge = (
4527
- ex: number,
4528
- ey: number,
4529
- ew: number,
4530
- eh: number,
4608
+ at: number,
4531
4609
  style: ComputedStyle['borderTopStyle'],
4532
4610
  color: string,
4533
- horizontal: boolean,
4534
4611
  ): void => {
4535
- const rect = clampRect(options, ex, ey, ew, eh);
4536
- if (!rect) return;
4537
4612
  ctx.fillStyle = color;
4538
- // The un-clamped start is the dash phase's origin, so the pattern does
4539
- // not crawl as the viewport moves along a long edge.
4540
- fillEdge(ctx, rect, horizontal ? ex : ey, style, horizontal);
4613
+ fillSide(ctx, options, at, x, y, w, h, widths, style, joins);
4541
4614
  };
4542
4615
  /** A side with a cut corner: its outer edge and then its inner one. */
4543
4616
  const side = (points: number[], color: string): void => {
@@ -4568,7 +4641,7 @@ function paintBorders(
4568
4641
  [x, y, x1, y, cutRight ? px1 : x1, py0, cutLeft ? px0 : x, py0],
4569
4642
  topInk,
4570
4643
  );
4571
- } else edge(x, y, w, t, topStyle, topInk, true);
4644
+ } else edge(0, topStyle, topInk);
4572
4645
  }
4573
4646
  if (bottomInk !== null) {
4574
4647
  const cutLeft = bl && !over(leftInk);
@@ -4578,23 +4651,23 @@ function paintBorders(
4578
4651
  [x1, y1, x, y1, cutLeft ? px0 : x, py1, cutRight ? px1 : x1, py1],
4579
4652
  bottomInk,
4580
4653
  );
4581
- } else edge(x, y1 - b, w, b, bottomStyle, bottomInk, true);
4654
+ } else edge(2, bottomStyle, bottomInk);
4582
4655
  }
4583
4656
  if (leftInk !== null) {
4584
4657
  if (tl || bl) {
4585
4658
  side([x, bl ? y1 : py1, x, tl ? y : py0, px0, py0, px0, py1], leftInk);
4586
- } else edge(x, y + t, l, h - t - b, leftStyle, leftInk, false);
4659
+ } else edge(3, leftStyle, leftInk);
4587
4660
  }
4588
4661
  if (rightInk !== null) {
4589
4662
  if (tr || br) {
4590
4663
  side([x1, tr ? y : py0, x1, br ? y1 : py1, px1, py1, px1, py0], rightInk);
4591
- } else edge(x1 - r, y + t, r, h - t - b, rightStyle, rightInk, false);
4664
+ } else edge(1, rightStyle, rightInk);
4592
4665
  }
4593
4666
  }
4594
4667
 
4595
4668
  /**
4596
4669
  * Whether the corner two sides meet at is cut on its diagonal, each side
4597
- * taking its half (CSS Backgrounds 3, 4.3): where both have a width, they
4670
+ * taking its half (CSS Backgrounds 3, 4.4): where both have a width, they
4598
4671
  * are painted in different colours or only one is painted at all, and each
4599
4672
  * is solid or unpainted. Sides of one colour make the same corner either
4600
4673
  * way, and so does a corner of a single pixel, which is left to the side
@@ -4616,12 +4689,14 @@ function mitred(
4616
4689
  }
4617
4690
 
4618
4691
  /**
4619
- * Fill a convex polygon, its corners as `x, y` pairs, cut to `area`: the
4620
- * part of it near what is painted, which is what keeps a side thousands of
4621
- * pixels long inside X's coordinates. Cut edge by edge rather than by
4622
- * moving its corners in, which would turn a diagonal that crosses the
4623
- * painted area: a slanted divider, `border-left: 100vw solid transparent`,
4624
- * has a corner far outside it.
4692
+ * Fill a polygon, its corners as `x, y` pairs, cut to `area`: the part of
4693
+ * it near what is painted, which is what keeps a side thousands of pixels
4694
+ * long inside X's coordinates. Cut edge by edge rather than by moving its
4695
+ * corners in, which would turn a diagonal that crosses the painted area: a
4696
+ * slanted divider, `border-left: 100vw solid transparent`, has a corner far
4697
+ * outside it. A polygon that is not convex, a rounded ring's share, may
4698
+ * come out of the cut with an edge run there and back along the area's
4699
+ * border, which fills nothing, and that border is outside the paint.
4625
4700
  */
4626
4701
  function fillPolygon(
4627
4702
  ctx: PaintContext,
@@ -4698,9 +4773,7 @@ function paintBorderImage(
4698
4773
  // the image's size, CSS Images' default sizing in the area: a drawing
4699
4774
  // with no size of its own is as large as it fits there, and a gradient,
4700
4775
  // which has none, is the area's
4701
- const [cw, ch] = loaded
4702
- ? tileSize(loaded, { x: 0, y: 0, width: aw, height: ah }, scale)
4703
- : [aw, ah];
4776
+ const [cw, ch] = loaded ? concreteSize(loaded, aw, ah, scale) : [aw, ah];
4704
4777
  const iw = cw / scale;
4705
4778
  const ih = ch / scale;
4706
4779
  if (!(iw > 0 && ih > 0)) return false;
@@ -4715,12 +4788,13 @@ function paintBorderImage(
4715
4788
  const svg = image instanceof SvgDrawing ? image : null;
4716
4789
  const w = Math.max(1, Math.round(cw));
4717
4790
  const h = Math.max(1, Math.round(ch));
4791
+ const scheme = box.style.colorScheme;
4718
4792
  const drawn = options.cached?.(
4719
- `border-image|${key}|${w}x${h}`,
4793
+ `border-image|${key}|${w}x${h}|${scheme}`,
4720
4794
  w,
4721
4795
  h,
4722
4796
  (sctx) => {
4723
- if (svg) svg.draw(sctx, 0, 0, w, h, scale);
4797
+ if (svg) svg.drawImage(sctx, 0, 0, w, h, scale, scheme);
4724
4798
  else if (typeof source !== 'string') {
4725
4799
  const paint = gradientFill(sctx, source, 0, 0, w, h, box.style.color);
4726
4800
  if (paint) fillGradient(sctx, paint, { x: 0, y: 0, w, h });
@@ -4907,13 +4981,14 @@ function edgeRun(
4907
4981
 
4908
4982
  /**
4909
4983
  * A rounded box's border as the ring between its border edge and its
4910
- * padding edge, each rounded, where every side that has a border has one
4911
- * of the same colour and a solid rule: a card's or a button's. Drawn
4912
- * straight, its corners were square over the background's rounded ones,
4913
- * and an accent border down one side did not follow the corner. The inner
4914
- * radius is the outer less the wider border at that corner (CSS
4915
- * Backgrounds 3, 5.2). False where the border is not such a one, and it is
4916
- * drawn a side at a time.
4984
+ * padding edge, each rounded, where every side that has a border has a
4985
+ * solid rule: a card's or a button's. Drawn straight, its corners were
4986
+ * square over the background's rounded ones, and an accent border down one
4987
+ * side did not follow the corner. The inner radius is the outer less the
4988
+ * wider border at that corner (CSS Backgrounds 3, 5.2). Sides of one colour
4989
+ * are one fill; sides of more are each given their share of the ring
4990
+ * (`ringSides`). False where the border is not such a one, and it is drawn
4991
+ * a side at a time.
4917
4992
  */
4918
4993
  function roundedRing(
4919
4994
  ctx: PaintContext,
@@ -4925,7 +5000,7 @@ function roundedRing(
4925
5000
  options: PaintOptions,
4926
5001
  ): boolean {
4927
5002
  const s = box.style;
4928
- if (!ctx.roundRect || !ctx.fill || !ctx.beginPath) return false;
5003
+ if (!ctx.fill || !ctx.beginPath) return false;
4929
5004
  const corners = cornersOf(s, w, h);
4930
5005
  if (!corners) return false;
4931
5006
  const sides: [number, string, string][] = [
@@ -4935,35 +5010,550 @@ function roundedRing(
4935
5010
  [box.borderLeft, s.borderLeftColor, s.borderLeftStyle],
4936
5011
  ];
4937
5012
  let color: string | null = null;
5013
+ let mixed = false;
4938
5014
  for (const [width, ink, style] of sides) {
4939
5015
  if (!width) continue;
4940
- if (style !== 'solid' || (color !== null && ink !== color)) return false;
5016
+ if (style !== 'solid') return false;
5017
+ if (color !== null && ink !== color) mixed = true;
4941
5018
  color = ink;
4942
5019
  }
5020
+ if (mixed) return ringSides(ctx, box, x, y, w, h, corners, options);
5021
+ if (!ctx.roundRect) return false;
4943
5022
  if (color === null) return true;
4944
5023
  if (isTransparent(color)) return true;
4945
- const rect = clampRect(options, x, y, w, h);
4946
- if (!rect) return true;
4947
- const top = box.borderTop;
4948
- const right = box.borderRight;
4949
- const bottom = box.borderBottom;
4950
- const left = box.borderLeft;
4951
- ctx.fillStyle = inkColor(color, s.color);
5024
+ wholeRing(ctx, box, inkColor(color, s.color), x, y, w, h, corners, options);
5025
+ return true;
5026
+ }
5027
+
5028
+ /** The ring of a rounded border in one colour, cut to what the paint
5029
+ * reaches (`cutRing`). */
5030
+ function wholeRing(
5031
+ ctx: PaintContext,
5032
+ box: Frame,
5033
+ color: string,
5034
+ x: number,
5035
+ y: number,
5036
+ w: number,
5037
+ h: number,
5038
+ corners: Corners,
5039
+ options: PaintOptions,
5040
+ ): void {
5041
+ const ring = cutRing(
5042
+ options,
5043
+ x,
5044
+ y,
5045
+ w,
5046
+ h,
5047
+ corners,
5048
+ box.borderTop,
5049
+ box.borderRight,
5050
+ box.borderBottom,
5051
+ box.borderLeft,
5052
+ );
5053
+ if (!ring) return;
5054
+ const { outer, inner } = ring;
5055
+ ctx.fillStyle = color;
4952
5056
  fillRing(
4953
5057
  ctx,
4954
- { x: rect.x, y: rect.y, width: rect.w, height: rect.h },
4955
- corners,
4956
- {
4957
- x: rect.x + left,
4958
- y: rect.y + top,
4959
- width: Math.max(0, rect.w - left - right),
4960
- height: Math.max(0, rect.h - top - bottom),
4961
- },
4962
- insetCorners(corners, top, right, bottom, left),
5058
+ outer.rect,
5059
+ outer.corners,
5060
+ inner ? inner.rect : NOWHERE,
5061
+ inner ? inner.corners : outer.corners,
5062
+ );
5063
+ }
5064
+
5065
+ /** How far a chord of a flattened corner may stray from its curve, in
5066
+ * device pixels: a sixteenth, well inside what antialiasing shows. */
5067
+ const CURVE_TOLERANCE = 1 / 16;
5068
+
5069
+ /** How far past a cut a share reaches under the one drawn over it, in
5070
+ * device pixels: past the antialiased edge of either. */
5071
+ const SEAM = 1.5;
5072
+
5073
+ /** Which way a border's sides run at each corner, clockwise from the top
5074
+ * left: the side that ends at corner i arrives along `ALONG[i]`, and the
5075
+ * side that begins there leaves along `ALONG[i + 1]`. */
5076
+ const ALONG: readonly (readonly [number, number])[] = [
5077
+ [0, -1],
5078
+ [1, 0],
5079
+ [0, 1],
5080
+ [-1, 0],
5081
+ ];
5082
+
5083
+ /**
5084
+ * A rounded border whose solid sides are not all one colour, each side's
5085
+ * share of the ring filled in its own. Where two sides of different colours
5086
+ * meet, the corner is cut on the line from the border box's corner through
5087
+ * the padding box's, as Blink cuts it: CSS Backgrounds 3, 4.4 puts the
5088
+ * transition on the curve at a point that follows the ratio of the two
5089
+ * widths and leaves the rest to the UA. A corner between a side and one
5090
+ * with no width is all the side's (4.4 again), and sides of one colour that
5091
+ * meet are one fill, so nothing is cut where the colour does not change.
5092
+ * Drawn a side at a time, the spinner the web draws — `border-radius: 50%`
5093
+ * with one side of another colour — was a square frame.
5094
+ *
5095
+ * A share is a polygon: the curves flattened to chords no further than
5096
+ * `CURVE_TOLERANCE` from them, so that it is cut to what the paint reaches
5097
+ * as the square corners are (`fillPolygon`) and a large ring repainted a
5098
+ * strip at a time is the same ring. False where the context draws no
5099
+ * lines, and the sides are drawn straight.
5100
+ */
5101
+ function ringSides(
5102
+ ctx: PaintContext,
5103
+ box: Frame,
5104
+ x: number,
5105
+ y: number,
5106
+ w: number,
5107
+ h: number,
5108
+ corners: Corners,
5109
+ options: PaintOptions,
5110
+ ): boolean {
5111
+ if (!ctx.moveTo || !ctx.lineTo) return false;
5112
+ const area = clampRect(options, x, y, w, h);
5113
+ if (!area) return true;
5114
+ const s = box.style;
5115
+ const widths = [
5116
+ box.borderTop,
5117
+ box.borderRight,
5118
+ box.borderBottom,
5119
+ box.borderLeft,
5120
+ ];
5121
+ const colors = [
5122
+ s.borderTopColor,
5123
+ s.borderRightColor,
5124
+ s.borderBottomColor,
5125
+ s.borderLeftColor,
5126
+ ];
5127
+ // what each side is painted in, null where it paints nothing
5128
+ const inks = widths.map((width, i) =>
5129
+ width > 0 && !isTransparent(colors[i])
5130
+ ? inkColor(colors[i], s.color)
5131
+ : null,
4963
5132
  );
5133
+ // Corners from the top left, clockwise. Corner i is where side i - 1
5134
+ // ends and side i begins: the top left ends the left and begins the top.
5135
+ // Two sides of one colour are joined across their corner.
5136
+ const joined = [0, 1, 2, 3].map((i) => {
5137
+ const before = (i + 3) % 4;
5138
+ return widths[before] > 0 && widths[i] > 0 && inks[before] === inks[i];
5139
+ });
5140
+ if (!joined.includes(false)) {
5141
+ // one colour after all, `currentColor` and the colour it is
5142
+ if (!ctx.roundRect) return false;
5143
+ if (inks[0] !== null) {
5144
+ wholeRing(ctx, box, inks[0], x, y, w, h, corners, options);
5145
+ }
5146
+ return true;
5147
+ }
5148
+ const [t, r, b, l] = widths;
5149
+ const x1 = x + w;
5150
+ const y1 = y + h;
5151
+ // the padding box, inside the borders
5152
+ const ix0 = x + l;
5153
+ const iy0 = y + t;
5154
+ const ix1 = Math.max(ix0, x1 - r);
5155
+ const iy1 = Math.max(iy0, y1 - b);
5156
+ const inside = insetCorners(corners, t, r, b, l);
5157
+ const hollow = ix1 > ix0 && iy1 > iy0;
5158
+ // each corner's point on the border box and on the padding box, which
5159
+ // way it points, and the quarter ellipses that round the two there
5160
+ const sx = [-1, 1, 1, -1];
5161
+ const sy = [-1, -1, 1, 1];
5162
+ const ox = [x, x1, x1, x];
5163
+ const oy = [y, y, y1, y1];
5164
+ const px = [ix0, ix1, ix1, ix0];
5165
+ const py = [iy0, iy0, iy1, iy1];
5166
+ type Quarter = { cx: number; cy: number; rx: number; ry: number } | null;
5167
+ const quarter = (
5168
+ i: number,
5169
+ cornerX: number,
5170
+ cornerY: number,
5171
+ rx: number,
5172
+ ry: number,
5173
+ ): Quarter =>
5174
+ rx > 0 && ry > 0
5175
+ ? { cx: cornerX - sx[i] * rx, cy: cornerY - sy[i] * ry, rx, ry }
5176
+ : null;
5177
+ const outer: Quarter[] = [];
5178
+ const inner: Quarter[] = [];
5179
+ for (let i = 0; i < 4; i += 1) {
5180
+ outer.push(quarter(i, ox[i], oy[i], corners.x[i], corners.y[i]));
5181
+ inner.push(
5182
+ hollow ? quarter(i, px[i], py[i], inside.x[i], inside.y[i]) : null,
5183
+ );
5184
+ }
5185
+ // A corner's quarter turns clockwise from the angle it starts at: the top
5186
+ // left's from pointing left to pointing up.
5187
+ const QUARTER = Math.PI / 2;
5188
+ const start = (i: number): number => Math.PI + i * QUARTER;
5189
+ /** Where a line from (fromX, fromY) along (dx, dy) first meets a quarter
5190
+ * ellipse, as the angle it is at; the line starts outside it. */
5191
+ const crossing = (
5192
+ i: number,
5193
+ q: Quarter,
5194
+ fromX: number,
5195
+ fromY: number,
5196
+ dx: number,
5197
+ dy: number,
5198
+ ): number => {
5199
+ const a0 = start(i);
5200
+ if (!q) return a0;
5201
+ const ux = (fromX - q.cx) / q.rx;
5202
+ const uy = (fromY - q.cy) / q.ry;
5203
+ const vx = dx / q.rx;
5204
+ const vy = dy / q.ry;
5205
+ const qa = vx * vx + vy * vy;
5206
+ const qb = ux * vx + uy * vy;
5207
+ const disc = qb * qb - qa * (ux * ux + uy * uy - 1);
5208
+ const k = qa > 0 && disc > 0 ? (-qb - Math.sqrt(disc)) / qa : 0;
5209
+ let a = Math.atan2(uy + k * vy, ux + k * vx);
5210
+ // into the quarter's own turn
5211
+ a += 2 * Math.PI * Math.round((a0 + QUARTER / 2 - a) / (2 * Math.PI));
5212
+ return Math.min(a0 + QUARTER, Math.max(a0, a));
5213
+ };
5214
+ // Where the shares meet at each corner, as angles on its two quarters: on
5215
+ // the cut, or at the end of the corner where one side has no width and
5216
+ // the other has all of it — the side that has none paints nothing.
5217
+ const cutOuter: number[] = [];
5218
+ const cutInner: number[] = [];
5219
+ for (let i = 0; i < 4; i += 1) {
5220
+ const before = widths[(i + 3) % 4] > 0;
5221
+ const after = widths[i] > 0;
5222
+ if (before && after) {
5223
+ const dx = px[i] - ox[i];
5224
+ const dy = py[i] - oy[i];
5225
+ cutOuter.push(crossing(i, outer[i], ox[i], oy[i], dx, dy));
5226
+ cutInner.push(crossing(i, inner[i], px[i], py[i], dx, dy));
5227
+ } else {
5228
+ const end = before ? start(i) + QUARTER : start(i);
5229
+ cutOuter.push(end);
5230
+ cutInner.push(end);
5231
+ }
5232
+ }
5233
+ // The shares, a run of sides of one colour each, from the corner the
5234
+ // colour changes at to the next one it changes at. Those that show what
5235
+ // is under them are drawn first.
5236
+ const runs: { from: number; to: number; ink: string | null }[] = [];
5237
+ for (let i = 0; i < 4; i += 1) {
5238
+ if (joined[i]) continue;
5239
+ let j = (i + 1) % 4;
5240
+ while (joined[j]) j = (j + 1) % 4;
5241
+ runs.push({ from: i, to: j, ink: inks[i] });
5242
+ }
5243
+ const opaque = (ink: string | null): boolean =>
5244
+ ink !== null && alphaOf(ink) === 1;
5245
+ runs.sort((a, b) => Number(opaque(a.ink)) - Number(opaque(b.ink)));
5246
+ /** Whether a share reaches past its cut at a corner, under the one on
5247
+ * the other side of it: where that one is drawn after it and covers it. */
5248
+ const under = (k: number, corner: number): boolean => {
5249
+ const before = widths[(corner + 3) % 4] > 0;
5250
+ if (!(before && widths[corner] > 0)) return false;
5251
+ const other = runs.findIndex(
5252
+ (run) => run !== runs[k] && (run.from === corner || run.to === corner),
5253
+ );
5254
+ return other > k && opaque(runs[other].ink);
5255
+ };
5256
+ // The straight part of each side's edge, between the quarters at its
5257
+ // ends, on the border edge and the padding edge: as far as a share may
5258
+ // reach along it past a corner.
5259
+ const straight = (q: Quarter[], across: number, down: number): number[] => {
5260
+ const rx = (i: number) => q[i]?.rx ?? 0;
5261
+ const ry = (i: number) => q[i]?.ry ?? 0;
5262
+ return [
5263
+ across - rx(0) - rx(1),
5264
+ down - ry(1) - ry(2),
5265
+ across - rx(2) - rx(3),
5266
+ down - ry(3) - ry(0),
5267
+ ].map((v) => Math.max(0, v));
5268
+ };
5269
+ const outerRoom = straight(outer, w, h);
5270
+ const innerRoom = straight(inner, ix1 - ix0, iy1 - iy0);
5271
+ /**
5272
+ * A share's end moved past the cut at a corner, onto the other share,
5273
+ * along an edge from the angle `a` it is at on a quarter: `forward` the
5274
+ * way the corner turns, onto the side that begins there, or back onto
5275
+ * the side that ends there. Far enough that the line it ends on is
5276
+ * `SEAM` from the cut, whatever angle the two make; as the angle it
5277
+ * reaches on the quarter and how far it goes on along the straight edge
5278
+ * past it, no further than `room`.
5279
+ */
5280
+ const past = (
5281
+ q: Quarter,
5282
+ i: number,
5283
+ a: number,
5284
+ forward: boolean,
5285
+ room: number,
5286
+ ): [number, number] => {
5287
+ const end = forward ? start(i) + QUARTER : start(i);
5288
+ const sign = forward ? 1 : -1;
5289
+ const [ex, ey] = forward ? ALONG[(i + 1) % 4] : ALONG[i];
5290
+ // the way it goes, and the sine of its angle with the cut
5291
+ let tx = forward ? ex : -ex;
5292
+ let ty = forward ? ey : -ey;
5293
+ if (q && a !== end) {
5294
+ const speed = Math.hypot(q.rx * Math.sin(a), q.ry * Math.cos(a));
5295
+ tx = (sign * -q.rx * Math.sin(a)) / speed;
5296
+ ty = (sign * q.ry * Math.cos(a)) / speed;
5297
+ }
5298
+ const dx = px[i] - ox[i];
5299
+ const dy = py[i] - oy[i];
5300
+ const sine = Math.abs(tx * dy - ty * dx) / Math.hypot(dx, dy);
5301
+ let left = SEAM / Math.max(0.25, sine);
5302
+ let at = a;
5303
+ for (let n = 0; q && n < 8 && left > 0 && at !== end; n += 1) {
5304
+ const speed = Math.hypot(q.rx * Math.sin(at), q.ry * Math.cos(at));
5305
+ const turn = Math.abs(end - at);
5306
+ if (left >= turn * speed) {
5307
+ left -= turn * speed;
5308
+ at = end;
5309
+ } else {
5310
+ at += (sign * left) / speed;
5311
+ left = 0;
5312
+ }
5313
+ }
5314
+ return [at, at === end ? Math.min(left, room) : 0];
5315
+ };
5316
+ const points: number[] = [];
5317
+ /** The point at an angle on a quarter, or the corner where it is square,
5318
+ * and `over` on past the quarter's end along the edge `along`. */
5319
+ const point = (
5320
+ q: Quarter,
5321
+ a: number,
5322
+ cx: number,
5323
+ cy: number,
5324
+ over = 0,
5325
+ along: readonly number[] = ALONG[0],
5326
+ ): void => {
5327
+ const x = (q ? q.cx + q.rx * Math.cos(a) : cx) + over * along[0];
5328
+ const y = (q ? q.cy + q.ry * Math.sin(a) : cy) + over * along[1];
5329
+ const n = points.length;
5330
+ if (n && points[n - 2] === x && points[n - 1] === y) return;
5331
+ points.push(x, y);
5332
+ };
5333
+ /** A quarter's curve from one angle to another, as chords, after the
5334
+ * point it starts at. */
5335
+ const sweep = (q: Quarter, from: number, to: number): void => {
5336
+ if (!q || from === to) return;
5337
+ const radius = Math.max(q.rx, q.ry);
5338
+ const step =
5339
+ radius > CURVE_TOLERANCE
5340
+ ? 2 * Math.acos(1 - CURVE_TOLERANCE / radius)
5341
+ : QUARTER;
5342
+ const n = Math.min(64, Math.ceil(Math.abs(to - from) / step));
5343
+ for (let k = 1; k <= n; k += 1) {
5344
+ point(q, from + ((to - from) * k) / n, 0, 0);
5345
+ }
5346
+ };
5347
+ for (let k = 0; k < runs.length; k += 1) {
5348
+ const { from: i, to: j, ink } = runs[k];
5349
+ if (ink === null) continue;
5350
+ // Where the share starts and ends on each edge. Two antialiased fills
5351
+ // that meet on a line each cover half of the pixels along it, and what
5352
+ // is under the border shows through between them. So a share reaches
5353
+ // on under the one drawn after it and covering it, and that one's
5354
+ // edge is the cut, over it — as Blink overdraws a corner, and as the
5355
+ // square corners are drawn.
5356
+ let [so, soOver] = [cutOuter[i], 0];
5357
+ let [si, siOver] = [cutInner[i], 0];
5358
+ let [eo, eoOver] = [cutOuter[j], 0];
5359
+ let [ei, eiOver] = [cutInner[j], 0];
5360
+ const back = (i + 3) % 4;
5361
+ if (under(k, i)) {
5362
+ [so, soOver] = past(outer[i], i, so, false, outerRoom[back]);
5363
+ [si, siOver] = past(inner[i], i, si, false, innerRoom[back]);
5364
+ }
5365
+ if (under(k, j)) {
5366
+ [eo, eoOver] = past(outer[j], j, eo, true, outerRoom[j]);
5367
+ [ei, eiOver] = past(inner[j], j, ei, true, innerRoom[j]);
5368
+ }
5369
+ const onward = ALONG[(j + 1) % 4];
5370
+ const backward = [-ALONG[i][0], -ALONG[i][1]];
5371
+ points.length = 0;
5372
+ // along the border edge, clockwise
5373
+ point(outer[i], so, ox[i], oy[i], soOver, backward);
5374
+ point(outer[i], so, ox[i], oy[i]);
5375
+ sweep(outer[i], so, start(i) + QUARTER);
5376
+ for (let c = (i + 1) % 4; ; c = (c + 1) % 4) {
5377
+ point(outer[c], start(c), ox[c], oy[c]);
5378
+ if (c === j) break;
5379
+ sweep(outer[c], start(c), start(c) + QUARTER);
5380
+ }
5381
+ sweep(outer[j], start(j), eo);
5382
+ point(outer[j], eo, ox[j], oy[j], eoOver, onward);
5383
+ // and back along the padding edge
5384
+ point(inner[j], ei, px[j], py[j], eiOver, onward);
5385
+ point(inner[j], ei, px[j], py[j]);
5386
+ sweep(inner[j], ei, start(j));
5387
+ for (let c = (j + 3) % 4; ; c = (c + 3) % 4) {
5388
+ point(inner[c], start(c) + QUARTER, px[c], py[c]);
5389
+ if (c === i) break;
5390
+ sweep(inner[c], start(c) + QUARTER, start(c));
5391
+ }
5392
+ sweep(inner[i], start(i) + QUARTER, si);
5393
+ point(inner[i], si, px[i], py[i], siOver, backward);
5394
+ ctx.fillStyle = ink;
5395
+ fillPolygon(ctx, points, area);
5396
+ }
4964
5397
  return true;
4965
5398
  }
4966
5399
 
5400
+ /** A rectangle and the radii of its corners. */
5401
+ interface Rounded {
5402
+ rect: Rect;
5403
+ corners: Corners;
5404
+ }
5405
+
5406
+ /**
5407
+ * A rounded box's ring — its border edge and its padding edge, the box
5408
+ * inside its borders — cut to the neighbourhood of what is being painted,
5409
+ * as `clampRect` cuts a rectangle. Both edges are the box's own, cut to
5410
+ * one window, so what is left is the part of the true ring in it. Cutting
5411
+ * the box first and insetting the cut by the borders put the hole a
5412
+ * border's width in from the cut rather than from the box's edge, and left
5413
+ * none where a border was wider than `CLAMP_PAD`: the ring filled whatever
5414
+ * of the box a paint reached. The window is the border edge's
5415
+ * (`roundedWindow`), whose curves the padding edge's run inside, and each
5416
+ * edge is cut to it as `cutRounded` cuts one. Null where the ring has
5417
+ * nothing in the window.
5418
+ */
5419
+ function cutRing(
5420
+ options: PaintOptions,
5421
+ x: number,
5422
+ y: number,
5423
+ w: number,
5424
+ h: number,
5425
+ corners: Corners,
5426
+ top: number,
5427
+ right: number,
5428
+ bottom: number,
5429
+ left: number,
5430
+ ): { outer: Rounded; inner: Rounded | null } | null {
5431
+ const edge = { rect: { x, y, width: w, height: h }, corners };
5432
+ const cut = roundedWindow(options, [edge]);
5433
+ const outer = cutRounded(cut, edge);
5434
+ if (!outer) return null;
5435
+ const iw = w - left - right;
5436
+ const ih = h - top - bottom;
5437
+ const inner =
5438
+ iw > 0 && ih > 0
5439
+ ? cutRounded(cut, {
5440
+ rect: { x: x + left, y: y + top, width: iw, height: ih },
5441
+ corners: insetCorners(corners, top, right, bottom, left),
5442
+ })
5443
+ : null;
5444
+ // a window inside the padding edge, where the ring has nothing
5445
+ if (inner && sameRounded(outer, inner)) return null;
5446
+ return { outer, inner };
5447
+ }
5448
+
5449
+ /** Where a paint is cut to (`roundedWindow`): left, top, right, bottom. */
5450
+ type Cut = [number, number, number, number];
5451
+
5452
+ /**
5453
+ * The window `clampRect` cuts to, `pad` round what the paint reaches, for
5454
+ * rectangles with rounded corners: a cut is straight, so a side of it is
5455
+ * moved out past any corner's curve it would cross, in any of them.
5456
+ */
5457
+ function roundedWindow(
5458
+ options: PaintOptions,
5459
+ shapes: readonly Rounded[],
5460
+ pad = CLAMP_PAD,
5461
+ ): Cut {
5462
+ // where each corner's curve runs, across and down
5463
+ const across: [number, number][] = [];
5464
+ const down: [number, number][] = [];
5465
+ for (const { rect, corners } of shapes) {
5466
+ const { x, y, width: w, height: h } = rect;
5467
+ const { x: cx, y: cy } = corners;
5468
+ across.push(
5469
+ [x, x + cx[0]],
5470
+ [x + w - cx[1], x + w],
5471
+ [x + w - cx[2], x + w],
5472
+ [x, x + cx[3]],
5473
+ );
5474
+ down.push(
5475
+ [y, y + cy[0]],
5476
+ [y, y + cy[1]],
5477
+ [y + h - cy[2], y + h],
5478
+ [y + h - cy[3], y + h],
5479
+ );
5480
+ }
5481
+ const d = options.damage;
5482
+ const side = (at: number, spans: [number, number][], towards: number) =>
5483
+ Math.max(
5484
+ -COORD_LIMIT,
5485
+ Math.min(COORD_LIMIT, outOfSpans(at, spans, towards)),
5486
+ );
5487
+ return [
5488
+ side(d ? d.x - pad : -COORD_LIMIT, across, -1),
5489
+ side(d ? d.y - pad : -COORD_LIMIT, down, -1),
5490
+ side(d ? d.x + d.width + pad : COORD_LIMIT, across, 1),
5491
+ side(d ? d.y + d.height + pad : COORD_LIMIT, down, 1),
5492
+ ];
5493
+ }
5494
+
5495
+ /**
5496
+ * A rounded rectangle cut to a window (`roundedWindow`), as `clampRect`
5497
+ * cuts a rectangle: a corner on a side the window cut is square, its
5498
+ * curve being outside, and the rest are whole. Null where none of it is in
5499
+ * the window.
5500
+ */
5501
+ function cutRounded(
5502
+ [x0, y0, x1, y1]: Cut,
5503
+ { rect: { x, y, width: w, height: h }, corners: c }: Rounded,
5504
+ ): Rounded | null {
5505
+ const l = Math.max(x, x0);
5506
+ const t = Math.max(y, y0);
5507
+ const r = Math.min(x + w, x1);
5508
+ const b = Math.min(y + h, y1);
5509
+ if (r <= l || b <= t) return null;
5510
+ // the corners of the sides the window left where they were
5511
+ const kept = [
5512
+ t === y && l === x,
5513
+ t === y && r === x + w,
5514
+ b === y + h && r === x + w,
5515
+ b === y + h && l === x,
5516
+ ];
5517
+ return {
5518
+ rect: { x: l, y: t, width: r - l, height: b - t },
5519
+ corners: {
5520
+ x: c.x.map((v, i) => (kept[i] ? v : 0)) as Corners['x'],
5521
+ y: c.y.map((v, i) => (kept[i] ? v : 0)) as Corners['y'],
5522
+ },
5523
+ };
5524
+ }
5525
+
5526
+ /** A position moved out of any of `spans` it is inside, towards their
5527
+ * starts where `towards` is negative and their ends where it is not. */
5528
+ function outOfSpans(
5529
+ at: number,
5530
+ spans: readonly [number, number][],
5531
+ towards: number,
5532
+ ): number {
5533
+ for (let moved = true; moved;) {
5534
+ moved = false;
5535
+ for (const [from, to] of spans) {
5536
+ if (at > from && at < to) {
5537
+ at = towards < 0 ? from : to;
5538
+ moved = true;
5539
+ }
5540
+ }
5541
+ }
5542
+ return at;
5543
+ }
5544
+
5545
+ /** Whether two rounded rectangles are one shape. */
5546
+ function sameRounded(a: Rounded, b: Rounded): boolean {
5547
+ return (
5548
+ a.rect.x === b.rect.x &&
5549
+ a.rect.y === b.rect.y &&
5550
+ a.rect.width === b.rect.width &&
5551
+ a.rect.height === b.rect.height &&
5552
+ a.corners.x.every((v, i) => v === b.corners.x[i]) &&
5553
+ a.corners.y.every((v, i) => v === b.corners.y[i])
5554
+ );
5555
+ }
5556
+
4967
5557
  /**
4968
5558
  * Clip to what a box's border paints, for a layer of `background-clip:
4969
5559
  * border-area` (CSS Backgrounds 4, 2.1): each side's width and style, and
@@ -4971,12 +5561,17 @@ function roundedRing(
4971
5561
  * the value is for. The shapes are the border painter's own, so that the
4972
5562
  * background shows exactly where that border would: the ring `roundedRing`
4973
5563
  * fills where a rounded border is solid, the ring the trapezoids of a 3D
4974
- * style make, and otherwise the rectangles `fillEdge` fills a side at a
4975
- * time — the dots, the dashes, the two lines of a double border. All of
5564
+ * style make, and otherwise the rectangles `fillSide` fills a side at a
5565
+ * time — the dots, the dashes, the two lines of a double border joined as
5566
+ * they are painted, by the sides' colours though neither is drawn. All of
4976
5567
  * them run clockwise and a ring's inside the other way, so the clip is the
4977
- * union by the non-zero rule, the one every context's `clip` takes. False
4978
- * where the border paints nothing or the context cannot clip, and nothing
4979
- * is pushed.
5568
+ * union by the non-zero rule, the one every context's `clip` takes. Each
5569
+ * is placed from the box's edges and then cut to the painted area: placed
5570
+ * from the cut, a border wider than `CLAMP_PAD` put its bands and its
5571
+ * ring's hole a border's width in from wherever the paint was cut, and
5572
+ * the background was painted over the content of a box repainted in part.
5573
+ * False where the border paints nothing in the painted area or the context
5574
+ * cannot clip, and nothing is pushed.
4980
5575
  */
4981
5576
  function pushBorderArea(
4982
5577
  ctx: PaintContext,
@@ -4998,43 +5593,60 @@ function pushBorderArea(
4998
5593
  const w = Math.round(left + box.width) - x;
4999
5594
  const h = Math.round(top + frameHeight(box)) - y;
5000
5595
  if (w <= 0 || h <= 0) return false;
5001
- const area = clampRect(options, x, y, w, h);
5002
- if (!area) return false;
5596
+ if (!clampRect(options, x, y, w, h)) return false;
5003
5597
  const corners = cornersOf(s, w, h);
5004
5598
  ctx.save();
5005
5599
  ctx.beginPath();
5600
+ // how many shapes the path has: none, and the border paints nothing here
5601
+ let shapes = 0;
5006
5602
  if (corners && canCurve(ctx) && solidBorder(box)) {
5007
- roundedRect(ctx, area.x, area.y, area.w, area.h, corners, true);
5008
- const iw = area.w - l - r;
5009
- const ih = area.h - t - b;
5010
- if (iw > 0 && ih > 0) {
5011
- const inner = insetCorners(corners, t, r, b, l);
5012
- roundedRect(ctx, area.x + l, area.y + t, iw, ih, inner, true, true);
5603
+ const ring = cutRing(options, x, y, w, h, corners, t, r, b, l);
5604
+ if (ring) {
5605
+ const add = ({ rect, corners: c }: Rounded, hole: boolean): void =>
5606
+ roundedRect(
5607
+ ctx,
5608
+ rect.x,
5609
+ rect.y,
5610
+ rect.width,
5611
+ rect.height,
5612
+ c,
5613
+ true,
5614
+ hole,
5615
+ );
5616
+ add(ring.outer, false);
5617
+ // the padding edge run the other way, which cuts it out
5618
+ if (ring.inner) add(ring.inner, true);
5619
+ shapes = 1;
5013
5620
  }
5014
- } else if (sculpted(s)) {
5015
- // a band a side, which the trapezoids cover between them
5016
- ctx.rect(area.x, area.y, area.w, t);
5017
- ctx.rect(area.x, area.y + area.h - b, area.w, b);
5018
- ctx.rect(area.x, area.y + t, l, area.h - t - b);
5019
- ctx.rect(area.x + area.w - r, area.y + t, r, area.h - t - b);
5020
5621
  } else {
5021
- const path = { fillRect: ctx.rect.bind(ctx) };
5022
- const edge = (
5023
- ex: number,
5024
- ey: number,
5025
- ew: number,
5026
- eh: number,
5027
- style: ComputedStyle['borderTopStyle'],
5028
- horizontal: boolean,
5029
- ): void => {
5030
- const rect = clampRect(options, ex, ey, ew, eh);
5031
- if (rect) fillEdge(path, rect, horizontal ? ex : ey, style, horizontal);
5622
+ // A side at a time, from the box's edges and cut to the painted area
5623
+ // after: a 3D side as a band, which the trapezoids cover between them,
5624
+ // and any other as `fillSide` fills it.
5625
+ const path = {
5626
+ fillRect(ex: number, ey: number, ew: number, eh: number) {
5627
+ ctx.rect!(ex, ey, ew, eh);
5628
+ shapes += 1;
5629
+ },
5032
5630
  };
5033
- const between = h - t - b;
5034
- if (t > 0) edge(x, y, w, t, s.borderTopStyle, true);
5035
- if (b > 0) edge(x, y + h - b, w, b, s.borderBottomStyle, true);
5036
- if (l > 0) edge(x, y + t, l, between, s.borderLeftStyle, false);
5037
- if (r > 0) edge(x + w - r, y + t, r, between, s.borderRightStyle, false);
5631
+ const widths = [t, r, b, l];
5632
+ const joins = doubleJoins(box);
5633
+ const styles: ComputedStyle['borderTopStyle'][] = sculpted(s)
5634
+ ? ['solid', 'solid', 'solid', 'solid']
5635
+ : [
5636
+ s.borderTopStyle,
5637
+ s.borderRightStyle,
5638
+ s.borderBottomStyle,
5639
+ s.borderLeftStyle,
5640
+ ];
5641
+ for (let at = 0; at < 4; at += 1) {
5642
+ if (widths[at] > 0) {
5643
+ fillSide(path, options, at, x, y, w, h, widths, styles[at], joins);
5644
+ }
5645
+ }
5646
+ }
5647
+ if (!shapes) {
5648
+ ctx.restore();
5649
+ return false;
5038
5650
  }
5039
5651
  ctx.clip();
5040
5652
  return true;
@@ -5071,10 +5683,12 @@ function sculpted(s: ComputedStyle): boolean {
5071
5683
  * colour's shadow and `outset` its bottom and right; `groove` is `inset`
5072
5684
  * outside `outset`, a band each, and `ridge` the other way round
5073
5685
  * (`borderShades`). A side of any other style takes its trapezoid in its
5074
- * colour. The trapezoids are clamped to the painted area, which leaves a
5075
- * cut end square and out of sight and keeps a long box's far corners out
5076
- * of X's coordinates. False where the context cannot draw a path, and the
5077
- * sides are drawn straight.
5686
+ * colour. The trapezoids are cut to the painted area (`fillPolygon`), which
5687
+ * keeps a long box's far corners out of X's coordinates and a join on its
5688
+ * diagonal: its corners clamped in, a border wider than `CLAMP_PAD` turned
5689
+ * the join to cross the damage at 45° wherever it was, and a scrolled
5690
+ * strip smeared the corners of a thick one. False where the context cannot
5691
+ * draw a path, and the sides are drawn straight.
5078
5692
  */
5079
5693
  function paintSculpted(
5080
5694
  ctx: PaintContext,
@@ -5093,10 +5707,6 @@ function paintSculpted(
5093
5707
  const r = box.borderRight;
5094
5708
  const b = box.borderBottom;
5095
5709
  const l = box.borderLeft;
5096
- const cx = (v: number) =>
5097
- Math.max(area.x - 1, Math.min(v, area.x + area.w + 1));
5098
- const cy = (v: number) =>
5099
- Math.max(area.y - 1, Math.min(v, area.y + area.h + 1));
5100
5710
  /** The band between the edge `outer` of the way in and `inner`, for one
5101
5711
  * side: 0 is the border box, 1 the padding box. */
5102
5712
  const band = (side: number, outer: number, inner: number, color: string) => {
@@ -5117,12 +5727,7 @@ function paintSculpted(
5117
5727
  ? [o.x1, o.y1, o.x0, o.y1, i.x0, i.y1, i.x1, i.y1]
5118
5728
  : [o.x0, o.y1, o.x0, o.y0, i.x0, i.y0, i.x0, i.y1];
5119
5729
  ctx.fillStyle = color;
5120
- ctx.beginPath!();
5121
- ctx.moveTo!(cx(points[0]), cy(points[1]));
5122
- for (let k = 2; k < 8; k += 2)
5123
- ctx.lineTo!(cx(points[k]), cy(points[k + 1]));
5124
- ctx.closePath?.();
5125
- ctx.fill!();
5730
+ fillPolygon(ctx, points, area);
5126
5731
  };
5127
5732
  const sides: [number, string, string][] = [
5128
5733
  [t, s.borderTopStyle, s.borderTopColor],
@@ -5242,52 +5847,174 @@ function paintCollapsedBorders(
5242
5847
  );
5243
5848
  for (const s of segments) {
5244
5849
  if (isTransparent(s.border.color)) continue;
5245
- const rect = clampRect(options, s.x, s.y, s.w, s.h);
5246
- if (!rect) continue;
5850
+ if (!clampRect(options, s.x, s.y, s.w, s.h)) continue;
5247
5851
  ctx.fillStyle = s.border.color;
5248
- fillEdge(ctx, rect, s.horizontal ? s.x : s.y, s.border.style, s.horizontal);
5852
+ fillEdge(ctx, options, s.x, s.y, s.w, s.h, s.border.style, s.horizontal);
5853
+ }
5854
+ }
5855
+
5856
+ /**
5857
+ * One side of a border box at `x, y`, `w` by `h` — `at` counts the top
5858
+ * first and clockwise — as rectangles cut to what the paint reaches: the
5859
+ * top and the bottom the width of the box and the left and the right
5860
+ * between them, so a corner is a square of one side or the other.
5861
+ *
5862
+ * A double side is two lines, a band each along its outer and its inner
5863
+ * edge. Where it meets another double side of its colour (`joins`, the
5864
+ * top left corner first) the lines of the two meet as two frames do, one
5865
+ * inside the other (CSS Backgrounds 3, 4.2 and 4.3): the outer line of the
5866
+ * left or the right runs on to the outer line of the top or the bottom,
5867
+ * and the inner line of the top or the bottom stops at the inner line of
5868
+ * the side. With square corners the outer frame was open at each of them
5869
+ * and the inner line of the top ran on past the side's to the border edge.
5870
+ * Each line is cut to the paint on its own, so a border wider than the
5871
+ * reach around the paint keeps its lines at its edges, and not at the
5872
+ * edges of the part of it cut out.
5873
+ */
5874
+ function fillSide(
5875
+ ctx: Pick<FillContext, 'fillRect'>,
5876
+ options: PaintOptions,
5877
+ at: number,
5878
+ x: number,
5879
+ y: number,
5880
+ w: number,
5881
+ h: number,
5882
+ widths: readonly number[],
5883
+ style: ComputedStyle['borderTopStyle'],
5884
+ joins: readonly boolean[],
5885
+ ): void {
5886
+ const [t, r, b, l] = widths;
5887
+ const width = widths[at];
5888
+ const horizontal = (at & 1) === 0;
5889
+ if (style === 'double' && width >= 3) {
5890
+ const band = doubleBand(width);
5891
+ const line = (lx: number, ly: number, lw: number, lh: number): void => {
5892
+ const rect = clampRect(options, lx, ly, lw, lh);
5893
+ if (rect) ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
5894
+ };
5895
+ if (horizontal) {
5896
+ // the outer line the width of the box, and the inner one from the
5897
+ // inner line of a side it joins, or from the edge of the box
5898
+ const from = joins[at === 0 ? 0 : 3] ? x + l - doubleBand(l) : x;
5899
+ const to = joins[at === 0 ? 1 : 2] ? x + w - r + doubleBand(r) : x + w;
5900
+ line(x, at === 0 ? y : y + h - band, w, band);
5901
+ line(from, at === 0 ? y + t - band : y + h - b, to - from, band);
5902
+ } else {
5903
+ // the outer line from the outer line of a side it joins, or from
5904
+ // that side's inner edge, and the inner one between the two sides
5905
+ const from = joins[at === 3 ? 0 : 1] ? y + doubleBand(t) : y + t;
5906
+ const to = joins[at === 3 ? 3 : 2] ? y + h - doubleBand(b) : y + h - b;
5907
+ line(at === 3 ? x : x + w - band, from, band, to - from);
5908
+ line(at === 3 ? x + l - band : x + w - r, y + t, band, h - t - b);
5909
+ }
5910
+ return;
5249
5911
  }
5912
+ const ex = at === 1 ? x + w - r : x;
5913
+ const ey = at === 0 ? y : at === 2 ? y + h - b : y + t;
5914
+ if (horizontal) fillEdge(ctx, options, ex, ey, w, width, style, true);
5915
+ else fillEdge(ctx, options, ex, ey, width, h - t - b, style, false);
5916
+ }
5917
+
5918
+ /**
5919
+ * The corners of a box where two double sides of one colour meet, the top
5920
+ * left first and clockwise, and whose lines `fillSide` joins. By the
5921
+ * colours as written, a transparent one included, which is how a clip to
5922
+ * the border's area draws the border it does not paint.
5923
+ */
5924
+ function doubleJoins(box: Frame): readonly boolean[] {
5925
+ const s = box.style;
5926
+ const color = (
5927
+ width: number,
5928
+ style: ComputedStyle['borderTopStyle'],
5929
+ value: string,
5930
+ ): string | null =>
5931
+ style === 'double' && width >= 3 ? inkColor(value, s.color) : null;
5932
+ const top = color(box.borderTop, s.borderTopStyle, s.borderTopColor);
5933
+ const bottom = color(
5934
+ box.borderBottom,
5935
+ s.borderBottomStyle,
5936
+ s.borderBottomColor,
5937
+ );
5938
+ if (top === null && bottom === null) return UNJOINED;
5939
+ const right = color(box.borderRight, s.borderRightStyle, s.borderRightColor);
5940
+ const left = color(box.borderLeft, s.borderLeftStyle, s.borderLeftColor);
5941
+ return [
5942
+ top !== null && top === left,
5943
+ top !== null && top === right,
5944
+ bottom !== null && bottom === right,
5945
+ bottom !== null && bottom === left,
5946
+ ];
5250
5947
  }
5251
5948
 
5949
+ const UNJOINED: readonly boolean[] = [false, false, false, false];
5950
+
5951
+ /** How wide each of a double border's two lines is, the gap between them
5952
+ * what is left of the side. */
5953
+ function doubleBand(thickness: number): number {
5954
+ return Math.max(1, Math.floor(thickness / 3));
5955
+ }
5956
+
5957
+ /**
5958
+ * A side at `x, y`, `w` by `h`, running across when `horizontal`, as the
5959
+ * rectangles its style makes, each cut to what the paint reaches. The
5960
+ * dashes, the dots and a double side's lines are measured from the side
5961
+ * and cut after: measured from the cut, a side wider than `CLAMP_PAD` had
5962
+ * dashes as long and as far apart as the cut was wide, starting wherever
5963
+ * it started, and a strip repainted down one dash was striped across it.
5964
+ */
5252
5965
  function fillEdge(
5253
5966
  ctx: Pick<FillContext, 'fillRect'>,
5254
- rect: { x: number; y: number; w: number; h: number },
5255
- phaseOrigin: number,
5967
+ options: PaintOptions,
5968
+ x: number,
5969
+ y: number,
5970
+ w: number,
5971
+ h: number,
5256
5972
  style: ComputedStyle['borderTopStyle'],
5257
5973
  horizontal: boolean,
5258
5974
  ): void {
5259
- const { x, y, w, h } = rect;
5260
5975
  const length = horizontal ? w : h;
5261
5976
  const thickness = horizontal ? h : w;
5262
- if (length <= 0 || thickness <= 0) return;
5977
+ if (!(length > 0 && thickness > 0)) return;
5978
+ const fill = (fx: number, fy: number, fw: number, fh: number): void => {
5979
+ const rect = clampRect(options, fx, fy, fw, fh);
5980
+ if (rect) ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
5981
+ };
5263
5982
  if (style === 'dashed' || style === 'dotted') {
5983
+ // what of the side the paint reaches, so that a side far longer than
5984
+ // that is not walked from its start
5985
+ const cut = clampRect(options, x, y, w, h);
5986
+ if (!cut) return;
5264
5987
  const period = style === 'dotted' ? thickness * 2 : thickness * 3;
5265
5988
  const on = style === 'dotted' ? thickness : thickness * 2;
5266
- const from = horizontal ? x : y;
5267
- // Start on the pattern boundary at or before the clamped start, so the
5268
- // dash the viewport cuts into is the same dash it always was.
5269
- let i = Math.floor((from - phaseOrigin) / period) * period + phaseOrigin;
5270
- for (; i < from + length; i += period) {
5271
- const start = Math.max(i, from);
5272
- const run = Math.min(i + on, from + length) - start;
5273
- if (run <= 0) continue;
5274
- if (horizontal) ctx.fillRect(start, y, run, thickness);
5275
- else ctx.fillRect(x, start, thickness, run);
5989
+ const start = horizontal ? x : y;
5990
+ const end = start + length;
5991
+ const from = horizontal ? cut.x : cut.y;
5992
+ const to = from + (horizontal ? cut.w : cut.h);
5993
+ // from the side's start, so the dash the paint cuts into is the one it
5994
+ // always was
5995
+ for (
5996
+ let i = start + Math.floor((from - start) / period) * period;
5997
+ i < to;
5998
+ i += period
5999
+ ) {
6000
+ const run = Math.min(i + on, end) - i;
6001
+ if (horizontal) fill(i, y, run, thickness);
6002
+ else fill(x, i, thickness, run);
5276
6003
  }
5277
6004
  return;
5278
6005
  }
5279
6006
  if (style === 'double' && thickness >= 3) {
5280
- const band = Math.max(1, Math.floor(thickness / 3));
6007
+ const band = doubleBand(thickness);
5281
6008
  if (horizontal) {
5282
- ctx.fillRect(x, y, length, band);
5283
- ctx.fillRect(x, y + thickness - band, length, band);
6009
+ fill(x, y, length, band);
6010
+ fill(x, y + thickness - band, length, band);
5284
6011
  } else {
5285
- ctx.fillRect(x, y, band, length);
5286
- ctx.fillRect(x + thickness - band, y, band, length);
6012
+ fill(x, y, band, length);
6013
+ fill(x + thickness - band, y, band, length);
5287
6014
  }
5288
6015
  return;
5289
6016
  }
5290
- ctx.fillRect(x, y, w, h);
6017
+ fill(x, y, w, h);
5291
6018
  }
5292
6019
 
5293
6020
  /** A list item's bullet or number, in the margin. */
@@ -5295,6 +6022,8 @@ function paintMarker(
5295
6022
  ctx: PaintContext,
5296
6023
  marker: Marker,
5297
6024
  options: PaintOptions,
6025
+ /** The item's colour scheme, which an SVG image in it is drawn in. */
6026
+ scheme: 'light' | 'dark',
5298
6027
  ): void {
5299
6028
  const x = marker.x + options.originX;
5300
6029
  const y = marker.y + options.originY;
@@ -5304,7 +6033,15 @@ function paintMarker(
5304
6033
  const loaded = options.backgroundImageFor?.(url);
5305
6034
  if (!loaded || !(width > 0 && height > 0)) return;
5306
6035
  if (loaded.image instanceof SvgDrawing) {
5307
- loaded.image.draw(ctx, x, y, width, height, options.scale ?? 1);
6036
+ loaded.image.drawImage(
6037
+ ctx,
6038
+ x,
6039
+ y,
6040
+ width,
6041
+ height,
6042
+ options.scale ?? 1,
6043
+ scheme,
6044
+ );
5308
6045
  } else ctx.drawImage?.(loaded.image, x, y, width, height);
5309
6046
  return;
5310
6047
  }
@@ -5336,7 +6073,15 @@ function paintImage(ctx: PaintContext, box: Box, options: PaintOptions): void {
5336
6073
  const clipped =
5337
6074
  (!!corners || past) && pushClip(ctx, { x, y, w, h }, corners);
5338
6075
  if (image instanceof SvgDrawing) {
5339
- image.draw(ctx, at.x, at.y, at.w, at.h, options.scale ?? 1);
6076
+ image.drawImage(
6077
+ ctx,
6078
+ at.x,
6079
+ at.y,
6080
+ at.w,
6081
+ at.h,
6082
+ options.scale ?? 1,
6083
+ box.style.colorScheme,
6084
+ );
5340
6085
  } else ctx.drawImage!(image, at.x, at.y, at.w, at.h);
5341
6086
  if (clipped) ctx.restore();
5342
6087
  return;
@@ -5427,7 +6172,8 @@ function fitted(
5427
6172
  /** An inline `<svg>`, drawn in its content box. Its `currentColor` is the
5428
6173
  * box's `color`, as an icon's is the text's around it, and what it is
5429
6174
  * painted with the box's `fill` and `stroke`, where the document's style
5430
- * sheets set them: an icon set's `.icon { fill: currentColor }`. */
6175
+ * sheets set them: an icon set's `.icon { fill: currentColor }`. What
6176
+ * they give a shape inside it is asked for here (`BoxTree.shapeStyler`). */
5431
6177
  function paintSvg(ctx: PaintContext, box: Box, options: PaintOptions): void {
5432
6178
  if (!box.el) return;
5433
6179
  const x = Math.round(box.contentX + options.originX);
@@ -5449,6 +6195,7 @@ function paintSvg(ctx: PaintContext, box: Box, options: PaintOptions): void {
5449
6195
  style.color,
5450
6196
  paint(style.fill),
5451
6197
  paint(style.stroke),
6198
+ options.shapeStyler?.(box) ?? null,
5452
6199
  );
5453
6200
  }
5454
6201
 
@@ -5732,7 +6479,7 @@ function paintClippedText(
5732
6479
  ) +
5733
6480
  2;
5734
6481
  for (const run of natural.runs) {
5735
- const clip = clipBoxOf(text.spans.boxAt?.(run.start) ?? null, block);
6482
+ const clip = clipBoxOf(text.spans.boxAt?.(run.start) ?? null);
5736
6483
  if (!clip) continue;
5737
6484
  let byLayout = parts.get(clip);
5738
6485
  if (!byLayout) parts.set(clip, (byLayout = new Map()));
@@ -5754,7 +6501,7 @@ function paintClippedText(
5754
6501
  const list = [...byLayout.values()];
5755
6502
  const style = clip.style;
5756
6503
  let area: Rect;
5757
- if (clip === block) area = paddingBox(block, options);
6504
+ if (clip.kind !== 'inline') area = paddingBox(clip, options);
5758
6505
  else {
5759
6506
  // an inline box: its runs, and its padding round them
5760
6507
  let x0 = Infinity;