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