@react-x11/components 0.14.1 → 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 (96) 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 +171 -26
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +616 -216
  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 +18 -0
  21. package/dist/html/css/shapes.d.ts.map +1 -1
  22. package/dist/html/css/shapes.js +21 -1
  23. package/dist/html/css/shapes.js.map +1 -1
  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 +74 -12
  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 +29 -9
  38. package/dist/html/layout/block.d.ts.map +1 -1
  39. package/dist/html/layout/block.js +141 -52
  40. package/dist/html/layout/block.js.map +1 -1
  41. package/dist/html/layout/boxes.d.ts +16 -2
  42. package/dist/html/layout/boxes.d.ts.map +1 -1
  43. package/dist/html/layout/boxes.js +144 -23
  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 +11 -9
  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 +1631 -221
  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 +886 -324
  58. package/dist/html/layout/table.js.map +1 -1
  59. package/dist/html/node.d.ts +35 -4
  60. package/dist/html/node.d.ts.map +1 -1
  61. package/dist/html/node.js +118 -26
  62. package/dist/html/node.js.map +1 -1
  63. package/dist/html/paint.d.ts.map +1 -1
  64. package/dist/html/paint.js +558 -178
  65. package/dist/html/paint.js.map +1 -1
  66. package/dist/html/resources.js +14 -1
  67. package/dist/html/resources.js.map +1 -1
  68. package/dist/html/svg.d.ts +23 -7
  69. package/dist/html/svg.d.ts.map +1 -1
  70. package/dist/html/svg.js +264 -59
  71. package/dist/html/svg.js.map +1 -1
  72. package/dist/html/widgets.d.ts.map +1 -1
  73. package/dist/html/widgets.js +57 -4
  74. package/dist/html/widgets.js.map +1 -1
  75. package/package.json +3 -3
  76. package/src/code-editor/node.ts +24 -5
  77. package/src/html/css/cascade.ts +697 -221
  78. package/src/html/css/color.ts +345 -18
  79. package/src/html/css/content.ts +173 -15
  80. package/src/html/css/parse.ts +60 -8
  81. package/src/html/css/shapes.ts +35 -1
  82. package/src/html/css/style.ts +122 -17
  83. package/src/html/css/ua.ts +15 -0
  84. package/src/html/css/values.ts +23 -9
  85. package/src/html/index.ts +7 -0
  86. package/src/html/layout/block.ts +149 -58
  87. package/src/html/layout/boxes.ts +171 -27
  88. package/src/html/layout/css-grid.ts +12 -9
  89. package/src/html/layout/flex.ts +1843 -212
  90. package/src/html/layout/inline.ts +27 -7
  91. package/src/html/layout/table.ts +961 -332
  92. package/src/html/node.ts +151 -27
  93. package/src/html/paint.ts +692 -234
  94. package/src/html/resources.ts +13 -1
  95. package/src/html/svg.ts +299 -56
  96. package/src/html/widgets.ts +61 -4
package/src/html/paint.ts CHANGED
@@ -101,7 +101,7 @@ import {
101
101
  } from './layout/inline.js';
102
102
  import { tableGrid } from './layout/grid.js';
103
103
  import type { Cell } from './layout/grid.js';
104
- import { SvgDrawing, inlineDrawing } from './svg.js';
104
+ import { SvgDrawing, concreteSize, inlineDrawing } from './svg.js';
105
105
  import type { IntrinsicSize } from './svg.js';
106
106
  import type { CollapsedBorder } from './layout/collapse.js';
107
107
 
