@react-x11/components 0.14.0 → 0.14.1

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 (51) hide show
  1. package/dist/html/css/cascade.d.ts +42 -1
  2. package/dist/html/css/cascade.d.ts.map +1 -1
  3. package/dist/html/css/cascade.js +310 -12
  4. package/dist/html/css/cascade.js.map +1 -1
  5. package/dist/html/css/shapes.d.ts +44 -0
  6. package/dist/html/css/shapes.d.ts.map +1 -0
  7. package/dist/html/css/shapes.js +194 -0
  8. package/dist/html/css/shapes.js.map +1 -0
  9. package/dist/html/css/style.d.ts.map +1 -1
  10. package/dist/html/css/style.js +1 -15
  11. package/dist/html/css/style.js.map +1 -1
  12. package/dist/html/layout/block.d.ts +16 -5
  13. package/dist/html/layout/block.d.ts.map +1 -1
  14. package/dist/html/layout/block.js +26 -5
  15. package/dist/html/layout/block.js.map +1 -1
  16. package/dist/html/layout/boxes.d.ts +13 -0
  17. package/dist/html/layout/boxes.d.ts.map +1 -1
  18. package/dist/html/layout/boxes.js +18 -0
  19. package/dist/html/layout/boxes.js.map +1 -1
  20. package/dist/html/layout/css-grid.d.ts.map +1 -1
  21. package/dist/html/layout/css-grid.js +58 -10
  22. package/dist/html/layout/css-grid.js.map +1 -1
  23. package/dist/html/layout/flex.js +19 -6
  24. package/dist/html/layout/flex.js.map +1 -1
  25. package/dist/html/layout/table.d.ts.map +1 -1
  26. package/dist/html/layout/table.js +38 -16
  27. package/dist/html/layout/table.js.map +1 -1
  28. package/dist/html/node.d.ts +1 -0
  29. package/dist/html/node.d.ts.map +1 -1
  30. package/dist/html/node.js +57 -12
  31. package/dist/html/node.js.map +1 -1
  32. package/dist/html/paint.d.ts +4 -1
  33. package/dist/html/paint.d.ts.map +1 -1
  34. package/dist/html/paint.js +269 -84
  35. package/dist/html/paint.js.map +1 -1
  36. package/dist/html/svg.d.ts +27 -10
  37. package/dist/html/svg.d.ts.map +1 -1
  38. package/dist/html/svg.js +259 -46
  39. package/dist/html/svg.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/html/css/cascade.ts +343 -8
  42. package/src/html/css/shapes.ts +239 -0
  43. package/src/html/css/style.ts +1 -13
  44. package/src/html/layout/block.ts +27 -5
  45. package/src/html/layout/boxes.ts +30 -0
  46. package/src/html/layout/css-grid.ts +77 -13
  47. package/src/html/layout/flex.ts +22 -9
  48. package/src/html/layout/table.ts +45 -18
  49. package/src/html/node.ts +54 -11
  50. package/src/html/paint.ts +379 -90
  51. package/src/html/svg.ts +318 -39
package/src/html/paint.ts CHANGED
@@ -88,6 +88,7 @@ import type {
88
88
  LineText,
89
89
  Marker,
90
90
  SelectionStyler,
91
+ ShapeStyler,
91
92
  } from './layout/boxes.js';
92
93
  import type { SelectionStyle } from './css/cascade.js';
