@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
@@ -29,7 +29,7 @@ import { contained, placedMatrix } from './layout/block.js';
29
29
  import { BOX_RAISES, LINE_BOX_RAISES, PADDED_FACES, Box, CLAMPED, CLIPPED_CELLS, COLLAPSED_CELLS, COLUMN_LINES, COLUMN_PIECES, COLUMN_ROWS, columned, FADED_BLOCKS, PAINT_ORDER, INLINE_OFFSETS, MOVED_OFF_LINES, SHADOWED_TEXT, SHIFTED_LINES, TEXT_RAISES, TEXT_SHIFTS, } from './layout/boxes.js';
30
30
  import { CLIPPED_TEXT, clipBoxOf, depthOf, inklessLayout, layoutOffsetOf, layoutOffsets, } from './layout/inline.js';
31
31
  import { tableGrid } from './layout/grid.js';
32
- import { SvgDrawing, inlineDrawing } from './svg.js';
32
+ import { SvgDrawing, concreteSize, inlineDrawing } from './svg.js';
33
33
  /**
34
34
  * The bounds of everything a box and its descendants draw, in document
35
35
  * coordinates, into `boundsX`…, with the text `position: relative` moved off
@@ -696,7 +696,8 @@ function originBox(frame, options, style) {
696
696
  * it falls nearest, as browsers snap a box — boxes that meet share the
697
697
  * column their edge is in, and a rule 1.33px wide is one pixel, not two —
698
698
  * with the border box's corners less the widths between (5.3), where
699
- * `rounded` asks for them. Null where it has no area.
699
+ * `rounded` asks for them — cut to what the paint reaches (`clampArea`).
700
+ * Null where it has no area there.
700
701
  */
701
702
  function clipArea(box, options, style, rounded) {
702
703
  const left = box.x + options.originX;
@@ -716,7 +717,7 @@ function clipArea(box, options, style, rounded) {
716
717
  if (corners && (t || r || b || l)) {
717
718
  corners = spreadCorners(corners, box.width, frameHeight(box), -t, -r, -b, -l);
718
719
  }
719
- return { x, y, w, h, corners };
720
+ return clampArea(options, x, y, w, h, corners);
720
721
  }
721
722
  /** Where a box's background and border go: its border box, which for a
722
723
  * table leaves out the captions around it (CSS 2.1 17.4). */
@@ -1364,10 +1365,9 @@ const ROW_REACH = 2048;
1364
1365
  */
1365
1366
  function frameImages(ctx, frame, options) {
1366
1367
  return (layer) => {
1367
- const painted = clipArea(frame, options, layer, true);
1368
- const area = painted && clampRect(options, painted.x, painted.y, painted.w, painted.h);
1368
+ const area = clipArea(frame, options, layer, true);
1369
1369
  if (area) {
1370
- paintBackgroundImage(ctx, layer, area, originBox(frame, options, layer), options, painted.corners);
1370
+ paintBackgroundImage(ctx, layer, area, originBox(frame, options, layer), options, area.corners);
1371
1371
  }
1372
1372
  };
1373
1373
  }
@@ -1377,6 +1377,8 @@ function shadowReach(blur) {
1377
1377
  return blur > 0 ? Math.ceil(blur * 1.5) : 0;
1378
1378
  }
1379
1379
  const SQUARE = { x: [0, 0, 0, 0], y: [0, 0, 0, 0] };
1380
+ /** A rectangle with no area, where a ring has no inside. */
1381
+ const NOWHERE = { x: 0, y: 0, width: 0, height: 0 };
1380
1382
  /** Corners shrunk by `by` — an inner shadow's spread — or grown where it
1381
1383
  * is negative; a square corner stays square. */
1382
1384
  function grownCorners(c, by) {
@@ -1444,28 +1446,28 @@ function paintShadows(ctx, box, options, inset) {
1444
1446
  if (!clampRect(options, shape.x - reach, shape.y - reach, shape.width + 2 * reach, shape.height + 2 * reach)) {
1445
1447
  continue;
1446
1448
  }
1447
- // Cut, as a background is (`clampRect`), to what the paint reaches
1449
+ const around = spreadCorners(corners, rect.width, rect.height, s.spread, s.spread, s.spread, s.spread) ?? SQUARE;
1450
+ // Cut, as a background is (`clampArea`), to what the paint reaches
1448
1451
  // and as far again as the blur does, so the cut edges cast nothing
1449
- // on it. A shadow down a box thousands of pixels tall went to the
1450
- // server whole, and its outline, sent in 16.16 fixed point, threw.
1451
- const within = reach + CLAMP_PAD;
1452
- const cut = clampAround(options, shape, within);
1452
+ // on it, and the box with it, which a ring is the band outside and
1453
+ // the shadow is clipped out of. A shadow down a box thousands of
1454
+ // pixels tall went to the server whole, and its outline, sent in
1455
+ // 16.16 fixed point, threw.
1456
+ const [cut, own] = clampPair(options, reach + CLAMP_PAD, { rect: shape, corners: around }, { rect, corners });
1453
1457
  if (!cut)
1454
1458
  continue;
1455
- Object.assign(shape, cut);
1456
- const own = clampAround(options, rect, within) ?? rect;
1457
- const around = spreadCorners(corners, rect.width, rect.height, s.spread, s.spread, s.spread, s.spread) ?? SQUARE;
1459
+ const { x, y, width, height } = cut.rect;
1458
1460
  const color = inkColor(s.color, style.color);
1459
1461
  if (!(s.blur > 0) && !s.x && !s.y && s.spread >= 0 && !covered) {
1460
1462
  // a ring about the box: the band between it and the spread
1461
1463
  ctx.fillStyle = color;
1462
- fillRing(ctx, shape, around, own, corners);
1464
+ fillRing(ctx, cut.rect, cut.corners, own ? own.rect : NOWHERE, own ? own.corners : SQUARE);
1463
1465
  continue;
1464
1466
  }
1465
1467
  if (!(s.blur > 0) && covered) {
1466
1468
  // a hard shadow, under a box that hides what falls under it
1467
1469
  fillShadow(ctx, s, color, (dx) => {
1468
- roundedRect(ctx, shape.x - dx, shape.y, shape.width, shape.height, around);
1470
+ roundedRect(ctx, x - dx, y, width, height, cut.corners);
1469
1471
  }, 0);
1470
1472
  continue;
1471
1473
  }
@@ -1474,13 +1476,16 @@ function paintShadows(ctx, box, options, inset) {
1474
1476
  if (clipped) {
1475
1477
  ctx.save();
1476
1478
  ctx.beginPath();
1477
- ctx.rect(shape.x - reach - 1, shape.y - reach - 1, shape.width + 2 * reach + 2, shape.height + 2 * reach + 2);
1478
- roundedRect(ctx, own.x, own.y, own.width, own.height, corners, true, true);
1479
+ ctx.rect(x - reach - 1, y - reach - 1, width + 2 * reach + 2, height + 2 * reach + 2);
1480
+ if (own) {
1481
+ const { rect: r } = own;
1482
+ roundedRect(ctx, r.x, r.y, r.width, r.height, own.corners, true, true);
1483
+ }
1479
1484
  ctx.clip();
1480
1485
  }
1481
1486
  fillShadow(ctx, s, color, (dx) => {
1482
- shadowShape(ctx, shape.x - dx, shape.y, shape.width, shape.height, around);
1483
- }, shape.x + shape.width + reach);
1487
+ shadowShape(ctx, x - dx, y, width, height, cut.corners);
1488
+ }, x + width + reach);
1484
1489
  if (clipped)
1485
1490
  ctx.restore();
1486
1491
  }
@@ -1505,48 +1510,53 @@ function paintShadows(ctx, box, options, inset) {
1505
1510
  furthest = Math.max(furthest, shadowReach(s.blur) + Math.abs(s.x) + Math.abs(s.y) + 1);
1506
1511
  }
1507
1512
  }