@@ -976,20 +976,15 @@ function originBox(
976
976
  * it falls nearest, as browsers snap a box — boxes that meet share the
977
977
  * column their edge is in, and a rule 1.33px wide is one pixel, not two —
978
978
  * with the border box's corners less the widths between (5.3), where
979
- * `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.
980
981
  */
981
982
  function clipArea(
982
983
  box: Frame,
983
984
  options: PaintOptions,
984
985
  style: ComputedStyle,
985
986
  rounded: boolean,
986
- ): {
987
- x: number;
988
- y: number;
989
- w: number;
990
- h: number;
991
- corners: Corners | null;
992
- } | null {
987
+ ): Area | null {
993
988
  const left = box.x + options.originX;
994
989
  const top = frameY(box) + options.originY;
995
990
  const right = left + box.width;
@@ -1018,7 +1013,7 @@ function clipArea(
1018
1013
  -l,
1019
1014
  );
1020
1015
  }
1021
- return { x, y, w, h, corners };
1016
+ return clampArea(options, x, y, w, h, corners);
1022
1017
  }
1023
1018
 
1024
1019
  /** What the background and border painters read of a box — which an
@@ -1744,9 +1739,7 @@ function frameImages(
1744
1739
  options: PaintOptions,
1745
1740
  ): (layer: ComputedStyle) => void {
1746
1741
  return (layer) => {
1747
- const painted = clipArea(frame, options, layer, true);
1748
- const area =
1749
- painted && clampRect(options, painted.x, painted.y, painted.w, painted.h);
1742
+ const area = clipArea(frame, options, layer, true);
1750
1743
  if (area) {
1751
1744
  paintBackgroundImage(
1752
1745
  ctx,
@@ -1754,7 +1747,7 @@ function frameImages(
1754
1747
  area,
1755
1748
  originBox(frame, options, layer),
1756
1749
  options,
1757
- painted.corners,
1750
+ area.corners,
1758
1751
  );
1759
1752
  }
1760
1753
  };
@@ -1768,6 +1761,9 @@ function shadowReach(blur: number): number {
1768
1761
 
1769
1762
  const SQUARE: Corners = { x: [0, 0, 0, 0], y: [0, 0, 0, 0] };
1770
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
+
1771
1767
  /** Corners shrunk by `by` — an inner shadow's spread — or grown where it
1772
1768
  * is negative; a square corner stays square. */
1773
1769
  function grownCorners(c: Corners, by: number): Corners {
@@ -1845,15 +1841,6 @@ function paintShadows(
1845
1841
  ) {
1846
1842
  continue;
1847
1843
  }
1848
- // Cut, as a background is (`clampRect`), to what the paint reaches
1849
- // and as far again as the blur does, so the cut edges cast nothing
1850
- // on it. A shadow down a box thousands of pixels tall went to the
1851
- // server whole, and its outline, sent in 16.16 fixed point, threw.
1852
- const within = reach + CLAMP_PAD;
1853
- const cut = clampAround(options, shape, within);
1854
- if (!cut) continue;
1855
- Object.assign(shape, cut);
1856
- const own = clampAround(options, rect, within) ?? rect;
1857
1844
  const around =
1858
1845
  spreadCorners(
1859
1846
  corners,
@@ -1864,11 +1851,31 @@ function paintShadows(
1864
1851
  s.spread,
1865
1852
  s.spread,
1866
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;
1867
1868
  const color = inkColor(s.color, style.color);
1868
1869
  if (!(s.blur > 0) && !s.x && !s.y && s.spread >= 0 && !covered) {
1869
1870
  // a ring about the box: the band between it and the spread
1870
1871
  ctx.fillStyle = color;
1871
- 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
+ );
1872
1879
  continue;
1873
1880
  }
1874
1881
  if (!(s.blur > 0) && covered) {
@@ -1878,14 +1885,7 @@ function paintShadows(
1878
1885
  s,
1879
1886
  color,
1880
1887
  (dx) => {
1881
- roundedRect(
1882
- ctx,
1883
- shape.x - dx,
1884
- shape.y,
1885
- shape.width,
1886
- shape.height,
1887
- around,
1888
- );
1888
+ roundedRect(ctx, x - dx, y, width, height, cut.corners);
1889
1889
  },
1890
1890
  0,
1891
1891
  );
@@ -1897,21 +1897,24 @@ function paintShadows(
1897
1897
  ctx.save();
1898
1898
  ctx.beginPath();
1899
1899
  ctx.rect!(
1900
- shape.x - reach - 1,
1901
- shape.y - reach - 1,
1902
- shape.width + 2 * reach + 2,
1903
- shape.height + 2 * reach + 2,
1904
- );
1905
- roundedRect(
1906
- ctx,
1907
- own.x,
1908
- own.y,
1909
- own.width,
1910
- own.height,
1911
- corners,
1912
- true,
1913
- true,
1900
+ x - reach - 1,
1901
+ y - reach - 1,
1902
+ width + 2 * reach + 2,
1903
+ height + 2 * reach + 2,
1914
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
+ }
1915
1918
  ctx.clip!();
1916
1919
  }
1917
1920
  fillShadow(
@@ -1919,16 +1922,9 @@ function paintShadows(
1919
1922
  s,
1920
1923
  color,
1921
1924
  (dx) => {
1922
- shadowShape(
1923
- ctx,
1924
- shape.x - dx,
1925
- shape.y,
1926
- shape.width,
1927
- shape.height,
1928
- around,
1929
- );
1925
+ shadowShape(ctx, x - dx, y, width, height, cut.corners);
1930
1926
  },
1931
- shape.x + shape.width + reach,
1927
+ x + width + reach,
1932
1928
  );
1933
1929
  if (clipped) ctx.restore();
1934
1930
  }
@@ -1954,9 +1950,6 @@ function paintShadows(
1954
1950
  );
1955
1951
  }
1956
1952
  }
1957
- const padCut = clampAround(options, pad, furthest + CLAMP_PAD);
1958
- if (!padCut) return;
1959
- Object.assign(pad, padCut);
1960
1953
  const inner = insetCorners(
1961
1954
  corners,
1962
1955
  box.borderTop,
@@ -1968,32 +1961,50 @@ function paintShadows(
1968
1961
  const s = shadows[i];
1969
1962
  if (!s.inset || (s.blur > 0 && !canShadow)) continue;
1970
1963
  const color = inkColor(s.color, style.color);
1971
- // the hole the shadow is cast around, moved and shrunk by the spread
1972
- const hole = {
1973
- x: pad.x + s.x + s.spread,
1974
- y: pad.y + s.y + s.spread,
1975
- width: Math.max(0, pad.width - 2 * s.spread),
1976
- height: Math.max(0, pad.height - 2 * s.spread),
1977
- };
1978
- 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;
1979
1983
  if (!(s.blur > 0) && !s.x && !s.y && s.spread >= 0) {
1980
1984
  // Tailwind's `ring-inset`: a band inside the padding edge
1981
1985
  ctx.fillStyle = color;
1982
- 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
+ );
1983
1993
  continue;
1984
1994
  }
1985
1995
  if (!ctx.clip || !ctx.save || !ctx.rect) continue;
1996
+ const { x, y, width, height } = area.rect;
1986
1997
  ctx.save();
1987
1998
  ctx.beginPath();
1988
- roundedRect(ctx, pad.x, pad.y, pad.width, pad.height, inner);
1999
+ roundedRect(ctx, x, y, width, height, area.corners);
1989
2000
  ctx.clip();
1990
2001
  // a frame around the hole, as wide as the blur and the offset reach
1991
2002
  const reach = shadowReach(s.blur) + Math.abs(s.x) + Math.abs(s.y) + 1;
1992
2003
  const frame = {
1993
- x: pad.x - reach,
1994
- y: pad.y - reach,
1995
- width: pad.width + 2 * reach,
1996
- height: pad.height + 2 * reach,
2004
+ x: x - reach,
2005
+ y: y - reach,
2006
+ width: width + 2 * reach,
2007
+ height: height + 2 * reach,
1997
2008
  };
1998
2009
  fillShadow(
1999
2010
  ctx,
@@ -2001,15 +2012,9 @@ function paintShadows(
2001
2012
  color,
2002
2013
  (dx) => {
2003
2014
  ctx.rect!(frame.x - dx, frame.y, frame.width, frame.height);
2004
- if (hole.width > 0 && hole.height > 0) {
2005
- shadowShape(
2006
- ctx,
2007
- hole.x - dx,
2008
- hole.y,
2009
- hole.width,
2010
- hole.height,
2011
- within,
2012
- );
2015
+ if (hole) {
2016
+ const { rect: r } = hole;
2017
+ shadowShape(ctx, r.x - dx, r.y, r.width, r.height, hole.corners);
2013
2018
  }
2014
2019
  },
2015
2020
  frame.x + frame.width + shadowReach(s.blur),
@@ -3417,6 +3422,92 @@ function clampRect(
3417
3422
  return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
3418
3423
  }
3419
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
+
3420
3511
  function paintBackground(
3421
3512
  ctx: PaintContext,
3422
3513
  box: Frame,
@@ -3427,20 +3518,16 @@ function paintBackground(
3427
3518
  const gradient = style.backgroundGradient;
3428
3519
  const solid = !isTransparent(color);
3429
3520
  if (!solid && !gradient) return;
3430
- const area = clipArea(
3521
+ const rect = clipArea(
3431
3522
  box,
3432
3523
  options,
3433
3524
  style,
3434
3525
  !!(ctx.roundRect && ctx.fill && ctx.beginPath),
3435
3526
  );
3436
- const rect = area && clampRect(options, area.x, area.y, area.w, area.h);
3437
3527
  if (!rect) return;
3438
- const rounded = area.corners;
3528
+ const rounded = rect.corners;
3439
3529
  const fill = (): void => {
3440
3530
  if (rounded) {
3441
- // The clamp can only have cut edges further than CLAMP_PAD outside
3442
- // the damage, and a sane radius is smaller than that — so a corner
3443
- // that survives the cut is whole, and a cut edge is offscreen.
3444
3531
  ctx.beginPath!();
3445
3532
  roundedRect(ctx, rect.x, rect.y, rect.w, rect.h, rounded);
3446
3533
  ctx.fill!();
@@ -4355,7 +4442,7 @@ function roundedTile(
4355
4442
 
4356
4443
  /**
4357
4444
  * A background tile's size (CSS Backgrounds 3, 3.9, over CSS Images' sizing
4358
- * 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`
4359
4446
  * and `contain` scale it to fill the positioning area or to fit inside it,
4360
4447
  * keeping its ratio, and one with no ratio is the size of the area; and a
4361
4448
  * width or a height alone takes the other from the ratio, or else from the
@@ -4368,7 +4455,8 @@ function sizedTile(
4368
4455
  area: Rect,
4369
4456
  scale: number,
4370
4457
  ): [number, number] {
4371
- if (size === 'auto') return tileSize(image, area, scale);
4458
+ if (size === 'auto')
4459
+ return concreteSize(image, area.width, area.height, scale);
4372
4460
  const { ratio } = image;
4373
4461
  if (size === 'cover' || size === 'contain') {
4374
4462
  if (!(ratio > 0 && area.height > 0)) return [area.width, area.height];
@@ -4389,34 +4477,7 @@ function sizedTile(
4389
4477
  if (ratio > 0) return [h * ratio, h];
4390
4478
  return [image.width === null ? area.width : image.width * scale, h];
4391
4479
  }
4392
- return tileSize(image, area, scale);
4393
- }
4394
-
4395
- /**
4396
- * A background image's size, in device pixels, where nothing sets it — CSS
4397
- * 2.1 has no `background-size` — which is CSS Images' default sizing: an
4398
- * image's own size where it has one; the dimension it lacks from its ratio,
4399
- * or else from the positioning area; and one with a ratio alone as large as
4400
- * fits in the area. An SVG may be any of these, and a raster image is the
4401
- * first.
4402
- */
4403
- function tileSize(
4404
- size: IntrinsicSize,
4405
- area: Rect,
4406
- scale: number,
4407
- ): [number, number] {
4408
- const { ratio } = size;
4409
- const width = size.width === null ? null : size.width * scale;
4410
- const height = size.height === null ? null : size.height * scale;
4411
- if (width !== null && height !== null) return [width, height];
4412
- if (width !== null) return [width, ratio > 0 ? width / ratio : area.height];
4413
- if (height !== null) return [ratio > 0 ? height * ratio : area.width, height];
4414
- if (ratio > 0) {
4415
- return area.width / area.height > ratio
4416
- ? [area.height * ratio, area.height]
4417
- : [area.width, area.width / ratio];
4418
- }
4419
- return [area.width, area.height];
4480
+ return concreteSize(image, area.width, area.height, scale);
4420
4481
  }
4421
4482
 
4422
4483
  /**
@@ -4489,7 +4550,7 @@ function paintOutline(
4489
4550
  *
4490
4551
  * Where two sides that meet differ in colour — one of them transparent
4491
4552
  * included — the corner is cut on the diagonal from its outer point to its
4492
- * 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
4493
4554
  * is a trapezoid, and a triangle where the box inside the borders has no
4494
4555
  * width: the CSS triangle a dropdown's caret and a tooltip's arrow are
4495
4556
  * drawn with, which came out as the rectangle around it. Only a solid side
@@ -4606,7 +4667,7 @@ function paintBorders(
4606
4667
 
4607
4668
  /**
4608
4669
  * Whether the corner two sides meet at is cut on its diagonal, each side
4609
- * 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
4610
4671
  * are painted in different colours or only one is painted at all, and each
4611
4672
  * is solid or unpainted. Sides of one colour make the same corner either
4612
4673
  * way, and so does a corner of a single pixel, which is left to the side
@@ -4628,12 +4689,14 @@ function mitred(
4628
4689
  }
4629
4690
 
4630
4691
  /**
4631
- * Fill a convex polygon, its corners as `x, y` pairs, cut to `area`: the
4632
- * part of it near what is painted, which is what keeps a side thousands of
4633
- * pixels long inside X's coordinates. Cut edge by edge rather than by
4634
- * moving its corners in, which would turn a diagonal that crosses the
4635
- * painted area: a slanted divider, `border-left: 100vw solid transparent`,
4636
- * 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.
4637
4700
  */
4638
4701
  function fillPolygon(
4639
4702
  ctx: PaintContext,
@@ -4710,9 +4773,7 @@ function paintBorderImage(
4710
4773
  // the image's size, CSS Images' default sizing in the area: a drawing
4711
4774
  // with no size of its own is as large as it fits there, and a gradient,
4712
4775
  // which has none, is the area's
4713
- const [cw, ch] = loaded
4714
- ? tileSize(loaded, { x: 0, y: 0, width: aw, height: ah }, scale)
4715
- : [aw, ah];
4776
+ const [cw, ch] = loaded ? concreteSize(loaded, aw, ah, scale) : [aw, ah];
4716
4777
  const iw = cw / scale;
4717
4778
  const ih = ch / scale;
4718
4779
  if (!(iw > 0 && ih > 0)) return false;
@@ -4920,13 +4981,14 @@ function edgeRun(
4920
4981
 
4921
4982
  /**
4922
4983
  * A rounded box's border as the ring between its border edge and its
4923
- * padding edge, each rounded, where every side that has a border has one
4924
- * of the same colour and a solid rule: a card's or a button's. Drawn
4925
- * straight, its corners were square over the background's rounded ones,
4926
- * and an accent border down one side did not follow the corner. The inner
4927
- * radius is the outer less the wider border at that corner (CSS
4928
- * Backgrounds 3, 5.2). False where the border is not such a one, and it is
4929
- * 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.
4930
4992
  */
4931
4993
  function roundedRing(
4932
4994
  ctx: PaintContext,
@@ -4938,7 +5000,7 @@ function roundedRing(
4938
5000
  options: PaintOptions,
4939
5001
  ): boolean {
4940
5002
  const s = box.style;
4941
- if (!ctx.roundRect || !ctx.fill || !ctx.beginPath) return false;
5003
+ if (!ctx.fill || !ctx.beginPath) return false;
4942
5004
  const corners = cornersOf(s, w, h);
4943
5005
  if (!corners) return false;
4944
5006
  const sides: [number, string, string][] = [
@@ -4948,13 +5010,34 @@ function roundedRing(
4948
5010
  [box.borderLeft, s.borderLeftColor, s.borderLeftStyle],
4949
5011
  ];
4950
5012
  let color: string | null = null;
5013
+ let mixed = false;
4951
5014
  for (const [width, ink, style] of sides) {
4952
5015
  if (!width) continue;
4953
- if (style !== 'solid' || (color !== null && ink !== color)) return false;
5016
+ if (style !== 'solid') return false;
5017
+ if (color !== null && ink !== color) mixed = true;
4954
5018
  color = ink;
4955
5019
  }
5020
+ if (mixed) return ringSides(ctx, box, x, y, w, h, corners, options);
5021
+ if (!ctx.roundRect) return false;
4956
5022
  if (color === null) return true;
4957
5023
  if (isTransparent(color)) return true;
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 {
4958
5041
  const ring = cutRing(
4959
5042
  options,
4960
5043
  x,
@@ -4967,16 +5050,350 @@ function roundedRing(
4967
5050
  box.borderBottom,
4968
5051
  box.borderLeft,
4969
5052
  );
4970
- if (!ring) return true;
5053
+ if (!ring) return;
4971
5054
  const { outer, inner } = ring;
4972
- ctx.fillStyle = inkColor(color, s.color);
5055
+ ctx.fillStyle = color;
4973
5056
  fillRing(
4974
5057
  ctx,
4975
5058
  outer.rect,
4976
5059
  outer.corners,
4977
- inner ? inner.rect : { x: 0, y: 0, width: 0, height: 0 },
5060
+ inner ? inner.rect : NOWHERE,
4978
5061
  inner ? inner.corners : outer.corners,
4979
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,
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
+ }
4980
5397
  return true;
4981
5398
  }
4982
5399
 
@@ -4994,10 +5411,10 @@ interface Rounded {
4994
5411
  * the box first and insetting the cut by the borders put the hole a
4995
5412
  * border's width in from the cut rather than from the box's edge, and left
4996
5413
  * none where a border was wider than `CLAMP_PAD`: the ring filled whatever
4997
- * of the box a paint reached. A cut is straight, so a side of the window
4998
- * is moved out past any corner's curve it would cross, and a corner on a
4999
- * side the window cut is square, its curve being outside. Null where the
5000
- * ring has nothing in the window.
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.
5001
5418
  */
5002
5419
  function cutRing(
5003
5420
  options: PaintOptions,
@@ -5011,77 +5428,101 @@ function cutRing(
5011
5428
  bottom: number,
5012
5429
  left: number,
5013
5430
  ): { outer: Rounded; inner: Rounded | null } | null {
5014
- const { x: cx, y: cy } = corners;
5015
- // where each corner's curve runs, across and down — the padding edge's
5016
- // run inside the border edge's
5017
- const across: [number, number][] = [
5018
- [x, x + cx[0]],
5019
- [x + w - cx[1], x + w],
5020
- [x + w - cx[2], x + w],
5021
- [x, x + cx[3]],
5022
- ];
5023
- const down: [number, number][] = [
5024
- [y, y + cy[0]],
5025
- [y, y + cy[1]],
5026
- [y + h - cy[2], y + h],
5027
- [y + h - cy[3], y + h],
5028
- ];
5029
- const d = options.damage;
5030
- const side = (at: number, spans: [number, number][], towards: number) =>
5031
- Math.max(
5032
- -COORD_LIMIT,
5033
- Math.min(COORD_LIMIT, outOfSpans(at, spans, towards)),
5034
- );
5035
- const x0 = side(d ? d.x - CLAMP_PAD : -COORD_LIMIT, across, -1);
5036
- const y0 = side(d ? d.y - CLAMP_PAD : -COORD_LIMIT, down, -1);
5037
- const x1 = side(d ? d.x + d.width + CLAMP_PAD : COORD_LIMIT, across, 1);
5038
- const y1 = side(d ? d.y + d.height + CLAMP_PAD : COORD_LIMIT, down, 1);
5039
- const cut = (
5040
- rx: number,
5041
- ry: number,
5042
- rw: number,
5043
- rh: number,
5044
- c: Corners,
5045
- ): Rounded | null => {
5046
- const l = Math.max(rx, x0);
5047
- const t = Math.max(ry, y0);
5048
- const r = Math.min(rx + rw, x1);
5049
- const b = Math.min(ry + rh, y1);
5050
- if (r <= l || b <= t) return null;
5051
- // the corners of the sides the window left where they were
5052
- const kept = [
5053
- t === ry && l === rx,
5054
- t === ry && r === rx + rw,
5055
- b === ry + rh && r === rx + rw,
5056
- b === ry + rh && l === rx,
5057
- ];
5058
- return {
5059
- rect: { x: l, y: t, width: r - l, height: b - t },
5060
- corners: {
5061
- x: c.x.map((v, i) => (kept[i] ? v : 0)) as Corners['x'],
5062
- y: c.y.map((v, i) => (kept[i] ? v : 0)) as Corners['y'],
5063
- },
5064
- };
5065
- };
5066
- const outer = cut(x, y, w, h, corners);
5431
+ const edge = { rect: { x, y, width: w, height: h }, corners };
5432
+ const cut = roundedWindow(options, [edge]);
5433
+ const outer = cutRounded(cut, edge);
5067
5434
  if (!outer) return null;
5068
5435
  const iw = w - left - right;
5069
5436
  const ih = h - top - bottom;
5070
5437
  const inner =
5071
5438
  iw > 0 && ih > 0
5072
- ? cut(
5073
- x + left,
5074
- y + top,
5075
- iw,
5076
- ih,
5077
- insetCorners(corners, top, right, bottom, left),
5078
- )
5439
+ ? cutRounded(cut, {
5440
+ rect: { x: x + left, y: y + top, width: iw, height: ih },
5441
+ corners: insetCorners(corners, top, right, bottom, left),
5442
+ })
5079
5443
  : null;
5080
5444
  // a window inside the padding edge, where the ring has nothing
5081
5445
  if (inner && sameRounded(outer, inner)) return null;
5082
5446
  return { outer, inner };
5083
5447
  }
5084
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
+
5085
5526
  /** A position moved out of any of `spans` it is inside, towards their
5086
5527
  * starts where `towards` is negative and their ends where it is not. */
5087
5528
  function outOfSpans(
@@ -5406,10 +5847,9 @@ function paintCollapsedBorders(
5406
5847
  );
5407
5848
  for (const s of segments) {
5408
5849
  if (isTransparent(s.border.color)) continue;
5409
- const rect = clampRect(options, s.x, s.y, s.w, s.h);
5410
- if (!rect) continue;
5850
+ if (!clampRect(options, s.x, s.y, s.w, s.h)) continue;
5411
5851
  ctx.fillStyle = s.border.color;
5412
- 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);
5413
5853
  }
5414
5854
  }
5415
5855
 
@@ -5471,12 +5911,8 @@ function fillSide(
5471
5911
  }
5472
5912
  const ex = at === 1 ? x + w - r : x;
5473
5913
  const ey = at === 0 ? y : at === 2 ? y + h - b : y + t;
5474
- const rect = horizontal
5475
- ? clampRect(options, ex, ey, w, width)
5476
- : clampRect(options, ex, ey, width, h - t - b);
5477
- // The un-clamped start is the dash phase's origin, so the pattern does
5478
- // not crawl as the viewport moves along a long edge.
5479
- if (rect) fillEdge(ctx, rect, horizontal ? ex : ey, style, horizontal);
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);
5480
5916
  }
5481
5917
 
5482
5918
  /**
@@ -5518,45 +5954,67 @@ function doubleBand(thickness: number): number {
5518
5954
  return Math.max(1, Math.floor(thickness / 3));
5519
5955
  }
5520
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
+ */
5521
5965
  function fillEdge(
5522
5966
  ctx: Pick<FillContext, 'fillRect'>,
5523
- rect: { x: number; y: number; w: number; h: number },
5524
- phaseOrigin: number,
5967
+ options: PaintOptions,
5968
+ x: number,
5969
+ y: number,
5970
+ w: number,
5971
+ h: number,
5525
5972
  style: ComputedStyle['borderTopStyle'],
5526
5973
  horizontal: boolean,
5527
5974
  ): void {
5528
- const { x, y, w, h } = rect;
5529
5975
  const length = horizontal ? w : h;
5530
5976
  const thickness = horizontal ? h : w;
5531
- 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
+ };
5532
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;
5533
5987
  const period = style === 'dotted' ? thickness * 2 : thickness * 3;
5534
5988
  const on = style === 'dotted' ? thickness : thickness * 2;
5535
- const from = horizontal ? x : y;
5536
- // Start on the pattern boundary at or before the clamped start, so the
5537
- // dash the viewport cuts into is the same dash it always was.
5538
- let i = Math.floor((from - phaseOrigin) / period) * period + phaseOrigin;
5539
- for (; i < from + length; i += period) {
5540
- const start = Math.max(i, from);
5541
- const run = Math.min(i + on, from + length) - start;
5542
- if (run <= 0) continue;
5543
- if (horizontal) ctx.fillRect(start, y, run, thickness);
5544
- 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);
5545
6003
  }
5546
6004
  return;
5547
6005
  }
5548
6006
  if (style === 'double' && thickness >= 3) {
5549
6007
  const band = doubleBand(thickness);
5550
6008
  if (horizontal) {
5551
- ctx.fillRect(x, y, length, band);
5552
- ctx.fillRect(x, y + thickness - band, length, band);
6009
+ fill(x, y, length, band);
6010
+ fill(x, y + thickness - band, length, band);
5553
6011
  } else {
5554
- ctx.fillRect(x, y, band, length);
5555
- ctx.fillRect(x + thickness - band, y, band, length);
6012
+ fill(x, y, band, length);
6013
+ fill(x + thickness - band, y, band, length);
5556
6014
  }
5557
6015
  return;
5558
6016
  }
5559
- ctx.fillRect(x, y, w, h);
6017
+ fill(x, y, w, h);
5560
6018
  }
5561
6019
 
5562
6020
  /** A list item's bullet or number, in the margin. */
@@ -6021,7 +6479,7 @@ function paintClippedText(
6021
6479
  ) +
6022
6480
  2;
6023
6481
  for (const run of natural.runs) {
6024
- const clip = clipBoxOf(text.spans.boxAt?.(run.start) ?? null, block);
6482
+ const clip = clipBoxOf(text.spans.boxAt?.(run.start) ?? null);
6025
6483
  if (!clip) continue;
6026
6484
  let byLayout = parts.get(clip);
6027
6485
  if (!byLayout) parts.set(clip, (byLayout = new Map()));
@@ -6043,7 +6501,7 @@ function paintClippedText(
6043
6501
  const list = [...byLayout.values()];
6044
6502
  const style = clip.style;
6045
6503
  let area: Rect;
6046
- if (clip === block) area = paddingBox(block, options);
6504
+ if (clip.kind !== 'inline') area = paddingBox(clip, options);
6047
6505
  else {
6048
6506
  // an inline box: its runs, and its padding round them
6049
6507
  let x0 = Infinity;