93
94
  import {
@@ -247,6 +248,9 @@ export interface PaintOptions {
247
248
  /** @internal Each box's `::selection`, where a rule styles one
248
249
  * (`BoxTree.selectionStyler`). */
249
250
  selectionStyler?: SelectionStyler | null;
251
+ /** @internal What the rules give the shapes in each drawing, where one
252
+ * could reach any (`BoxTree.shapeStyler`). */
253
+ shapeStyler?: ShapeStyler | null;
250
254
  }
251
255
 
252
256
  /**
@@ -705,6 +709,7 @@ export function paintDocument(
705
709
  canvasSource: canvas?.source,
706
710
  negative: tree.negative,
707
711
  selectionStyler: options.selection ? tree.selectionStyler : null,
712
+ shapeStyler: tree.shapeStyler,
708
713
  });
709
714
  ctx.restore();
710
715
  }
@@ -1535,7 +1540,9 @@ function paintContent(
1535
1540
  for (const child of below) paintStacked(ctx, child, options);
1536
1541
  }
1537
1542
  if (visible) {
1538
- if (box.marker) paintMarker(ctx, box.marker, options);
1543
+ if (box.marker) {
1544
+ paintMarker(ctx, box.marker, options, box.style.colorScheme);
1545
+ }
1539
1546
  if (box.replaced === 'image') paintImage(ctx, box, options);
1540
1547
  else if (box.replaced === 'svg') paintSvg(ctx, box, options);
1541
1548
  }
@@ -2250,7 +2257,7 @@ function paintFlowLines(
2250
2257
  }
2251
2258
  if (!intersects(child, options)) continue;
2252
2259
  if (child.marker && child.style.visibility === 'visible') {
2253
- paintMarker(ctx, child.marker, options);
2260
+ paintMarker(ctx, child.marker, options, child.style.colorScheme);
2254
2261
  }
2255
2262
  if (child.lines) paintLines(ctx, child, options);
2256
2263
  paintFlowLines(ctx, child, options, outlines);
@@ -4230,11 +4237,20 @@ function paintBackgroundImage(
4230
4237
  // a drawing is drawn a tile at a time, at the size it was given
4231
4238
  if (tiles <= MAX_TILES) {
4232
4239
  for (let y = fromY; y < toY; y += stepY) {
4233
- for (let x = fromX; x < toX; x += stepX)
4234
- svg.draw(ctx, Math.round(x), Math.round(y), iw, ih, scale);
4240
+ for (let x = fromX; x < toX; x += stepX) {
4241
+ svg.drawImage(
4242
+ ctx,
4243
+ Math.round(x),
4244
+ Math.round(y),
4245
+ iw,
4246
+ ih,
4247
+ scale,
4248
+ style.colorScheme,
4249
+ );
4250
+ }
4235
4251
  }
4236
4252
  } else {
4237
- svg.draw(ctx, x0, y0, iw, ih, scale);
4253
+ svg.drawImage(ctx, x0, y0, iw, ih, scale, style.colorScheme);
4238
4254
  }
4239
4255
  } else if (
4240
4256
  tiles > 1 &&
@@ -4478,7 +4494,8 @@ function paintOutline(
4478
4494
  * width: the CSS triangle a dropdown's caret and a tooltip's arrow are
4479
4495
  * drawn with, which came out as the rectangle around it. Only a solid side
4480
4496
  * is cut, and only against a side that is solid or paints nothing; dots,
4481
- * dashes and a double border's two lines keep their rectangles.
4497
+ * dashes and a double border's two lines keep their rectangles, the lines
4498
+ * joined where two double sides of one colour meet (`fillSide`).
4482
4499
  */
4483
4500
  function paintBorders(
4484
4501
  ctx: PaintContext,
@@ -4523,21 +4540,16 @@ function paintBorders(
4523
4540
  paths && mitred(b, r, bottomInk, rightInk, bottomStyle, rightStyle);
4524
4541
  const bl = paths && mitred(b, l, bottomInk, leftInk, bottomStyle, leftStyle);
4525
4542
 
4543
+ const widths = [t, r, b, l];
4544
+ const joins = doubleJoins(box);
4545
+ /** A side with square corners, the top first and clockwise. */
4526
4546
  const edge = (
4527
- ex: number,
4528
- ey: number,
4529
- ew: number,
4530
- eh: number,
4547
+ at: number,
4531
4548
  style: ComputedStyle['borderTopStyle'],
4532
4549
  color: string,
4533
- horizontal: boolean,
4534
4550
  ): void => {
4535
- const rect = clampRect(options, ex, ey, ew, eh);
4536
- if (!rect) return;
4537
4551
  ctx.fillStyle = color;
4538
- // The un-clamped start is the dash phase's origin, so the pattern does
4539
- // not crawl as the viewport moves along a long edge.
4540
- fillEdge(ctx, rect, horizontal ? ex : ey, style, horizontal);
4552
+ fillSide(ctx, options, at, x, y, w, h, widths, style, joins);
4541
4553
  };
4542
4554
  /** A side with a cut corner: its outer edge and then its inner one. */
4543
4555
  const side = (points: number[], color: string): void => {
@@ -4568,7 +4580,7 @@ function paintBorders(
4568
4580
  [x, y, x1, y, cutRight ? px1 : x1, py0, cutLeft ? px0 : x, py0],
4569
4581
  topInk,
4570
4582
  );
4571
- } else edge(x, y, w, t, topStyle, topInk, true);
4583
+ } else edge(0, topStyle, topInk);
4572
4584
  }
4573
4585
  if (bottomInk !== null) {
4574
4586
  const cutLeft = bl && !over(leftInk);
@@ -4578,17 +4590,17 @@ function paintBorders(
4578
4590
  [x1, y1, x, y1, cutLeft ? px0 : x, py1, cutRight ? px1 : x1, py1],
4579
4591
  bottomInk,
4580
4592
  );
4581
- } else edge(x, y1 - b, w, b, bottomStyle, bottomInk, true);
4593
+ } else edge(2, bottomStyle, bottomInk);
4582
4594
  }