1508
- const padCut = clampAround(options, pad, furthest + CLAMP_PAD);
1509
- if (!padCut)
1510
- return;
1511
- Object.assign(pad, padCut);
1512
1513
  const inner = insetCorners(corners, box.borderTop, box.borderRight, box.borderBottom, box.borderLeft);
1513
1514
  for (let i = shadows.length - 1; i >= 0; i -= 1) {
1514
1515
  const s = shadows[i];
1515
1516
  if (!s.inset || (s.blur > 0 && !canShadow))
1516
1517
  continue;
1517
1518
  const color = inkColor(s.color, style.color);
1518
- // the hole the shadow is cast around, moved and shrunk by the spread
1519
- const hole = {
1520
- x: pad.x + s.x + s.spread,
1521
- y: pad.y + s.y + s.spread,
1522
- width: Math.max(0, pad.width - 2 * s.spread),
1523
- height: Math.max(0, pad.height - 2 * s.spread),
1524
- };
1525
- const within = grownCorners(inner, -s.spread);
1519
+ // The hole the shadow is cast around, moved and shrunk by the spread,
1520
+ // cut with the padding box: made from the cut one, it was a spread in
1521
+ // from the cut rather than from the box's edge, and a spread wider
1522
+ // than the margin left none.
1523
+ const [area, hole] = clampPair(options, furthest + CLAMP_PAD, { rect: pad, corners: inner }, {
1524
+ rect: {
1525
+ x: pad.x + s.x + s.spread,
1526
+ y: pad.y + s.y + s.spread,
1527
+ width: Math.max(0, pad.width - 2 * s.spread),
1528
+ height: Math.max(0, pad.height - 2 * s.spread),
1529
+ },
1530
+ corners: grownCorners(inner, -s.spread),
1531
+ });
1532
+ if (!area)
1533
+ continue;
1526
1534
  if (!(s.blur > 0) && !s.x && !s.y && s.spread >= 0) {
1527
1535
  // Tailwind's `ring-inset`: a band inside the padding edge
1528
1536
  ctx.fillStyle = color;
1529
- fillRing(ctx, pad, inner, hole, within);
1537
+ fillRing(ctx, area.rect, area.corners, hole ? hole.rect : NOWHERE, hole ? hole.corners : SQUARE);
1530
1538
  continue;
1531
1539
  }
1532
1540
  if (!ctx.clip || !ctx.save || !ctx.rect)
1533
1541
  continue;
1542
+ const { x, y, width, height } = area.rect;
1534
1543
  ctx.save();
1535
1544
  ctx.beginPath();
1536
- roundedRect(ctx, pad.x, pad.y, pad.width, pad.height, inner);
1545
+ roundedRect(ctx, x, y, width, height, area.corners);
1537
1546
  ctx.clip();
1538
1547
  // a frame around the hole, as wide as the blur and the offset reach
1539
1548
  const reach = shadowReach(s.blur) + Math.abs(s.x) + Math.abs(s.y) + 1;
1540
1549
  const frame = {
1541
- x: pad.x - reach,
1542
- y: pad.y - reach,
1543
- width: pad.width + 2 * reach,
1544
- height: pad.height + 2 * reach,
1550
+ x: x - reach,
1551
+ y: y - reach,
1552
+ width: width + 2 * reach,
1553
+ height: height + 2 * reach,
1545
1554
  };
1546
1555
  fillShadow(ctx, s, color, (dx) => {
1547
1556
  ctx.rect(frame.x - dx, frame.y, frame.width, frame.height);
1548
- if (hole.width > 0 && hole.height > 0) {
1549
- shadowShape(ctx, hole.x - dx, hole.y, hole.width, hole.height, within);
1557
+ if (hole) {
1558
+ const { rect: r } = hole;
1559
+ shadowShape(ctx, r.x - dx, r.y, r.width, r.height, hole.corners);
1550
1560
  }
1551
1561
  }, frame.x + frame.width + shadowReach(s.blur), 'evenodd');
1552
1562
  ctx.restore();
@@ -2778,22 +2788,78 @@ function clampRect(options, x, y, w, h) {
2778
2788
  return null;
2779
2789
  return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
2780
2790
  }
2791
+ /**
2792
+ * `clampRect` for a rectangle that may be rounded, with the corners it is
2793
+ * left with. Where no radius is larger than `CLAMP_PAD` they are its own,
2794
+ * and it is cut as a square one is: a corner the cut leaves on a cut edge
2795
+ * curves no further in from it than the margin, which keeps it out of what
2796
+ * is painted. A larger radius, a circle's or a pill's, would bend into it
2797
+ * — a circle 400 across repainted a strip at a time was another shape in
2798
+ * every strip across its curve — so there the window is moved out past
2799
+ * any curve it would cross, and a corner on a side it cut is square
2800
+ * (`cutRounded`, as a rounded border's ring is cut).
2801
+ */
2802
+ function clampArea(options, x, y, w, h, corners) {
2803
+ if (corners && curvesPast(corners, CLAMP_PAD)) {
2804
+ const shape = { rect: { x, y, width: w, height: h }, corners };
2805
+ const cut = cutRounded(roundedWindow(options, [shape]), shape);
2806
+ if (!cut)
2807
+ return null;
2808
+ const { rect } = cut;
2809
+ return {
2810
+ x: rect.x,
2811
+ y: rect.y,
2812
+ w: rect.width,
2813
+ h: rect.height,
2814
+ corners: cut.corners,
2815
+ };
2816
+ }
2817
+ const rect = clampRect(options, x, y, w, h);
2818
+ return rect && { x: rect.x, y: rect.y, w: rect.w, h: rect.h, corners };
2819
+ }
2820
+ /**
2821
+ * Two rounded rectangles drawn against each other — a shadow's shape and
2822
+ * the box it is clipped out of, a hole and what it is cut in — cut to one
2823
+ * window, `pad` round what the paint reaches, as `clampArea` cuts one:
2824
+ * where neither has a corner that curves further than `CLAMP_PAD`, as
2825
+ * square ones are, keeping their corners, and otherwise to a window moved
2826
+ * out past every curve of either it would cross, each square on the sides
2827
+ * it cut. `CLAMP_PAD` and not `pad`, because what a shadow's margin has
2828
+ * past it is what the blur casts back across the cut, and a corner bent
2829
+ * into the margin is cast that far further in. Null for one with nothing
2830
+ * in the window.
2831
+ */
2832
+ function clampPair(options, pad, a, b) {
2833
+ if (!curvesPast(a.corners, CLAMP_PAD) && !curvesPast(b.corners, CLAMP_PAD)) {
2834
+ return [clampKeeping(options, a, pad), clampKeeping(options, b, pad)];
2835
+ }
2836
+ const cut = roundedWindow(options, [a, b], pad);
2837
+ return [cutRounded(cut, a), cutRounded(cut, b)];
2838
+ }
2839
+ /** `clampAround` for a rounded rectangle, keeping its corners. */
2840
+ function clampKeeping(options, { rect, corners }, pad) {
2841
+ const cut = clampAround(options, rect, pad);
2842
+ return cut && { rect: cut, corners };
2843
+ }
2844
+ /** Whether a corner curves further than `by` along either of its sides. */
2845
+ function curvesPast({ x, y }, by) {
2846
+ for (let i = 0; i < 4; i += 1)
2847
+ if (x[i] > by || y[i] > by)
2848
+ return true;
2849
+ return false;
2850
+ }
2781
2851
  function paintBackground(ctx, box, options, style) {
2782
2852
  const color = style.backgroundColor;
2783
2853
  const gradient = style.backgroundGradient;
2784
2854
  const solid = !isTransparent(color);
2785
2855
  if (!solid && !gradient)
2786
2856
  return;
2787
- const area = clipArea(box, options, style, !!(ctx.roundRect && ctx.fill && ctx.beginPath));
2788
- const rect = area && clampRect(options, area.x, area.y, area.w, area.h);
2857
+ const rect = clipArea(box, options, style, !!(ctx.roundRect && ctx.fill && ctx.beginPath));
2789
2858
  if (!rect)
2790
2859
  return;
2791
- const rounded = area.corners;
2860
+ const rounded = rect.corners;
2792
2861
  const fill = () => {
2793
2862
  if (rounded) {
2794
- // The clamp can only have cut edges further than CLAMP_PAD outside
2795
- // the damage, and a sane radius is smaller than that — so a corner
2796
- // that survives the cut is whole, and a cut edge is offscreen.
2797
2863
  ctx.beginPath();
2798
2864
  roundedRect(ctx, rect.x, rect.y, rect.w, rect.h, rounded);
2799
2865
  ctx.fill();
@@ -3521,7 +3587,7 @@ function roundedTile(size, [modeX, modeY], [w, h], at) {
3521
3587
  }
3522
3588
  /**
3523
3589
  * A background tile's size (CSS Backgrounds 3, 3.9, over CSS Images' sizing
3524
- * of an object): `auto` is the image's own, as `tileSize` has it; `cover`
3590
+ * of an object): `auto` is the image's own, as `concreteSize` has it; `cover`
3525
3591
  * and `contain` scale it to fill the positioning area or to fit inside it,
3526
3592
  * keeping its ratio, and one with no ratio is the size of the area; and a
3527
3593
  * width or a height alone takes the other from the ratio, or else from the
@@ -3530,7 +3596,7 @@ function roundedTile(size, [modeX, modeY], [w, h], at) {
3530
3596
  */
3531
3597
  function sizedTile(size, image, area, scale) {
3532
3598
  if (size === 'auto')
3533
- return tileSize(image, area, scale);
3599
+ return concreteSize(image, area.width, area.height, scale);
3534
3600
  const { ratio } = image;
3535
3601
  if (size === 'cover' || size === 'contain') {
3536
3602
  if (!(ratio > 0 && area.height > 0))
@@ -3555,32 +3621,7 @@ function sizedTile(size, image, area, scale) {
3555
3621
  return [h * ratio, h];
3556
3622
  return [image.width === null ? area.width : image.width * scale, h];
3557
3623
  }
3558
- return tileSize(image, area, scale);
3559
- }
3560
- /**
3561
- * A background image's size, in device pixels, where nothing sets it — CSS
3562
- * 2.1 has no `background-size` — which is CSS Images' default sizing: an
3563
- * image's own size where it has one; the dimension it lacks from its ratio,
3564
- * or else from the positioning area; and one with a ratio alone as large as
3565
- * fits in the area. An SVG may be any of these, and a raster image is the
3566
- * first.
3567
- */
3568
- function tileSize(size, area, scale) {
3569
- const { ratio } = size;
3570
- const width = size.width === null ? null : size.width * scale;
3571
- const height = size.height === null ? null : size.height * scale;
3572
- if (width !== null && height !== null)
3573
- return [width, height];
3574
- if (width !== null)
3575
- return [width, ratio > 0 ? width / ratio : area.height];
3576
- if (height !== null)
3577
- return [ratio > 0 ? height * ratio : area.width, height];
3578
- if (ratio > 0) {
3579
- return area.width / area.height > ratio
3580
- ? [area.height * ratio, area.height]
3581
- : [area.width, area.width / ratio];
3582
- }
3583
- return [area.width, area.height];
3624
+ return concreteSize(image, area.width, area.height, scale);
3584
3625
  }
3585
3626
  /**
3586
3627
  * A box's outline (CSS 2.1 18.4, CSS UI 4 5): a border of its own width,
@@ -3645,7 +3686,7 @@ function paintOutline(ctx, box, options) {
3645
3686
  *
3646
3687
  * Where two sides that meet differ in colour — one of them transparent
3647
3688
  * included — the corner is cut on the diagonal from its outer point to its
3648
- * inner one and each side takes its half (CSS Backgrounds 3, 4.3), so a side
3689
+ * inner one and each side takes its half (CSS Backgrounds 3, 4.4), so a side
3649
3690
  * is a trapezoid, and a triangle where the box inside the borders has no
3650
3691
  * width: the CSS triangle a dropdown's caret and a tooltip's arrow are
3651
3692
  * drawn with, which came out as the rectangle around it. Only a solid side
@@ -3754,7 +3795,7 @@ function paintBorders(ctx, box, options) {
3754
3795
  }
3755
3796
  /**
3756
3797
  * Whether the corner two sides meet at is cut on its diagonal, each side
3757
- * taking its half (CSS Backgrounds 3, 4.3): where both have a width, they
3798
+ * taking its half (CSS Backgrounds 3, 4.4): where both have a width, they
3758
3799
  * are painted in different colours or only one is painted at all, and each
3759
3800
  * is solid or unpainted. Sides of one colour make the same corner either
3760
3801
  * way, and so does a corner of a single pixel, which is left to the side
@@ -3767,12 +3808,14 @@ function mitred(a, b, inkA, inkB, styleA, styleB) {
3767
3808
  (inkB === null || styleB === 'solid'));
3768
3809
  }
3769
3810
  /**
3770
- * Fill a convex polygon, its corners as `x, y` pairs, cut to `area`: the
3771
- * part of it near what is painted, which is what keeps a side thousands of
3772
- * pixels long inside X's coordinates. Cut edge by edge rather than by
3773
- * moving its corners in, which would turn a diagonal that crosses the
3774
- * painted area: a slanted divider, `border-left: 100vw solid transparent`,
3775
- * has a corner far outside it.
3811
+ * Fill a polygon, its corners as `x, y` pairs, cut to `area`: the part of
3812
+ * it near what is painted, which is what keeps a side thousands of pixels
3813
+ * long inside X's coordinates. Cut edge by edge rather than by moving its
3814
+ * corners in, which would turn a diagonal that crosses the painted area: a
3815
+ * slanted divider, `border-left: 100vw solid transparent`, has a corner far
3816
+ * outside it. A polygon that is not convex, a rounded ring's share, may
3817
+ * come out of the cut with an edge run there and back along the area's
3818
+ * border, which fills nothing, and that border is outside the paint.
3776
3819
  */
3777
3820
  function fillPolygon(ctx, points, area) {
3778
3821
  const bounds = [area.x, area.y, area.x + area.w, area.y + area.h];
@@ -3843,9 +3886,7 @@ function paintBorderImage(ctx, box, options) {
3843
3886
  // the image's size, CSS Images' default sizing in the area: a drawing
3844
3887
  // with no size of its own is as large as it fits there, and a gradient,
3845
3888
  // which has none, is the area's
3846
- const [cw, ch] = loaded
3847
- ? tileSize(loaded, { x: 0, y: 0, width: aw, height: ah }, scale)
3848
- : [aw, ah];
3889
+ const [cw, ch] = loaded ? concreteSize(loaded, aw, ah, scale) : [aw, ah];
3849
3890
  const iw = cw / scale;
3850
3891
  const ih = ch / scale;
3851
3892
  if (!(iw > 0 && ih > 0))
@@ -4032,17 +4073,18 @@ function edgeRun(mode, start, length, tile) {
4032
4073
  }
4033
4074
  /**
4034
4075
  * A rounded box's border as the ring between its border edge and its
4035
- * padding edge, each rounded, where every side that has a border has one
4036
- * of the same colour and a solid rule: a card's or a button's. Drawn
4037
- * straight, its corners were square over the background's rounded ones,
4038
- * and an accent border down one side did not follow the corner. The inner
4039
- * radius is the outer less the wider border at that corner (CSS
4040
- * Backgrounds 3, 5.2). False where the border is not such a one, and it is
4041
- * drawn a side at a time.
4076
+ * padding edge, each rounded, where every side that has a border has a
4077
+ * solid rule: a card's or a button's. Drawn straight, its corners were
4078
+ * square over the background's rounded ones, and an accent border down one
4079
+ * side did not follow the corner. The inner radius is the outer less the
4080
+ * wider border at that corner (CSS Backgrounds 3, 5.2). Sides of one colour
4081
+ * are one fill; sides of more are each given their share of the ring
4082
+ * (`ringSides`). False where the border is not such a one, and it is drawn
4083
+ * a side at a time.
4042
4084
  */
4043
4085
  function roundedRing(ctx, box, x, y, w, h, options) {
4044
4086
  const s = box.style;
4045
- if (!ctx.roundRect || !ctx.fill || !ctx.beginPath)
4087
+ if (!ctx.fill || !ctx.beginPath)
4046
4088
  return false;
4047
4089
  const corners = cornersOf(s, w, h);
4048
4090
  if (!corners)
@@ -4054,23 +4096,335 @@ function roundedRing(ctx, box, x, y, w, h, options) {
4054
4096
  [box.borderLeft, s.borderLeftColor, s.borderLeftStyle],
4055
4097
  ];
4056
4098
  let color = null;
4099
+ let mixed = false;
4057
4100
  for (const [width, ink, style] of sides) {
4058
4101
  if (!width)
4059
4102
  continue;
4060
- if (style !== 'solid' || (color !== null && ink !== color))
4103
+ if (style !== 'solid')
4061
4104
  return false;
4105
+ if (color !== null && ink !== color)
4106
+ mixed = true;
4062
4107
  color = ink;
4063
4108
  }
4109
+ if (mixed)
4110
+ return ringSides(ctx, box, x, y, w, h, corners, options);
4111
+ if (!ctx.roundRect)
4112
+ return false;
4064
4113
  if (color === null)
4065
4114
  return true;
4066
4115
  if (isTransparent(color))
4067
4116
  return true;
4117
+ wholeRing(ctx, box, inkColor(color, s.color), x, y, w, h, corners, options);
4118
+ return true;
4119
+ }
4120
+ /** The ring of a rounded border in one colour, cut to what the paint
4121
+ * reaches (`cutRing`). */
4122
+ function wholeRing(ctx, box, color, x, y, w, h, corners, options) {
4068
4123
  const ring = cutRing(options, x, y, w, h, corners, box.borderTop, box.borderRight, box.borderBottom, box.borderLeft);
4069
4124
  if (!ring)
4070
- return true;
4125
+ return;
4071
4126
  const { outer, inner } = ring;
4072
- ctx.fillStyle = inkColor(color, s.color);
4073
- fillRing(ctx, outer.rect, outer.corners, inner ? inner.rect : { x: 0, y: 0, width: 0, height: 0 }, inner ? inner.corners : outer.corners);
4127
+ ctx.fillStyle = color;
4128
+ fillRing(ctx, outer.rect, outer.corners, inner ? inner.rect : NOWHERE, inner ? inner.corners : outer.corners);
4129
+ }
4130
+ /** How far a chord of a flattened corner may stray from its curve, in
4131
+ * device pixels: a sixteenth, well inside what antialiasing shows. */
4132
+ const CURVE_TOLERANCE = 1 / 16;
4133
+ /** How far past a cut a share reaches under the one drawn over it, in
4134
+ * device pixels: past the antialiased edge of either. */
4135
+ const SEAM = 1.5;
4136
+ /** Which way a border's sides run at each corner, clockwise from the top
4137
+ * left: the side that ends at corner i arrives along `ALONG[i]`, and the
4138
+ * side that begins there leaves along `ALONG[i + 1]`. */
4139
+ const ALONG = [
4140
+ [0, -1],
4141
+ [1, 0],
4142
+ [0, 1],
4143
+ [-1, 0],
4144
+ ];
4145
+ /**
4146
+ * A rounded border whose solid sides are not all one colour, each side's
4147
+ * share of the ring filled in its own. Where two sides of different colours
4148
+ * meet, the corner is cut on the line from the border box's corner through
4149
+ * the padding box's, as Blink cuts it: CSS Backgrounds 3, 4.4 puts the
4150
+ * transition on the curve at a point that follows the ratio of the two
4151
+ * widths and leaves the rest to the UA. A corner between a side and one
4152
+ * with no width is all the side's (4.4 again), and sides of one colour that
4153
+ * meet are one fill, so nothing is cut where the colour does not change.
4154
+ * Drawn a side at a time, the spinner the web draws — `border-radius: 50%`
4155
+ * with one side of another colour — was a square frame.
4156
+ *
4157
+ * A share is a polygon: the curves flattened to chords no further than
4158
+ * `CURVE_TOLERANCE` from them, so that it is cut to what the paint reaches
4159
+ * as the square corners are (`fillPolygon`) and a large ring repainted a
4160
+ * strip at a time is the same ring. False where the context draws no
4161
+ * lines, and the sides are drawn straight.
4162
+ */
4163
+ function ringSides(ctx, box, x, y, w, h, corners, options) {
4164
+ if (!ctx.moveTo || !ctx.lineTo)
4165
+ return false;
4166
+ const area = clampRect(options, x, y, w, h);
4167
+ if (!area)
4168
+ return true;
4169
+ const s = box.style;
4170
+ const widths = [
4171
+ box.borderTop,
4172
+ box.borderRight,
4173
+ box.borderBottom,
4174
+ box.borderLeft,
4175
+ ];
4176
+ const colors = [
4177
+ s.borderTopColor,
4178
+ s.borderRightColor,
4179
+ s.borderBottomColor,
4180
+ s.borderLeftColor,
4181
+ ];
4182
+ // what each side is painted in, null where it paints nothing
4183
+ const inks = widths.map((width, i) => width > 0 && !isTransparent(colors[i])
4184
+ ? inkColor(colors[i], s.color)
4185
+ : null);
4186
+ // Corners from the top left, clockwise. Corner i is where side i - 1
4187
+ // ends and side i begins: the top left ends the left and begins the top.
4188
+ // Two sides of one colour are joined across their corner.
4189
+ const joined = [0, 1, 2, 3].map((i) => {
4190
+ const before = (i + 3) % 4;
4191
+ return widths[before] > 0 && widths[i] > 0 && inks[before] === inks[i];
4192
+ });
4193
+ if (!joined.includes(false)) {
4194
+ // one colour after all, `currentColor` and the colour it is
4195
+ if (!ctx.roundRect)
4196
+ return false;
4197
+ if (inks[0] !== null) {
4198
+ wholeRing(ctx, box, inks[0], x, y, w, h, corners, options);
4199
+ }
4200
+ return true;
4201
+ }
4202
+ const [t, r, b, l] = widths;
4203
+ const x1 = x + w;
4204
+ const y1 = y + h;
4205
+ // the padding box, inside the borders
4206
+ const ix0 = x + l;
4207
+ const iy0 = y + t;
4208
+ const ix1 = Math.max(ix0, x1 - r);
4209
+ const iy1 = Math.max(iy0, y1 - b);
4210
+ const inside = insetCorners(corners, t, r, b, l);
4211
+ const hollow = ix1 > ix0 && iy1 > iy0;
4212
+ // each corner's point on the border box and on the padding box, which
4213
+ // way it points, and the quarter ellipses that round the two there
4214
+ const sx = [-1, 1, 1, -1];
4215
+ const sy = [-1, -1, 1, 1];
4216
+ const ox = [x, x1, x1, x];
4217
+ const oy = [y, y, y1, y1];
4218
+ const px = [ix0, ix1, ix1, ix0];
4219
+ const py = [iy0, iy0, iy1, iy1];
4220
+ const quarter = (i, cornerX, cornerY, rx, ry) => rx > 0 && ry > 0
4221
+ ? { cx: cornerX - sx[i] * rx, cy: cornerY - sy[i] * ry, rx, ry }
4222
+ : null;
4223
+ const outer = [];
4224
+ const inner = [];
4225
+ for (let i = 0; i < 4; i += 1) {
4226
+ outer.push(quarter(i, ox[i], oy[i], corners.x[i], corners.y[i]));
4227
+ inner.push(hollow ? quarter(i, px[i], py[i], inside.x[i], inside.y[i]) : null);
4228
+ }
4229
+ // A corner's quarter turns clockwise from the angle it starts at: the top
4230
+ // left's from pointing left to pointing up.
4231
+ const QUARTER = Math.PI / 2;
4232
+ const start = (i) => Math.PI + i * QUARTER;
4233
+ /** Where a line from (fromX, fromY) along (dx, dy) first meets a quarter
4234
+ * ellipse, as the angle it is at; the line starts outside it. */
4235
+ const crossing = (i, q, fromX, fromY, dx, dy) => {
4236
+ const a0 = start(i);
4237
+ if (!q)
4238
+ return a0;
4239
+ const ux = (fromX - q.cx) / q.rx;
4240
+ const uy = (fromY - q.cy) / q.ry;
4241
+ const vx = dx / q.rx;
4242
+ const vy = dy / q.ry;
4243
+ const qa = vx * vx + vy * vy;
4244
+ const qb = ux * vx + uy * vy;
4245
+ const disc = qb * qb - qa * (ux * ux + uy * uy - 1);
4246
+ const k = qa > 0 && disc > 0 ? (-qb - Math.sqrt(disc)) / qa : 0;
4247
+ let a = Math.atan2(uy + k * vy, ux + k * vx);
4248
+ // into the quarter's own turn
4249
+ a += 2 * Math.PI * Math.round((a0 + QUARTER / 2 - a) / (2 * Math.PI));
4250
+ return Math.min(a0 + QUARTER, Math.max(a0, a));
4251
+ };
4252
+ // Where the shares meet at each corner, as angles on its two quarters: on
4253
+ // the cut, or at the end of the corner where one side has no width and
4254
+ // the other has all of it — the side that has none paints nothing.
4255
+ const cutOuter = [];
4256
+ const cutInner = [];
4257
+ for (let i = 0; i < 4; i += 1) {
4258
+ const before = widths[(i + 3) % 4] > 0;
4259
+ const after = widths[i] > 0;
4260
+ if (before && after) {
4261
+ const dx = px[i] - ox[i];
4262
+ const dy = py[i] - oy[i];
4263
+ cutOuter.push(crossing(i, outer[i], ox[i], oy[i], dx, dy));
4264
+ cutInner.push(crossing(i, inner[i], px[i], py[i], dx, dy));
4265
+ }
4266
+ else {
4267
+ const end = before ? start(i) + QUARTER : start(i);
4268
+ cutOuter.push(end);
4269
+ cutInner.push(end);
4270
+ }
4271
+ }
4272
+ // The shares, a run of sides of one colour each, from the corner the
4273
+ // colour changes at to the next one it changes at. Those that show what
4274
+ // is under them are drawn first.
4275
+ const runs = [];
4276
+ for (let i = 0; i < 4; i += 1) {
4277
+ if (joined[i])
4278
+ continue;
4279
+ let j = (i + 1) % 4;
4280
+ while (joined[j])
4281
+ j = (j + 1) % 4;
4282
+ runs.push({ from: i, to: j, ink: inks[i] });
4283
+ }
4284
+ const opaque = (ink) => ink !== null && alphaOf(ink) === 1;
4285
+ runs.sort((a, b) => Number(opaque(a.ink)) - Number(opaque(b.ink)));
4286
+ /** Whether a share reaches past its cut at a corner, under the one on
4287
+ * the other side of it: where that one is drawn after it and covers it. */
4288
+ const under = (k, corner) => {
4289
+ const before = widths[(corner + 3) % 4] > 0;
4290
+ if (!(before && widths[corner] > 0))
4291
+ return false;
4292
+ const other = runs.findIndex((run) => run !== runs[k] && (run.from === corner || run.to === corner));
4293
+ return other > k && opaque(runs[other].ink);
4294
+ };
4295
+ // The straight part of each side's edge, between the quarters at its
4296
+ // ends, on the border edge and the padding edge: as far as a share may
4297
+ // reach along it past a corner.
4298
+ const straight = (q, across, down) => {
4299
+ const rx = (i) => q[i]?.rx ?? 0;
4300
+ const ry = (i) => q[i]?.ry ?? 0;
4301
+ return [
4302
+ across - rx(0) - rx(1),
4303
+ down - ry(1) - ry(2),
4304
+ across - rx(2) - rx(3),
4305
+ down - ry(3) - ry(0),
4306
+ ].map((v) => Math.max(0, v));
4307
+ };
4308
+ const outerRoom = straight(outer, w, h);
4309
+ const innerRoom = straight(inner, ix1 - ix0, iy1 - iy0);
4310
+ /**
4311
+ * A share's end moved past the cut at a corner, onto the other share,
4312
+ * along an edge from the angle `a` it is at on a quarter: `forward` the
4313
+ * way the corner turns, onto the side that begins there, or back onto
4314
+ * the side that ends there. Far enough that the line it ends on is
4315
+ * `SEAM` from the cut, whatever angle the two make; as the angle it
4316
+ * reaches on the quarter and how far it goes on along the straight edge
4317
+ * past it, no further than `room`.
4318
+ */
4319
+ const past = (q, i, a, forward, room) => {
4320
+ const end = forward ? start(i) + QUARTER : start(i);
4321
+ const sign = forward ? 1 : -1;
4322
+ const [ex, ey] = forward ? ALONG[(i + 1) % 4] : ALONG[i];
4323
+ // the way it goes, and the sine of its angle with the cut
4324
+ let tx = forward ? ex : -ex;
4325
+ let ty = forward ? ey : -ey;
4326
+ if (q && a !== end) {
4327
+ const speed = Math.hypot(q.rx * Math.sin(a), q.ry * Math.cos(a));
4328
+ tx = (sign * -q.rx * Math.sin(a)) / speed;
4329
+ ty = (sign * q.ry * Math.cos(a)) / speed;
4330
+ }
4331
+ const dx = px[i] - ox[i];
4332
+ const dy = py[i] - oy[i];
4333
+ const sine = Math.abs(tx * dy - ty * dx) / Math.hypot(dx, dy);
4334
+ let left = SEAM / Math.max(0.25, sine);
4335
+ let at = a;
4336
+ for (let n = 0; q && n < 8 && left > 0 && at !== end; n += 1) {
4337
+ const speed = Math.hypot(q.rx * Math.sin(at), q.ry * Math.cos(at));
4338
+ const turn = Math.abs(end - at);
4339
+ if (left >= turn * speed) {
4340
+ left -= turn * speed;
4341
+ at = end;
4342
+ }
4343
+ else {
4344
+ at += (sign * left) / speed;
4345
+ left = 0;
4346
+ }
4347
+ }
4348
+ return [at, at === end ? Math.min(left, room) : 0];
4349
+ };
4350
+ const points = [];
4351
+ /** The point at an angle on a quarter, or the corner where it is square,
4352
+ * and `over` on past the quarter's end along the edge `along`. */
4353
+ const point = (q, a, cx, cy, over = 0, along = ALONG[0]) => {
4354
+ const x = (q ? q.cx + q.rx * Math.cos(a) : cx) + over * along[0];
4355
+ const y = (q ? q.cy + q.ry * Math.sin(a) : cy) + over * along[1];
4356
+ const n = points.length;
4357
+ if (n && points[n - 2] === x && points[n - 1] === y)
4358
+ return;
4359
+ points.push(x, y);
4360
+ };
4361
+ /** A quarter's curve from one angle to another, as chords, after the
4362
+ * point it starts at. */
4363
+ const sweep = (q, from, to) => {
4364
+ if (!q || from === to)
4365
+ return;
4366
+ const radius = Math.max(q.rx, q.ry);
4367
+ const step = radius > CURVE_TOLERANCE
4368
+ ? 2 * Math.acos(1 - CURVE_TOLERANCE / radius)
4369
+ : QUARTER;
4370
+ const n = Math.min(64, Math.ceil(Math.abs(to - from) / step));
4371
+ for (let k = 1; k <= n; k += 1) {
4372
+ point(q, from + ((to - from) * k) / n, 0, 0);
4373
+ }
4374
+ };
4375
+ for (let k = 0; k < runs.length; k += 1) {
4376
+ const { from: i, to: j, ink } = runs[k];
4377
+ if (ink === null)
4378
+ continue;
4379
+ // Where the share starts and ends on each edge. Two antialiased fills
4380
+ // that meet on a line each cover half of the pixels along it, and what
4381
+ // is under the border shows through between them. So a share reaches
4382
+ // on under the one drawn after it and covering it, and that one's
4383
+ // edge is the cut, over it — as Blink overdraws a corner, and as the
4384
+ // square corners are drawn.
4385
+ let [so, soOver] = [cutOuter[i], 0];
4386
+ let [si, siOver] = [cutInner[i], 0];
4387
+ let [eo, eoOver] = [cutOuter[j], 0];
4388
+ let [ei, eiOver] = [cutInner[j], 0];
4389
+ const back = (i + 3) % 4;
4390
+ if (under(k, i)) {
4391
+ [so, soOver] = past(outer[i], i, so, false, outerRoom[back]);
4392
+ [si, siOver] = past(inner[i], i, si, false, innerRoom[back]);
4393
+ }
4394
+ if (under(k, j)) {
4395
+ [eo, eoOver] = past(outer[j], j, eo, true, outerRoom[j]);
4396
+ [ei, eiOver] = past(inner[j], j, ei, true, innerRoom[j]);
4397
+ }
4398
+ const onward = ALONG[(j + 1) % 4];
4399
+ const backward = [-ALONG[i][0], -ALONG[i][1]];
4400
+ points.length = 0;
4401
+ // along the border edge, clockwise
4402
+ point(outer[i], so, ox[i], oy[i], soOver, backward);
4403
+ point(outer[i], so, ox[i], oy[i]);
4404
+ sweep(outer[i], so, start(i) + QUARTER);
4405
+ for (let c = (i + 1) % 4;; c = (c + 1) % 4) {
4406
+ point(outer[c], start(c), ox[c], oy[c]);
4407
+ if (c === j)
4408
+ break;
4409
+ sweep(outer[c], start(c), start(c) + QUARTER);
4410
+ }
4411
+ sweep(outer[j], start(j), eo);
4412
+ point(outer[j], eo, ox[j], oy[j], eoOver, onward);
4413
+ // and back along the padding edge
4414
+ point(inner[j], ei, px[j], py[j], eiOver, onward);
4415
+ point(inner[j], ei, px[j], py[j]);
4416
+ sweep(inner[j], ei, start(j));
4417
+ for (let c = (j + 3) % 4;; c = (c + 3) % 4) {
4418
+ point(inner[c], start(c) + QUARTER, px[c], py[c]);
4419
+ if (c === i)
4420
+ break;
4421
+ sweep(inner[c], start(c) + QUARTER, start(c));
4422
+ }
4423
+ sweep(inner[i], start(i) + QUARTER, si);
4424
+ point(inner[i], si, px[i], py[i], siOver, backward);
4425
+ ctx.fillStyle = ink;
4426
+ fillPolygon(ctx, points, area);
4427
+ }
4074
4428
  return true;
4075
4429
  }
4076
4430
  /**
@@ -4081,68 +4435,82 @@ function roundedRing(ctx, box, x, y, w, h, options) {
4081
4435
  * the box first and insetting the cut by the borders put the hole a
4082
4436
  * border's width in from the cut rather than from the box's edge, and left
4083
4437
  * none where a border was wider than `CLAMP_PAD`: the ring filled whatever
4084
- * of the box a paint reached. A cut is straight, so a side of the window
4085
- * is moved out past any corner's curve it would cross, and a corner on a
4086
- * side the window cut is square, its curve being outside. Null where the
4087
- * ring has nothing in the window.
4438
+ * of the box a paint reached. The window is the border edge's
4439
+ * (`roundedWindow`), whose curves the padding edge's run inside, and each
4440
+ * edge is cut to it as `cutRounded` cuts one. Null where the ring has
4441
+ * nothing in the window.
4088
4442
  */
4089
4443
  function cutRing(options, x, y, w, h, corners, top, right, bottom, left) {
4090
- const { x: cx, y: cy } = corners;
4091
- // where each corner's curve runs, across and down — the padding edge's
4092
- // run inside the border edge's
4093
- const across = [
4094
- [x, x + cx[0]],
4095
- [x + w - cx[1], x + w],
4096
- [x + w - cx[2], x + w],
4097
- [x, x + cx[3]],
4098
- ];
4099
- const down = [
4100
- [y, y + cy[0]],
4101
- [y, y + cy[1]],
4102
- [y + h - cy[2], y + h],
4103
- [y + h - cy[3], y + h],
4104
- ];
4105
- const d = options.damage;
4106
- const side = (at, spans, towards) => Math.max(-COORD_LIMIT, Math.min(COORD_LIMIT, outOfSpans(at, spans, towards)));
4107
- const x0 = side(d ? d.x - CLAMP_PAD : -COORD_LIMIT, across, -1);
4108
- const y0 = side(d ? d.y - CLAMP_PAD : -COORD_LIMIT, down, -1);
4109
- const x1 = side(d ? d.x + d.width + CLAMP_PAD : COORD_LIMIT, across, 1);
4110
- const y1 = side(d ? d.y + d.height + CLAMP_PAD : COORD_LIMIT, down, 1);
4111
- const cut = (rx, ry, rw, rh, c) => {
4112
- const l = Math.max(rx, x0);
4113
- const t = Math.max(ry, y0);
4114
- const r = Math.min(rx + rw, x1);
4115
- const b = Math.min(ry + rh, y1);
4116
- if (r <= l || b <= t)
4117
- return null;
4118
- // the corners of the sides the window left where they were
4119
- const kept = [
4120
- t === ry && l === rx,
4121
- t === ry && r === rx + rw,
4122
- b === ry + rh && r === rx + rw,
4123
- b === ry + rh && l === rx,
4124
- ];
4125
- return {
4126
- rect: { x: l, y: t, width: r - l, height: b - t },
4127
- corners: {
4128
- x: c.x.map((v, i) => (kept[i] ? v : 0)),
4129
- y: c.y.map((v, i) => (kept[i] ? v : 0)),
4130
- },
4131
- };
4132
- };
4133
- const outer = cut(x, y, w, h, corners);
4444
+ const edge = { rect: { x, y, width: w, height: h }, corners };
4445
+ const cut = roundedWindow(options, [edge]);
4446
+ const outer = cutRounded(cut, edge);
4134
4447
  if (!outer)
4135
4448
  return null;
4136
4449
  const iw = w - left - right;
4137
4450
  const ih = h - top - bottom;
4138
4451
  const inner = iw > 0 && ih > 0
4139
- ? cut(x + left, y + top, iw, ih, insetCorners(corners, top, right, bottom, left))
4452
+ ? cutRounded(cut, {
4453
+ rect: { x: x + left, y: y + top, width: iw, height: ih },
4454
+ corners: insetCorners(corners, top, right, bottom, left),
4455
+ })
4140
4456
  : null;
4141
4457
  // a window inside the padding edge, where the ring has nothing
4142
4458
  if (inner && sameRounded(outer, inner))
4143
4459
  return null;
4144
4460
  return { outer, inner };
4145
4461
  }
4462
+ /**
4463
+ * The window `clampRect` cuts to, `pad` round what the paint reaches, for
4464
+ * rectangles with rounded corners: a cut is straight, so a side of it is
4465
+ * moved out past any corner's curve it would cross, in any of them.
4466
+ */
4467
+ function roundedWindow(options, shapes, pad = CLAMP_PAD) {
4468
+ // where each corner's curve runs, across and down
4469
+ const across = [];
4470
+ const down = [];
4471
+ for (const { rect, corners } of shapes) {
4472
+ const { x, y, width: w, height: h } = rect;
4473
+ const { x: cx, y: cy } = corners;
4474
+ across.push([x, x + cx[0]], [x + w - cx[1], x + w], [x + w - cx[2], x + w], [x, x + cx[3]]);
4475
+ down.push([y, y + cy[0]], [y, y + cy[1]], [y + h - cy[2], y + h], [y + h - cy[3], y + h]);
4476
+ }
4477
+ const d = options.damage;
4478
+ const side = (at, spans, towards) => Math.max(-COORD_LIMIT, Math.min(COORD_LIMIT, outOfSpans(at, spans, towards)));
4479
+ return [
4480
+ side(d ? d.x - pad : -COORD_LIMIT, across, -1),
4481
+ side(d ? d.y - pad : -COORD_LIMIT, down, -1),
4482
+ side(d ? d.x + d.width + pad : COORD_LIMIT, across, 1),
4483
+ side(d ? d.y + d.height + pad : COORD_LIMIT, down, 1),
4484
+ ];
4485
+ }
4486
+ /**
4487
+ * A rounded rectangle cut to a window (`roundedWindow`), as `clampRect`
4488
+ * cuts a rectangle: a corner on a side the window cut is square, its
4489
+ * curve being outside, and the rest are whole. Null where none of it is in
4490
+ * the window.
4491
+ */
4492
+ function cutRounded([x0, y0, x1, y1], { rect: { x, y, width: w, height: h }, corners: c }) {
4493
+ const l = Math.max(x, x0);
4494
+ const t = Math.max(y, y0);
4495
+ const r = Math.min(x + w, x1);
4496
+ const b = Math.min(y + h, y1);
4497
+ if (r <= l || b <= t)
4498
+ return null;
4499
+ // the corners of the sides the window left where they were
4500
+ const kept = [
4501
+ t === y && l === x,
4502
+ t === y && r === x + w,
4503
+ b === y + h && r === x + w,
4504
+ b === y + h && l === x,
4505
+ ];
4506
+ return {
4507
+ rect: { x: l, y: t, width: r - l, height: b - t },
4508
+ corners: {
4509
+ x: c.x.map((v, i) => (kept[i] ? v : 0)),
4510
+ y: c.y.map((v, i) => (kept[i] ? v : 0)),
4511
+ },
4512
+ };
4513
+ }
4146
4514
  /** A position moved out of any of `spans` it is inside, towards their
4147
4515
  * starts where `towards` is negative and their ends where it is not. */
4148
4516
  function outOfSpans(at, spans, towards) {
@@ -4419,11 +4787,10 @@ function paintCollapsedBorders(ctx, table, options) {
4419
4787
  for (const s of segments) {
4420
4788
  if (isTransparent(s.border.color))
4421
4789
  continue;
4422
- const rect = clampRect(options, s.x, s.y, s.w, s.h);
4423
- if (!rect)
4790
+ if (!clampRect(options, s.x, s.y, s.w, s.h))
4424
4791
  continue;
4425
4792
  ctx.fillStyle = s.border.color;
4426
- fillEdge(ctx, rect, s.horizontal ? s.x : s.y, s.border.style, s.horizontal);
4793
+ fillEdge(ctx, options, s.x, s.y, s.w, s.h, s.border.style, s.horizontal);
4427
4794
  }
4428
4795
  }
4429
4796
  /**
@@ -4475,13 +4842,10 @@ function fillSide(ctx, options, at, x, y, w, h, widths, style, joins) {
4475
4842
  }
4476
4843
  const ex = at === 1 ? x + w - r : x;
4477
4844
  const ey = at === 0 ? y : at === 2 ? y + h - b : y + t;
4478
- const rect = horizontal
4479
- ? clampRect(options, ex, ey, w, width)
4480
- : clampRect(options, ex, ey, width, h - t - b);
4481
- // The un-clamped start is the dash phase's origin, so the pattern does
4482
- // not crawl as the viewport moves along a long edge.
4483
- if (rect)
4484
- fillEdge(ctx, rect, horizontal ? ex : ey, style, horizontal);
4845
+ if (horizontal)
4846
+ fillEdge(ctx, options, ex, ey, w, width, style, true);
4847
+ else
4848
+ fillEdge(ctx, options, ex, ey, width, h - t - b, style, false);
4485
4849
  }
4486
4850
  /**
4487
4851
  * The corners of a box where two double sides of one colour meet, the top
@@ -4511,44 +4875,60 @@ const UNJOINED = [false, false, false, false];
4511
4875
  function doubleBand(thickness) {
4512
4876
  return Math.max(1, Math.floor(thickness / 3));
4513
4877
  }
4514
- function fillEdge(ctx, rect, phaseOrigin, style, horizontal) {
4515
- const { x, y, w, h } = rect;
4878
+ /**
4879
+ * A side at `x, y`, `w` by `h`, running across when `horizontal`, as the
4880
+ * rectangles its style makes, each cut to what the paint reaches. The
4881
+ * dashes, the dots and a double side's lines are measured from the side
4882
+ * and cut after: measured from the cut, a side wider than `CLAMP_PAD` had
4883
+ * dashes as long and as far apart as the cut was wide, starting wherever
4884
+ * it started, and a strip repainted down one dash was striped across it.
4885
+ */
4886
+ function fillEdge(ctx, options, x, y, w, h, style, horizontal) {
4516
4887
  const length = horizontal ? w : h;
4517
4888
  const thickness = horizontal ? h : w;
4518
- if (length <= 0 || thickness <= 0)
4889
+ if (!(length > 0 && thickness > 0))
4519
4890
  return;
4891
+ const fill = (fx, fy, fw, fh) => {
4892
+ const rect = clampRect(options, fx, fy, fw, fh);
4893
+ if (rect)
4894
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
4895
+ };
4520
4896
  if (style === 'dashed' || style === 'dotted') {
4897
+ // what of the side the paint reaches, so that a side far longer than
4898
+ // that is not walked from its start
4899
+ const cut = clampRect(options, x, y, w, h);
4900
+ if (!cut)
4901
+ return;
4521
4902
  const period = style === 'dotted' ? thickness * 2 : thickness * 3;
4522
4903
  const on = style === 'dotted' ? thickness : thickness * 2;
4523
- const from = horizontal ? x : y;
4524
- // Start on the pattern boundary at or before the clamped start, so the
4525
- // dash the viewport cuts into is the same dash it always was.
4526
- let i = Math.floor((from - phaseOrigin) / period) * period + phaseOrigin;
4527
- for (; i < from + length; i += period) {
4528
- const start = Math.max(i, from);
4529
- const run = Math.min(i + on, from + length) - start;
4530
- if (run <= 0)
4531
- continue;
4904
+ const start = horizontal ? x : y;
4905
+ const end = start + length;
4906
+ const from = horizontal ? cut.x : cut.y;
4907
+ const to = from + (horizontal ? cut.w : cut.h);
4908
+ // from the side's start, so the dash the paint cuts into is the one it
4909
+ // always was
4910
+ for (let i = start + Math.floor((from - start) / period) * period; i < to; i += period) {
4911
+ const run = Math.min(i + on, end) - i;
4532
4912
  if (horizontal)
4533
- ctx.fillRect(start, y, run, thickness);
4913
+ fill(i, y, run, thickness);
4534
4914
  else
4535
- ctx.fillRect(x, start, thickness, run);
4915
+ fill(x, i, thickness, run);
4536
4916
  }
4537
4917
  return;
4538
4918
  }
4539
4919
  if (style === 'double' && thickness >= 3) {
4540
4920
  const band = doubleBand(thickness);
4541
4921
  if (horizontal) {
4542
- ctx.fillRect(x, y, length, band);
4543
- ctx.fillRect(x, y + thickness - band, length, band);
4922
+ fill(x, y, length, band);
4923
+ fill(x, y + thickness - band, length, band);
4544
4924
  }
4545
4925
  else {
4546
- ctx.fillRect(x, y, band, length);
4547
- ctx.fillRect(x + thickness - band, y, band, length);
4926
+ fill(x, y, band, length);
4927
+ fill(x + thickness - band, y, band, length);
4548
4928
  }
4549
4929
  return;
4550
4930
  }
4551
- ctx.fillRect(x, y, w, h);
4931
+ fill(x, y, w, h);
4552
4932
  }
4553
4933
  /** A list item's bullet or number, in the margin. */
4554
4934
  function paintMarker(ctx, marker, options,
@@ -4943,7 +5323,7 @@ function paintClippedText(ctx, block, lines, options) {
4943
5323
  Math.max(natural.y + natural.height, natural.baseline + (natural.descent ?? 0)) +
4944
5324
  2;
4945
5325
  for (const run of natural.runs) {
4946
- const clip = clipBoxOf(text.spans.boxAt?.(run.start) ?? null, block);
5326
+ const clip = clipBoxOf(text.spans.boxAt?.(run.start) ?? null);
4947
5327
  if (!clip)
4948
5328
  continue;
4949
5329
  let byLayout = parts.get(clip);
@@ -4967,8 +5347,8 @@ function paintClippedText(ctx, block, lines, options) {
4967
5347
  const list = [...byLayout.values()];
4968
5348
  const style = clip.style;
4969
5349
  let area;
4970
- if (clip === block)
4971
- area = paddingBox(block, options);
5350
+ if (clip.kind !== 'inline')
5351
+ area = paddingBox(clip, options);
4972
5352
  else {
4973
5353
  // an inline box: its runs, and its padding round them
4974
5354
  let x0 = Infinity;