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