4583
4595
  if (leftInk !== null) {
4584
4596
  if (tl || bl) {
4585
4597
  side([x, bl ? y1 : py1, x, tl ? y : py0, px0, py0, px0, py1], leftInk);
4586
- } else edge(x, y + t, l, h - t - b, leftStyle, leftInk, false);
4598
+ } else edge(3, leftStyle, leftInk);
4587
4599
  }
4588
4600
  if (rightInk !== null) {
4589
4601
  if (tr || br) {
4590
4602
  side([x1, tr ? y : py0, x1, br ? y1 : py1, px1, py1, px1, py0], rightInk);
4591
- } else edge(x1 - r, y + t, r, h - t - b, rightStyle, rightInk, false);
4603
+ } else edge(1, rightStyle, rightInk);
4592
4604
  }
4593
4605
  }
4594
4606
 
@@ -4715,12 +4727,13 @@ function paintBorderImage(
4715
4727
  const svg = image instanceof SvgDrawing ? image : null;
4716
4728
  const w = Math.max(1, Math.round(cw));
4717
4729
  const h = Math.max(1, Math.round(ch));
4730
+ const scheme = box.style.colorScheme;
4718
4731
  const drawn = options.cached?.(
4719
- `border-image|${key}|${w}x${h}`,
4732
+ `border-image|${key}|${w}x${h}|${scheme}`,
4720
4733
  w,
4721
4734
  h,
4722
4735
  (sctx) => {
4723
- if (svg) svg.draw(sctx, 0, 0, w, h, scale);
4736
+ if (svg) svg.drawImage(sctx, 0, 0, w, h, scale, scheme);
4724
4737
  else if (typeof source !== 'string') {
4725
4738
  const paint = gradientFill(sctx, source, 0, 0, w, h, box.style.color);
4726
4739
  if (paint) fillGradient(sctx, paint, { x: 0, y: 0, w, h });
@@ -4942,28 +4955,164 @@ function roundedRing(
4942
4955
  }
4943
4956
  if (color === null) return true;
4944
4957
  if (isTransparent(color)) return true;
4945
- const rect = clampRect(options, x, y, w, h);
4946
- if (!rect) return true;
4947
- const top = box.borderTop;
4948
- const right = box.borderRight;
4949
- const bottom = box.borderBottom;
4950
- const left = box.borderLeft;
4958
+ const ring = cutRing(
4959
+ options,
4960
+ x,
4961
+ y,
4962
+ w,
4963
+ h,
4964
+ corners,
4965
+ box.borderTop,
4966
+ box.borderRight,
4967
+ box.borderBottom,
4968
+ box.borderLeft,
4969
+ );
4970
+ if (!ring) return true;
4971
+ const { outer, inner } = ring;
4951
4972
  ctx.fillStyle = inkColor(color, s.color);
4952
4973
  fillRing(
4953
4974
  ctx,
4954
- { x: rect.x, y: rect.y, width: rect.w, height: rect.h },
4955
- corners,
4956
- {
4957
- x: rect.x + left,
4958
- y: rect.y + top,
4959
- width: Math.max(0, rect.w - left - right),
4960
- height: Math.max(0, rect.h - top - bottom),
4961
- },
4962
- insetCorners(corners, top, right, bottom, left),
4975
+ outer.rect,
4976
+ outer.corners,
4977
+ inner ? inner.rect : { x: 0, y: 0, width: 0, height: 0 },
4978
+ inner ? inner.corners : outer.corners,
4963
4979
  );
4964
4980
  return true;
4965
4981
  }
4966
4982
 
4983
+ /** A rectangle and the radii of its corners. */
4984
+ interface Rounded {
4985
+ rect: Rect;
4986
+ corners: Corners;
4987
+ }
4988
+
4989
+ /**
4990
+ * A rounded box's ring — its border edge and its padding edge, the box
4991
+ * inside its borders — cut to the neighbourhood of what is being painted,
4992
+ * as `clampRect` cuts a rectangle. Both edges are the box's own, cut to
4993
+ * one window, so what is left is the part of the true ring in it. Cutting
4994
+ * the box first and insetting the cut by the borders put the hole a
4995
+ * border's width in from the cut rather than from the box's edge, and left
4996
+ * 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.
5001
+ */
5002
+ function cutRing(
5003
+ options: PaintOptions,
5004
+ x: number,
5005
+ y: number,
5006
+ w: number,
5007
+ h: number,
5008
+ corners: Corners,
5009
+ top: number,
5010
+ right: number,
5011
+ bottom: number,
5012
+ left: number,
5013
+ ): { 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);
5067
+ if (!outer) return null;
5068
+ const iw = w - left - right;
5069
+ const ih = h - top - bottom;
5070
+ const inner =
5071
+ iw > 0 && ih > 0
5072
+ ? cut(
5073
+ x + left,
5074
+ y + top,
5075
+ iw,
5076
+ ih,
5077
+ insetCorners(corners, top, right, bottom, left),
5078
+ )
5079
+ : null;
5080
+ // a window inside the padding edge, where the ring has nothing
5081
+ if (inner && sameRounded(outer, inner)) return null;
5082
+ return { outer, inner };
5083
+ }
5084
+
5085
+ /** A position moved out of any of `spans` it is inside, towards their
5086
+ * starts where `towards` is negative and their ends where it is not. */
5087
+ function outOfSpans(
5088
+ at: number,
5089
+ spans: readonly [number, number][],
5090
+ towards: number,
5091
+ ): number {
5092
+ for (let moved = true; moved;) {
5093
+ moved = false;
5094
+ for (const [from, to] of spans) {
5095
+ if (at > from && at < to) {
5096
+ at = towards < 0 ? from : to;
5097
+ moved = true;
5098
+ }
5099
+ }
5100
+ }
5101
+ return at;
5102
+ }
5103
+
5104
+ /** Whether two rounded rectangles are one shape. */
5105
+ function sameRounded(a: Rounded, b: Rounded): boolean {
5106
+ return (
5107
+ a.rect.x === b.rect.x &&
5108
+ a.rect.y === b.rect.y &&
5109
+ a.rect.width === b.rect.width &&
5110
+ a.rect.height === b.rect.height &&
5111
+ a.corners.x.every((v, i) => v === b.corners.x[i]) &&
5112
+ a.corners.y.every((v, i) => v === b.corners.y[i])
5113
+ );
5114
+ }
5115
+
4967
5116
  /**
4968
5117
  * Clip to what a box's border paints, for a layer of `background-clip:
4969
5118
  * border-area` (CSS Backgrounds 4, 2.1): each side's width and style, and
@@ -4971,12 +5120,17 @@ function roundedRing(
4971
5120
  * the value is for. The shapes are the border painter's own, so that the
4972
5121
  * background shows exactly where that border would: the ring `roundedRing`
4973
5122
  * fills where a rounded border is solid, the ring the trapezoids of a 3D
4974
- * style make, and otherwise the rectangles `fillEdge` fills a side at a
4975
- * time — the dots, the dashes, the two lines of a double border. All of
5123
+ * style make, and otherwise the rectangles `fillSide` fills a side at a
5124
+ * time — the dots, the dashes, the two lines of a double border joined as
5125
+ * they are painted, by the sides' colours though neither is drawn. All of
4976
5126
  * them run clockwise and a ring's inside the other way, so the clip is the
4977
- * union by the non-zero rule, the one every context's `clip` takes. False
4978
- * where the border paints nothing or the context cannot clip, and nothing
4979
- * is pushed.
5127
+ * union by the non-zero rule, the one every context's `clip` takes. Each
5128
+ * is placed from the box's edges and then cut to the painted area: placed
5129
+ * from the cut, a border wider than `CLAMP_PAD` put its bands and its
5130
+ * ring's hole a border's width in from wherever the paint was cut, and
5131
+ * the background was painted over the content of a box repainted in part.
5132
+ * False where the border paints nothing in the painted area or the context
5133
+ * cannot clip, and nothing is pushed.
4980
5134
  */
4981
5135
  function pushBorderArea(
4982
5136
  ctx: PaintContext,
@@ -4998,43 +5152,60 @@ function pushBorderArea(
4998
5152
  const w = Math.round(left + box.width) - x;
4999
5153
  const h = Math.round(top + frameHeight(box)) - y;
5000
5154
  if (w <= 0 || h <= 0) return false;
5001
- const area = clampRect(options, x, y, w, h);
5002
- if (!area) return false;
5155
+ if (!clampRect(options, x, y, w, h)) return false;
5003
5156
  const corners = cornersOf(s, w, h);
5004
5157
  ctx.save();
5005
5158
  ctx.beginPath();
5159
+ // how many shapes the path has: none, and the border paints nothing here
5160
+ let shapes = 0;
5006
5161
  if (corners && canCurve(ctx) && solidBorder(box)) {
5007
- roundedRect(ctx, area.x, area.y, area.w, area.h, corners, true);
5008
- const iw = area.w - l - r;
5009
- const ih = area.h - t - b;
5010
- if (iw > 0 && ih > 0) {
5011
- const inner = insetCorners(corners, t, r, b, l);
5012
- roundedRect(ctx, area.x + l, area.y + t, iw, ih, inner, true, true);
5162
+ const ring = cutRing(options, x, y, w, h, corners, t, r, b, l);
5163
+ if (ring) {
5164
+ const add = ({ rect, corners: c }: Rounded, hole: boolean): void =>
5165
+ roundedRect(
5166
+ ctx,
5167
+ rect.x,
5168
+ rect.y,
5169
+ rect.width,
5170
+ rect.height,
5171
+ c,
5172
+ true,
5173
+ hole,
5174
+ );
5175
+ add(ring.outer, false);
5176
+ // the padding edge run the other way, which cuts it out
5177
+ if (ring.inner) add(ring.inner, true);
5178
+ shapes = 1;
5013
5179
  }
5014
- } else if (sculpted(s)) {
5015
- // a band a side, which the trapezoids cover between them
5016
- ctx.rect(area.x, area.y, area.w, t);
5017
- ctx.rect(area.x, area.y + area.h - b, area.w, b);
5018
- ctx.rect(area.x, area.y + t, l, area.h - t - b);
5019
- ctx.rect(area.x + area.w - r, area.y + t, r, area.h - t - b);
5020
5180
  } else {
5021
- const path = { fillRect: ctx.rect.bind(ctx) };
5022
- const edge = (
5023
- ex: number,
5024
- ey: number,
5025
- ew: number,
5026
- eh: number,
5027
- style: ComputedStyle['borderTopStyle'],
5028
- horizontal: boolean,
5029
- ): void => {
5030
- const rect = clampRect(options, ex, ey, ew, eh);
5031
- if (rect) fillEdge(path, rect, horizontal ? ex : ey, style, horizontal);
5181
+ // A side at a time, from the box's edges and cut to the painted area
5182
+ // after: a 3D side as a band, which the trapezoids cover between them,
5183
+ // and any other as `fillSide` fills it.
5184
+ const path = {
5185
+ fillRect(ex: number, ey: number, ew: number, eh: number) {
5186
+ ctx.rect!(ex, ey, ew, eh);
5187
+ shapes += 1;
5188
+ },
5032
5189
  };
5033
- const between = h - t - b;
5034
- if (t > 0) edge(x, y, w, t, s.borderTopStyle, true);
5035
- if (b > 0) edge(x, y + h - b, w, b, s.borderBottomStyle, true);
5036
- if (l > 0) edge(x, y + t, l, between, s.borderLeftStyle, false);
5037
- if (r > 0) edge(x + w - r, y + t, r, between, s.borderRightStyle, false);
5190
+ const widths = [t, r, b, l];
5191
+ const joins = doubleJoins(box);
5192
+ const styles: ComputedStyle['borderTopStyle'][] = sculpted(s)
5193
+ ? ['solid', 'solid', 'solid', 'solid']
5194
+ : [
5195
+ s.borderTopStyle,
5196
+ s.borderRightStyle,
5197
+ s.borderBottomStyle,
5198
+ s.borderLeftStyle,
5199
+ ];
5200
+ for (let at = 0; at < 4; at += 1) {
5201
+ if (widths[at] > 0) {
5202
+ fillSide(path, options, at, x, y, w, h, widths, styles[at], joins);
5203
+ }
5204
+ }
5205
+ }
5206
+ if (!shapes) {
5207
+ ctx.restore();
5208
+ return false;
5038
5209
  }
5039
5210
  ctx.clip();
5040
5211
  return true;
@@ -5071,10 +5242,12 @@ function sculpted(s: ComputedStyle): boolean {
5071
5242
  * colour's shadow and `outset` its bottom and right; `groove` is `inset`
5072
5243
  * outside `outset`, a band each, and `ridge` the other way round
5073
5244
  * (`borderShades`). A side of any other style takes its trapezoid in its
5074
- * colour. The trapezoids are clamped to the painted area, which leaves a
5075
- * cut end square and out of sight and keeps a long box's far corners out
5076
- * of X's coordinates. False where the context cannot draw a path, and the
5077
- * sides are drawn straight.
5245
+ * colour. The trapezoids are cut to the painted area (`fillPolygon`), which
5246
+ * keeps a long box's far corners out of X's coordinates and a join on its
5247
+ * diagonal: its corners clamped in, a border wider than `CLAMP_PAD` turned
5248
+ * the join to cross the damage at 45° wherever it was, and a scrolled
5249
+ * strip smeared the corners of a thick one. False where the context cannot
5250
+ * draw a path, and the sides are drawn straight.
5078
5251
  */
5079
5252
  function paintSculpted(
5080
5253
  ctx: PaintContext,
@@ -5093,10 +5266,6 @@ function paintSculpted(
5093
5266
  const r = box.borderRight;
5094
5267
  const b = box.borderBottom;
5095
5268
  const l = box.borderLeft;
5096
- const cx = (v: number) =>
5097
- Math.max(area.x - 1, Math.min(v, area.x + area.w + 1));
5098
- const cy = (v: number) =>
5099
- Math.max(area.y - 1, Math.min(v, area.y + area.h + 1));
5100
5269
  /** The band between the edge `outer` of the way in and `inner`, for one
5101
5270
  * side: 0 is the border box, 1 the padding box. */
5102
5271
  const band = (side: number, outer: number, inner: number, color: string) => {
@@ -5117,12 +5286,7 @@ function paintSculpted(
5117
5286
  ? [o.x1, o.y1, o.x0, o.y1, i.x0, i.y1, i.x1, i.y1]
5118
5287
  : [o.x0, o.y1, o.x0, o.y0, i.x0, i.y0, i.x0, i.y1];
5119
5288
  ctx.fillStyle = color;
5120
- ctx.beginPath!();
5121
- ctx.moveTo!(cx(points[0]), cy(points[1]));
5122
- for (let k = 2; k < 8; k += 2)
5123
- ctx.lineTo!(cx(points[k]), cy(points[k + 1]));
5124
- ctx.closePath?.();
5125
- ctx.fill!();
5289
+ fillPolygon(ctx, points, area);
5126
5290
  };
5127
5291
  const sides: [number, string, string][] = [
5128
5292
  [t, s.borderTopStyle, s.borderTopColor],
@@ -5249,6 +5413,111 @@ function paintCollapsedBorders(
5249
5413
  }
5250
5414
  }
5251
5415
 
5416
+ /**
5417
+ * One side of a border box at `x, y`, `w` by `h` — `at` counts the top
5418
+ * first and clockwise — as rectangles cut to what the paint reaches: the
5419
+ * top and the bottom the width of the box and the left and the right
5420
+ * between them, so a corner is a square of one side or the other.
5421
+ *
5422
+ * A double side is two lines, a band each along its outer and its inner
5423
+ * edge. Where it meets another double side of its colour (`joins`, the
5424
+ * top left corner first) the lines of the two meet as two frames do, one
5425
+ * inside the other (CSS Backgrounds 3, 4.2 and 4.3): the outer line of the
5426
+ * left or the right runs on to the outer line of the top or the bottom,
5427
+ * and the inner line of the top or the bottom stops at the inner line of
5428
+ * the side. With square corners the outer frame was open at each of them
5429
+ * and the inner line of the top ran on past the side's to the border edge.
5430
+ * Each line is cut to the paint on its own, so a border wider than the
5431
+ * reach around the paint keeps its lines at its edges, and not at the
5432
+ * edges of the part of it cut out.
5433
+ */
5434
+ function fillSide(
5435
+ ctx: Pick<FillContext, 'fillRect'>,
5436
+ options: PaintOptions,
5437
+ at: number,
5438
+ x: number,
5439
+ y: number,
5440
+ w: number,
5441
+ h: number,
5442
+ widths: readonly number[],
5443
+ style: ComputedStyle['borderTopStyle'],
5444
+ joins: readonly boolean[],
5445
+ ): void {
5446
+ const [t, r, b, l] = widths;
5447
+ const width = widths[at];
5448
+ const horizontal = (at & 1) === 0;
5449
+ if (style === 'double' && width >= 3) {
5450
+ const band = doubleBand(width);
5451
+ const line = (lx: number, ly: number, lw: number, lh: number): void => {
5452
+ const rect = clampRect(options, lx, ly, lw, lh);
5453
+ if (rect) ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
5454
+ };
5455
+ if (horizontal) {
5456
+ // the outer line the width of the box, and the inner one from the
5457
+ // inner line of a side it joins, or from the edge of the box
5458
+ const from = joins[at === 0 ? 0 : 3] ? x + l - doubleBand(l) : x;
5459
+ const to = joins[at === 0 ? 1 : 2] ? x + w - r + doubleBand(r) : x + w;
5460
+ line(x, at === 0 ? y : y + h - band, w, band);
5461
+ line(from, at === 0 ? y + t - band : y + h - b, to - from, band);
5462
+ } else {
5463
+ // the outer line from the outer line of a side it joins, or from
5464
+ // that side's inner edge, and the inner one between the two sides
5465
+ const from = joins[at === 3 ? 0 : 1] ? y + doubleBand(t) : y + t;
5466
+ const to = joins[at === 3 ? 3 : 2] ? y + h - doubleBand(b) : y + h - b;
5467
+ line(at === 3 ? x : x + w - band, from, band, to - from);
5468
+ line(at === 3 ? x + l - band : x + w - r, y + t, band, h - t - b);
5469
+ }
5470
+ return;
5471
+ }
5472
+ const ex = at === 1 ? x + w - r : x;
5473
+ 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);
5480
+ }
5481
+
5482
+ /**
5483
+ * The corners of a box where two double sides of one colour meet, the top
5484
+ * left first and clockwise, and whose lines `fillSide` joins. By the
5485
+ * colours as written, a transparent one included, which is how a clip to
5486
+ * the border's area draws the border it does not paint.
5487
+ */
5488
+ function doubleJoins(box: Frame): readonly boolean[] {
5489
+ const s = box.style;
5490
+ const color = (
5491
+ width: number,
5492
+ style: ComputedStyle['borderTopStyle'],
5493
+ value: string,
5494
+ ): string | null =>
5495
+ style === 'double' && width >= 3 ? inkColor(value, s.color) : null;
5496
+ const top = color(box.borderTop, s.borderTopStyle, s.borderTopColor);
5497
+ const bottom = color(
5498
+ box.borderBottom,
5499
+ s.borderBottomStyle,
5500
+ s.borderBottomColor,
5501
+ );
5502
+ if (top === null && bottom === null) return UNJOINED;
5503
+ const right = color(box.borderRight, s.borderRightStyle, s.borderRightColor);
5504
+ const left = color(box.borderLeft, s.borderLeftStyle, s.borderLeftColor);
5505
+ return [
5506
+ top !== null && top === left,
5507
+ top !== null && top === right,
5508
+ bottom !== null && bottom === right,
5509
+ bottom !== null && bottom === left,
5510
+ ];
5511
+ }
5512
+
5513
+ const UNJOINED: readonly boolean[] = [false, false, false, false];
5514
+
5515
+ /** How wide each of a double border's two lines is, the gap between them
5516
+ * what is left of the side. */
5517
+ function doubleBand(thickness: number): number {
5518
+ return Math.max(1, Math.floor(thickness / 3));
5519
+ }
5520
+
5252
5521
  function fillEdge(
5253
5522
  ctx: Pick<FillContext, 'fillRect'>,
5254
5523
  rect: { x: number; y: number; w: number; h: number },
@@ -5277,7 +5546,7 @@ function fillEdge(
5277
5546
  return;
5278
5547
  }
5279
5548
  if (style === 'double' && thickness >= 3) {
5280
- const band = Math.max(1, Math.floor(thickness / 3));
5549
+ const band = doubleBand(thickness);
5281
5550
  if (horizontal) {
5282
5551
  ctx.fillRect(x, y, length, band);
5283
5552
  ctx.fillRect(x, y + thickness - band, length, band);
@@ -5295,6 +5564,8 @@ function paintMarker(
5295
5564
  ctx: PaintContext,
5296
5565
  marker: Marker,
5297
5566
  options: PaintOptions,
5567
+ /** The item's colour scheme, which an SVG image in it is drawn in. */
5568
+ scheme: 'light' | 'dark',
5298
5569
  ): void {
5299
5570
  const x = marker.x + options.originX;
5300
5571
  const y = marker.y + options.originY;
@@ -5304,7 +5575,15 @@ function paintMarker(
5304
5575
  const loaded = options.backgroundImageFor?.(url);
5305
5576
  if (!loaded || !(width > 0 && height > 0)) return;
5306
5577
  if (loaded.image instanceof SvgDrawing) {
5307
- loaded.image.draw(ctx, x, y, width, height, options.scale ?? 1);
5578
+ loaded.image.drawImage(
5579
+ ctx,
5580
+ x,
5581
+ y,
5582
+ width,
5583
+ height,
5584
+ options.scale ?? 1,
5585
+ scheme,
5586
+ );
5308
5587
  } else ctx.drawImage?.(loaded.image, x, y, width, height);
5309
5588
  return;
5310
5589
  }
@@ -5336,7 +5615,15 @@ function paintImage(ctx: PaintContext, box: Box, options: PaintOptions): void {
5336
5615
  const clipped =
5337
5616
  (!!corners || past) && pushClip(ctx, { x, y, w, h }, corners);
5338
5617
  if (image instanceof SvgDrawing) {
5339
- image.draw(ctx, at.x, at.y, at.w, at.h, options.scale ?? 1);
5618
+ image.drawImage(
5619
+ ctx,
5620
+ at.x,
5621
+ at.y,
5622
+ at.w,
5623
+ at.h,
5624
+ options.scale ?? 1,
5625
+ box.style.colorScheme,
5626
+ );
5340
5627
  } else ctx.drawImage!(image, at.x, at.y, at.w, at.h);
5341
5628
  if (clipped) ctx.restore();
5342
5629
  return;
@@ -5427,7 +5714,8 @@ function fitted(
5427
5714
  /** An inline `<svg>`, drawn in its content box. Its `currentColor` is the
5428
5715
  * box's `color`, as an icon's is the text's around it, and what it is
5429
5716
  * painted with the box's `fill` and `stroke`, where the document's style
5430
- * sheets set them: an icon set's `.icon { fill: currentColor }`. */
5717
+ * sheets set them: an icon set's `.icon { fill: currentColor }`. What
5718
+ * they give a shape inside it is asked for here (`BoxTree.shapeStyler`). */
5431
5719
  function paintSvg(ctx: PaintContext, box: Box, options: PaintOptions): void {
5432
5720
  if (!box.el) return;
5433
5721
  const x = Math.round(box.contentX + options.originX);
@@ -5449,6 +5737,7 @@ function paintSvg(ctx: PaintContext, box: Box, options: PaintOptions): void {
5449
5737
  style.color,
5450
5738
  paint(style.fill),
5451
5739
  paint(style.stroke),
5740
+ options.shapeStyler?.(box) ?? null,
5452
5741
  );
5453
5742
  }
5454
5743