@react-x11/components 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/html/controls.d.ts +29 -2
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +70 -2
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +42 -1
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +336 -16
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +18 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +27 -0
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/parse.d.ts +18 -4
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +66 -28
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/shapes.d.ts +44 -0
- package/dist/html/css/shapes.d.ts.map +1 -0
- package/dist/html/css/shapes.js +194 -0
- package/dist/html/css/shapes.js.map +1 -0
- package/dist/html/css/style.d.ts +128 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +547 -144
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/transform.d.ts +85 -0
- package/dist/html/css/transform.d.ts.map +1 -0
- package/dist/html/css/transform.js +425 -0
- package/dist/html/css/transform.js.map +1 -0
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +14 -2
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +4 -0
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/fonts.d.ts +55 -5
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +284 -23
- package/dist/html/fonts.js.map +1 -1
- package/dist/html/layout/axes.d.ts +14 -0
- package/dist/html/layout/axes.d.ts.map +1 -0
- package/dist/html/layout/axes.js +28 -0
- package/dist/html/layout/axes.js.map +1 -0
- package/dist/html/layout/block.d.ts +104 -12
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +410 -39
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +72 -1
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +40 -2
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +1 -1
- package/dist/html/layout/cache.d.ts.map +1 -1
- package/dist/html/layout/cache.js +6 -0
- package/dist/html/layout/cache.js.map +1 -1
- package/dist/html/layout/css-grid.d.ts.map +1 -1
- package/dist/html/layout/css-grid.js +144 -24
- 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 +585 -85
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/inline.d.ts +3 -0
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +144 -54
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/multicol.d.ts +42 -0
- package/dist/html/layout/multicol.d.ts.map +1 -0
- package/dist/html/layout/multicol.js +897 -0
- package/dist/html/layout/multicol.js.map +1 -0
- package/dist/html/layout/shaping.d.ts +7 -0
- package/dist/html/layout/shaping.d.ts.map +1 -1
- package/dist/html/layout/shaping.js +25 -20
- package/dist/html/layout/shaping.js.map +1 -1
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +97 -31
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/node.d.ts +4 -0
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +319 -47
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +83 -5
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +1474 -217
- package/dist/html/paint.js.map +1 -1
- package/dist/html/svg.d.ts +39 -9
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +482 -37
- package/dist/html/svg.js.map +1 -1
- package/dist/html/widgets.js +87 -7
- package/dist/html/widgets.js.map +1 -1
- package/dist/html/woff2.d.ts +14 -0
- package/dist/html/woff2.d.ts.map +1 -0
- package/dist/html/woff2.js +608 -0
- package/dist/html/woff2.js.map +1 -0
- package/dist/maps/controller.d.ts +17 -1
- package/dist/maps/controller.d.ts.map +1 -1
- package/dist/maps/controller.js +27 -7
- package/dist/maps/controller.js.map +1 -1
- package/dist/richtext/node.d.ts +8 -1
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +5 -1
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +25 -6
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +240 -24
- package/dist/richtext/runs.js.map +1 -1
- package/package.json +1 -1
- package/src/html/controls.ts +85 -2
- package/src/html/css/cascade.ts +366 -8
- package/src/html/css/color.ts +29 -0
- package/src/html/css/parse.ts +71 -31
- package/src/html/css/shapes.ts +239 -0
- package/src/html/css/style.ts +652 -147
- package/src/html/css/transform.ts +459 -0
- package/src/html/css/ua.ts +15 -2
- package/src/html/css/values.ts +4 -0
- package/src/html/fonts.ts +312 -29
- package/src/html/layout/axes.ts +62 -0
- package/src/html/layout/block.ts +466 -37
- package/src/html/layout/boxes.ts +95 -3
- package/src/html/layout/cache.ts +6 -0
- package/src/html/layout/css-grid.ts +169 -32
- package/src/html/layout/flex.ts +686 -79
- package/src/html/layout/inline.ts +149 -55
- package/src/html/layout/multicol.ts +977 -0
- package/src/html/layout/shaping.ts +27 -18
- package/src/html/layout/table.ts +105 -35
- package/src/html/node.ts +354 -48
- package/src/html/paint.ts +1804 -315
- package/src/html/svg.ts +573 -36
- package/src/html/widgets.ts +95 -9
- package/src/html/woff2.ts +612 -0
- package/src/maps/controller.ts +28 -7
- package/src/richtext/node.ts +13 -2
- package/src/richtext/runs.ts +313 -35
package/dist/html/paint.js
CHANGED
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
// back on the laid-out lines is exactly what that module already knows how to
|
|
21
21
|
// draw.
|
|
22
22
|
import { canFill, lineBands, paintRunBackgrounds, paintRunRules, } from '../richtext/runs.js';
|
|
23
|
-
import { alphaOf, inkColor, isTransparent, resolve } from './css/values.js';
|
|
24
|
-
import { blend, borderShades } from './css/color.js';
|
|
23
|
+
import { AUTO, alphaOf, inkColor, isTransparent, resolve, } from './css/values.js';
|
|
24
|
+
import { SCHEME_COLORS, blend, borderShades } from './css/color.js';
|
|
25
25
|
import { CONTAIN_LAYOUT, CONTAIN_PAINT, CONTAIN_SIZE, copyStyle, masked, scrolls, } from './css/style.js';
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
26
|
+
import { LARGEST } from './css/calc.js';
|
|
27
|
+
import { invert, mapRect, transformed } from './css/transform.js';
|
|
28
|
+
import { contained, placedMatrix } from './layout/block.js';
|
|
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';
|
|
28
30
|
import { CLIPPED_TEXT, clipBoxOf, depthOf, inklessLayout, layoutOffsetOf, layoutOffsets, } from './layout/inline.js';
|
|
29
31
|
import { tableGrid } from './layout/grid.js';
|
|
30
32
|
import { SvgDrawing, inlineDrawing } from './svg.js';
|
|
@@ -52,6 +54,24 @@ export const OUT_OF_FLOW_REACH = new WeakMap();
|
|
|
52
54
|
* is inside them.
|
|
53
55
|
*/
|
|
54
56
|
const LINE_INK = new WeakMap();
|
|
57
|
+
/**
|
|
58
|
+
* For a box painted through a matrix (`placedMatrix`), the bounds of what
|
|
59
|
+
* it and its descendants draw before the matrix is applied: what
|
|
60
|
+
* `boundsX`… would have been, which are where it puts them. The paint
|
|
61
|
+
* inside the box, and a point taken back into it, are in these.
|
|
62
|
+
*/
|
|
63
|
+
const UNTRANSFORMED = new WeakMap();
|
|
64
|
+
/** The bounds of what a box painted through a matrix draws, in its own
|
|
65
|
+
* coordinates: asked of a box `placedMatrix` answers for, whose entry the
|
|
66
|
+
* last `computePaintBounds` wrote. */
|
|
67
|
+
export function ownBounds(box) {
|
|
68
|
+
return (UNTRANSFORMED.get(box) ?? {
|
|
69
|
+
x: box.boundsX,
|
|
70
|
+
y: box.boundsY,
|
|
71
|
+
width: box.boundsWidth,
|
|
72
|
+
height: box.boundsHeight,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
55
75
|
export function computePaintBounds(box, moved = false) {
|
|
56
76
|
// A box with no rectangle of its own gives only what it holds: nothing,
|
|
57
77
|
// until a child with bounds is met.
|
|
@@ -231,6 +251,40 @@ export function computePaintBounds(box, moved = false) {
|
|
|
231
251
|
LINE_INK.set(lines, { above, below });
|
|
232
252
|
else if (lines)
|
|
233
253
|
LINE_INK.delete(lines);
|
|
254
|
+
// a `clip-path` cuts the box and all it holds: no ink past the path, and
|
|
255
|
+
// none where it leaves nothing — though the scrollable overflow is what
|
|
256
|
+
// it was, as a browser has it, which is why a page that hides a box with
|
|
257
|
+
// one makes it a pixel square as well
|
|
258
|
+
if (own && box.style.clipPath) {
|
|
259
|
+
const { rect } = clipPathOf(box, DOCUMENT);
|
|
260
|
+
x1 = Math.max(x1, rect.x);
|
|
261
|
+
y1 = Math.max(y1, rect.y);
|
|
262
|
+
x2 = Math.min(x2, rect.x + rect.w);
|
|
263
|
+
y2 = Math.min(y2, rect.y + rect.h);
|
|
264
|
+
if (!(x2 > x1 && y2 > y1))
|
|
265
|
+
x1 = Infinity;
|
|
266
|
+
}
|
|
267
|
+
// A box that turns, scales or skews draws what it holds through its
|
|
268
|
+
// matrix (`paintTransformed`): its ink is where that puts it, the
|
|
269
|
+
// rectangle around the one it would have filled, and the one it would
|
|
270
|
+
// have filled is kept for the paint inside it, which is in its own
|
|
271
|
+
// coordinates. One flattened to nothing draws nothing.
|
|
272
|
+
const matrix = own ? placedMatrix(box) : null;
|
|
273
|
+
if (matrix && x1 !== Infinity) {
|
|
274
|
+
UNTRANSFORMED.set(box, { x: x1, y: y1, width: x2 - x1, height: y2 - y1 });
|
|
275
|
+
if (invert(matrix)) {
|
|
276
|
+
// no further than a length reaches (`LARGEST`): a page's
|
|
277
|
+
// `scale(1e30)` is a box as large as a browser holds one, and no
|
|
278
|
+
// larger
|
|
279
|
+
const to = mapRect(matrix, x1, y1, x2 - x1, y2 - y1);
|
|
280
|
+
x1 = Math.max(to.x, -LARGEST);
|
|
281
|
+
y1 = Math.max(to.y, -LARGEST);
|
|
282
|
+
x2 = Math.min(to.x + to.width, LARGEST);
|
|
283
|
+
y2 = Math.min(to.y + to.height, LARGEST);
|
|
284
|
+
}
|
|
285
|
+
else
|
|
286
|
+
x1 = Infinity;
|
|
287
|
+
}
|
|
234
288
|
if (x1 === Infinity) {
|
|
235
289
|
// nothing to draw: no damage meets it, and no parent takes it in
|
|
236
290
|
box.boundsX = Infinity;
|
|
@@ -255,13 +309,21 @@ export function computePaintBounds(box, moved = false) {
|
|
|
255
309
|
// whether the box clips only matters where its content reaches past it,
|
|
256
310
|
// and its style is one more object a walk of every box would read
|
|
257
311
|
const end = empty ? -Infinity : box.y + box.height;
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
312
|
+
let reach = end;
|
|
313
|
+
if (bottom > end) {
|
|
314
|
+
const style = box.style;
|
|
315
|
+
reach =
|
|
316
|
+
box.parent &&
|
|
317
|
+
(style.overflowX !== 'visible' || style.overflowY !== 'visible')
|
|
318
|
+
? end
|
|
319
|
+
: bottom;
|
|
320
|
+
}
|
|
321
|
+
// the scrollable overflow of a transformed box is where its transform
|
|
322
|
+
// puts it (CSS Overflow 3, 2.2)
|
|
323
|
+
if (!matrix || reach === -Infinity)
|
|
324
|
+
return reach;
|
|
325
|
+
const to = mapRect(matrix, box.x, box.y, box.width, reach - box.y);
|
|
326
|
+
return Math.min(to.y + to.height, LARGEST);
|
|
265
327
|
}
|
|
266
328
|
/**
|
|
267
329
|
* Whether a box has a rectangle of its own to draw in. An inline box has
|
|
@@ -411,6 +473,20 @@ export function paintDocument(ctx, tree, options) {
|
|
|
411
473
|
return;
|
|
412
474
|
ctx.save();
|
|
413
475
|
const canvas = canvasBackground(tree);
|
|
476
|
+
const scheme = rootScheme(tree);
|
|
477
|
+
if (scheme !== tree.paletteScheme) {
|
|
478
|
+
// A document is drawn on the window's own ground, the palette's, as an
|
|
479
|
+
// embedded one is on a transparent canvas. Where its root's colour
|
|
480
|
+
// scheme is not the scheme of what it is embedded in, its canvas is
|
|
481
|
+
// opaque, in the `Canvas` colour of its own (CSS Color Adjust 1, 2.2):
|
|
482
|
+
// a page that says it is light, and sets its text dark on no
|
|
483
|
+
// background, is not read against a dark window.
|
|
484
|
+
const area = canvasArea(tree.root, options);
|
|
485
|
+
if (area) {
|
|
486
|
+
ctx.fillStyle = SCHEME_COLORS[scheme].canvas;
|
|
487
|
+
ctx.fillRect(area.x, area.y, area.w, area.h);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
414
490
|
if (canvas)
|
|
415
491
|
paintCanvas(ctx, canvas, tree.root, options);
|
|
416
492
|
paintBox(ctx, tree.root, {
|
|
@@ -418,6 +494,7 @@ export function paintDocument(ctx, tree, options) {
|
|
|
418
494
|
canvasSource: canvas?.source,
|
|
419
495
|
negative: tree.negative,
|
|
420
496
|
selectionStyler: options.selection ? tree.selectionStyler : null,
|
|
497
|
+
shapeStyler: tree.shapeStyler,
|
|
421
498
|
});
|
|
422
499
|
ctx.restore();
|
|
423
500
|
}
|
|
@@ -463,6 +540,24 @@ function canvasBackground(tree) {
|
|
|
463
540
|
? { style: body.style, source: body, anchor: top }
|
|
464
541
|
: null;
|
|
465
542
|
}
|
|
543
|
+
/** The used colour scheme of the document's root element: the `<html>`'s,
|
|
544
|
+
* or that of whichever box stands in for it (`canvasBackground`). */
|
|
545
|
+
function rootScheme(tree) {
|
|
546
|
+
if (tree.impliedHtml)
|
|
547
|
+
return tree.impliedHtml.colorScheme;
|
|
548
|
+
return (childNamed(tree.root, 'html') ?? tree.root).style.colorScheme;
|
|
549
|
+
}
|
|
550
|
+
/** The canvas, cut to what is being painted: the whole element where the
|
|
551
|
+
* host says what that is, and else the root box. */
|
|
552
|
+
function canvasArea(root, options) {
|
|
553
|
+
const whole = options.canvas ?? {
|
|
554
|
+
x: root.x + options.originX,
|
|
555
|
+
y: root.y + options.originY,
|
|
556
|
+
width: root.width,
|
|
557
|
+
height: root.height,
|
|
558
|
+
};
|
|
559
|
+
return clampRect(options, Math.round(whole.x), Math.round(whole.y), Math.ceil(whole.width), Math.ceil(whole.height));
|
|
560
|
+
}
|
|
466
561
|
/** A box's child element of that name, or one inside the anonymous boxes
|
|
467
562
|
* it is wrapped in: a `<body>` in an `<html>` set `display: table` is in
|
|
468
563
|
* an anonymous row and cell. */
|
|
@@ -479,13 +574,7 @@ function childNamed(box, name) {
|
|
|
479
574
|
return null;
|
|
480
575
|
}
|
|
481
576
|
function paintCanvas(ctx, { style: source, anchor }, root, options) {
|
|
482
|
-
const
|
|
483
|
-
x: root.x + options.originX,
|
|
484
|
-
y: root.y + options.originY,
|
|
485
|
-
width: root.width,
|
|
486
|
-
height: root.height,
|
|
487
|
-
};
|
|
488
|
-
const area = clampRect(options, Math.round(whole.x), Math.round(whole.y), Math.ceil(whole.width), Math.ceil(whole.height));
|
|
577
|
+
const area = canvasArea(root, options);
|
|
489
578
|
if (!area)
|
|
490
579
|
return;
|
|
491
580
|
const layers = layersOf(source) ?? [source];
|
|
@@ -496,7 +585,7 @@ function paintCanvas(ctx, { style: source, anchor }, root, options) {
|
|
|
496
585
|
ctx.fillStyle = inkColor(style.backgroundColor, style.color);
|
|
497
586
|
ctx.fillRect(area.x, area.y, area.w, area.h);
|
|
498
587
|
}
|
|
499
|
-
if (style.backgroundGradient &&
|
|
588
|
+
if (style.backgroundGradient && visible) {
|
|
500
589
|
// sized by the root element's box and repeated down the canvas, as a
|
|
501
590
|
// browser does — the stripes a short page with a gradient on its
|
|
502
591
|
// body shows — or by the viewport, where it is fixed
|
|
@@ -664,28 +753,289 @@ function paintBox(ctx, box, options) {
|
|
|
664
753
|
return;
|
|
665
754
|
}
|
|
666
755
|
// `clip` shows the part of an absolutely positioned box it names, its own
|
|
667
|
-
// background and borders among it (CSS 2.1 11.1.2)
|
|
668
|
-
//
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
756
|
+
// background and borders among it (CSS 2.1 11.1.2), and `clip-path` the
|
|
757
|
+
// part of any box (`pushOwnClips`) — the content painted here, not in a
|
|
758
|
+
// call of its own: a frame more a level and a deep document ran out of
|
|
759
|
+
// stack
|
|
760
|
+
const pushed = pushOwnClips(ctx, box, options);
|
|
761
|
+
if (pushed >= 0) {
|
|
762
|
+
paintContent(ctx, box, pathClips(box) ? underPath(options) : options);
|
|
763
|
+
for (let i = 0; i < pushed; i += 1)
|
|
674
764
|
ctx.restore();
|
|
675
765
|
}
|
|
676
766
|
if (fade)
|
|
677
767
|
ctx.restore();
|
|
678
768
|
}
|
|
679
|
-
/**
|
|
769
|
+
/** How large a surface a transformed box is drawn on: a large window's
|
|
770
|
+
* worth of pixels twice over, and no side longer than a pixmap's may be. */
|
|
771
|
+
const RASTER_LIMIT = 16 * 1024 * 1024;
|
|
772
|
+
const RASTER_SIDE = 16384;
|
|
773
|
+
/** What XRender carries a picture's transform in is 16.16 fixed point: a
|
|
774
|
+
* number past this does not fit, and is thrown out of the request's
|
|
775
|
+
* encoder. */
|
|
776
|
+
const FIXED_LIMIT = 32000;
|
|
777
|
+
/**
|
|
778
|
+
* A box that turns, scales or skews (CSS Transforms 1): painted as it was
|
|
779
|
+
* laid out, through its matrix — about its `transform-origin`, where its
|
|
780
|
+
* translation has already put it (`placedMatrix`). What it holds is
|
|
781
|
+
* painted with it, as the stacking context it is (`stacksLayers`), culled
|
|
782
|
+
* against the damage taken back through the matrix.
|
|
783
|
+
*
|
|
784
|
+
* The native contexts draw everything through a matrix, a text layout's
|
|
785
|
+
* outlines among it, glyphs and all (`scalesText`). ntk's draws paths
|
|
786
|
+
* through one; a glyph it draws as it was shaped, upright and at its size,
|
|
787
|
+
* where the matrix puts its origin, and an image through a transform the
|
|
788
|
+
* server holds in fixed point, which a picture drawn small far across a
|
|
789
|
+
* window does not fit. So there only a box that is paths and flat colour —
|
|
790
|
+
* an icon, a chevron, a spinner — is drawn through the matrix, and any
|
|
791
|
+
* other on a surface of its own, as it was laid out, with the surface
|
|
792
|
+
* drawn through the matrix (`paintRaster`): a picture of its text rather
|
|
793
|
+
* than the text, resampled, and right about where it is and which way up.
|
|
794
|
+
*
|
|
795
|
+
* A context with no `transform` draws the box where it was laid out: the
|
|
796
|
+
* headless mock, which draws nothing, and a recording one.
|
|
797
|
+
*/
|
|
798
|
+
function paintTransformed(ctx, box, options) {
|
|
799
|
+
const matrix = placedMatrix(box);
|
|
800
|
+
if (!matrix) {
|
|
801
|
+
paintBox(ctx, box, options);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (!intersects(box, options) ||
|
|
805
|
+
COLLAPSED_CELLS.has(box) ||
|
|
806
|
+
CLAMPED.has(box)) {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
if (typeof ctx.transform !== 'function') {
|
|
810
|
+
paintPlaced(ctx, box, options);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
// In the window's coordinates, where the document's origin is the
|
|
814
|
+
// options': T(origin) · matrix · T(-origin). And about an origin moved
|
|
815
|
+
// with the box's corner to the pixel it is drawn from: what is painted is
|
|
816
|
+
// snapped to the pixel grid a box at a time, so a box a fraction of a
|
|
817
|
+
// pixel down the page is drawn from a whole one, and turned about where
|
|
818
|
+
// it was laid out it came back off the grid — a square mirrored onto
|
|
819
|
+
// itself showed a line of what was under it along each side. A browser
|
|
820
|
+
// snaps a transformed box's corner before its transform the same way.
|
|
821
|
+
const left = box.x + options.originX;
|
|
822
|
+
const top = box.y + options.originY;
|
|
823
|
+
const ox = options.originX + Math.round(left) - left;
|
|
824
|
+
const oy = options.originY + Math.round(top) - top;
|
|
825
|
+
const [a, b, c, d] = matrix;
|
|
826
|
+
const through = [
|
|
827
|
+
a,
|
|
828
|
+
b,
|
|
829
|
+
c,
|
|
830
|
+
d,
|
|
831
|
+
matrix[4] + ox - (a * ox + c * oy),
|
|
832
|
+
matrix[5] + oy - (b * ox + d * oy),
|
|
833
|
+
];
|
|
834
|
+
const back = invert(through);
|
|
835
|
+
if (!back)
|
|
836
|
+
return;
|
|
837
|
+
const damage = options.damage;
|
|
838
|
+
const inside = {
|
|
839
|
+
...options,
|
|
840
|
+
damage: damage && mapRect(back, damage.x, damage.y, damage.width, damage.height),
|
|
841
|
+
};
|
|
842
|
+
if (ctx.scalesText !== true &&
|
|
843
|
+
!drawnAsPaths(box) &&
|
|
844
|
+
paintRaster(ctx, box, inside, through)) {
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
ctx.save();
|
|
848
|
+
ctx.transform(a, b, c, d, through[4], through[5]);
|
|
849
|
+
paintPlaced(ctx, box, inside);
|
|
850
|
+
ctx.restore();
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Whether a box's style turns, scales or skews it, which is asked before
|
|
854
|
+
* every box that may be one is painted (`paintPositioned`, `paintLines`) —
|
|
855
|
+
* there, and not in `paintBox`, whose frame is one of three a level of
|
|
856
|
+
* nesting costs the stack: a name more in it, and a document a thousand
|
|
857
|
+
* boxes deep ran out.
|
|
858
|
+
*/
|
|
859
|
+
function turns(box) {
|
|
860
|
+
const style = box.style;
|
|
861
|
+
return (style.transform !== null || style.rotate !== null || style.scale !== null);
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* `paintBox` for a box whose ink bounds are not in the coordinates it is
|
|
865
|
+
* being painted in, and so is not culled by them: one painted through a
|
|
866
|
+
* matrix, in its own (`paintTransformed`).
|
|
867
|
+
*/
|
|
868
|
+
function paintPlaced(ctx, box, options) {
|
|
869
|
+
const opacity = opacityOf(box);
|
|
870
|
+
if (opacity <= 0)
|
|
871
|
+
return;
|
|
872
|
+
const fade = opacity < 1 && typeof ctx.globalAlpha === 'number';
|
|
873
|
+
if (fade) {
|
|
874
|
+
ctx.save();
|
|
875
|
+
ctx.globalAlpha = ctx.globalAlpha * opacity;
|
|
876
|
+
}
|
|
877
|
+
if (!masked(box.style) || !paintMasked(ctx, box, options)) {
|
|
878
|
+
paintClipped(ctx, box, options);
|
|
879
|
+
}
|
|
880
|
+
if (fade)
|
|
881
|
+
ctx.restore();
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Whether all a box and what it holds draw is paths and flat colour:
|
|
885
|
+
* backgrounds, borders, outlines and inline drawings — no text and no list
|
|
886
|
+
* marker, whose glyphs a context may not turn, and no image, gradient,
|
|
887
|
+
* shadow or mask, which one draws through a picture's transform
|
|
888
|
+
* (`paintTransformed`).
|
|
889
|
+
*/
|
|
890
|
+
function drawnAsPaths(box) {
|
|
891
|
+
if (box.subtreeTextEnd > box.subtreeTextStart)
|
|
892
|
+
return false;
|
|
893
|
+
const stack = [box];
|
|
894
|
+
while (stack.length) {
|
|
895
|
+
const at = stack.pop();
|
|
896
|
+
if (at.marker || at.replaced === 'image')
|
|
897
|
+
return false;
|
|
898
|
+
const style = at.style;
|
|
899
|
+
if (style.backgroundImage !== null ||
|
|
900
|
+
style.backgroundGradient !== null ||
|
|
901
|
+
style.backgroundImages !== null ||
|
|
902
|
+
style.boxShadow !== null ||
|
|
903
|
+
style.borderImage.source !== null ||
|
|
904
|
+
masked(style)) {
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
for (const child of at.children)
|
|
908
|
+
stack.push(child);
|
|
909
|
+
}
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* A transformed box painted on a surface of its own, as it was laid out,
|
|
914
|
+
* and the surface drawn through its matrix: the part of what it draws that
|
|
915
|
+
* the damage reaches. True where that is done — or is nothing to do, or
|
|
916
|
+
* cannot be: a part too large for a surface, or a matrix that does not fit
|
|
917
|
+
* the fixed point a picture's transform is sent in, which is one that draws
|
|
918
|
+
* the box a thirtieth its size far across a window, or flatter than can be
|
|
919
|
+
* seen. None of it is drawn then. False where the context has no surface to
|
|
920
|
+
* draw on, and the caller draws the box through the matrix itself.
|
|
921
|
+
*/
|
|
922
|
+
function paintRaster(ctx, box, options, through) {
|
|
923
|
+
if (!options.surface || !ctx.drawImage)
|
|
924
|
+
return false;
|
|
925
|
+
const own = ownBounds(box);
|
|
926
|
+
let x0 = Math.floor(own.x + options.originX);
|
|
927
|
+
let y0 = Math.floor(own.y + options.originY);
|
|
928
|
+
let x1 = Math.ceil(own.x + own.width + options.originX);
|
|
929
|
+
let y1 = Math.ceil(own.y + own.height + options.originY);
|
|
930
|
+
const damage = options.damage;
|
|
931
|
+
if (damage) {
|
|
932
|
+
// a pixel more each way: the edge of what is drawn is sampled from
|
|
933
|
+
// beside it
|
|
934
|
+
x0 = Math.max(x0, Math.floor(damage.x) - 1);
|
|
935
|
+
y0 = Math.max(y0, Math.floor(damage.y) - 1);
|
|
936
|
+
x1 = Math.min(x1, Math.ceil(damage.x + damage.width) + 1);
|
|
937
|
+
y1 = Math.min(y1, Math.ceil(damage.y + damage.height) + 1);
|
|
938
|
+
}
|
|
939
|
+
const w = x1 - x0;
|
|
940
|
+
const h = y1 - y0;
|
|
941
|
+
if (!(w > 0 && h > 0))
|
|
942
|
+
return true;
|
|
943
|
+
if (w > RASTER_SIDE || h > RASTER_SIDE || w * h > RASTER_LIMIT)
|
|
944
|
+
return true;
|
|
945
|
+
// what the context makes of the draw below: the picture's transform is
|
|
946
|
+
// the inverse of its matrix after the surface's place in it
|
|
947
|
+
const placed = invert([
|
|
948
|
+
through[0],
|
|
949
|
+
through[1],
|
|
950
|
+
through[2],
|
|
951
|
+
through[3],
|
|
952
|
+
through[0] * x0 + through[2] * y0 + through[4],
|
|
953
|
+
through[1] * x0 + through[3] * y0 + through[5],
|
|
954
|
+
]);
|
|
955
|
+
if (!placed || placed.some((n) => Math.abs(n) > FIXED_LIMIT))
|
|
956
|
+
return true;
|
|
957
|
+
const surface = options.surface(w, h);
|
|
958
|
+
if (!surface)
|
|
959
|
+
return false;
|
|
960
|
+
try {
|
|
961
|
+
// the surface's own coordinates, the window's moved to its corner
|
|
962
|
+
const on = {
|
|
963
|
+
...options,
|
|
964
|
+
originX: options.originX - x0,
|
|
965
|
+
originY: options.originY - y0,
|
|
966
|
+
damage: { x: 0, y: 0, width: w, height: h },
|
|
967
|
+
canvas: options.canvas && {
|
|
968
|
+
...options.canvas,
|
|
969
|
+
x: options.canvas.x - x0,
|
|
970
|
+
y: options.canvas.y - y0,
|
|
971
|
+
},
|
|
972
|
+
viewport: options.viewport && {
|
|
973
|
+
...options.viewport,
|
|
974
|
+
x: options.viewport.x - x0,
|
|
975
|
+
y: options.viewport.y - y0,
|
|
976
|
+
},
|
|
977
|
+
clips: [],
|
|
978
|
+
};
|
|
979
|
+
paintPlaced(surface.getContext('2d'), box, on);
|
|
980
|
+
ctx.save();
|
|
981
|
+
ctx.transform(through[0], through[1], through[2], through[3], through[4], through[5]);
|
|
982
|
+
ctx.drawImage(surface, x0, y0);
|
|
983
|
+
ctx.restore();
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
finally {
|
|
987
|
+
surface.destroy?.();
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
/** A box and what it holds, cut to its `clip` and its `clip-path` as
|
|
991
|
+
* `paintBox` cuts it. */
|
|
680
992
|
function paintClipped(ctx, box, options) {
|
|
681
|
-
const
|
|
682
|
-
if (
|
|
993
|
+
const pushed = pushOwnClips(ctx, box, options);
|
|
994
|
+
if (pushed < 0)
|
|
683
995
|
return;
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
if (clipped)
|
|
996
|
+
paintContent(ctx, box, pathClips(box) ? underPath(options) : options);
|
|
997
|
+
for (let i = 0; i < pushed; i += 1)
|
|
687
998
|
ctx.restore();
|
|
688
999
|
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Cut what follows to what a box shows of itself: the part its `clip`
|
|
1002
|
+
* names, where it is absolutely positioned, and the part its `clip-path`
|
|
1003
|
+
* does (CSS Masking 1, 5.1). How many clips were pushed, for the caller to
|
|
1004
|
+
* restore — none where the context cannot clip — or -1 where they leave
|
|
1005
|
+
* none of the box to show, and nothing was pushed.
|
|
1006
|
+
*/
|
|
1007
|
+
function pushOwnClips(ctx, box, options) {
|
|
1008
|
+
const style = box.style;
|
|
1009
|
+
const clip = box.outOfFlow && style.clip ? clipOf(box, options) : null;
|
|
1010
|
+
if (clip && !(clip.w > 0 && clip.h > 0))
|
|
1011
|
+
return -1;
|
|
1012
|
+
const path = pathClips(box) ? clipPathOf(box, options) : null;
|
|
1013
|
+
if (path && !(path.rect.w > 0 && path.rect.h > 0))
|
|
1014
|
+
return -1;
|
|
1015
|
+
let pushed = 0;
|
|
1016
|
+
if (clip && pushClip(ctx, clip, null))
|
|
1017
|
+
pushed += 1;
|
|
1018
|
+
if (path && pushClip(ctx, path.rect, path.radii))
|
|
1019
|
+
pushed += 1;
|
|
1020
|
+
return pushed;
|
|
1021
|
+
}
|
|
1022
|
+
/** Whether a box is cut to a `clip-path`: one with a rectangle of its own
|
|
1023
|
+
* for the path to be measured in. An inline box is drawn on its block's
|
|
1024
|
+
* lines, and is not cut. */
|
|
1025
|
+
export function pathClips(box) {
|
|
1026
|
+
return box.style.clipPath !== null && hasRect(box);
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* The options what a box cut to a `clip-path` holds is painted with: no
|
|
1030
|
+
* clip around it for a positioned box to escape. The path cuts all the
|
|
1031
|
+
* element holds, whatever that is positioned from, where a box that clips
|
|
1032
|
+
* its overflow lets out a box whose containing block is outside it — put
|
|
1033
|
+
* off until that clip ended (`paintPositioned`), one in here was painted
|
|
1034
|
+
* with the path gone too.
|
|
1035
|
+
*/
|
|
1036
|
+
function underPath(options) {
|
|
1037
|
+
return { ...options, clips: [] };
|
|
1038
|
+
}
|
|
689
1039
|
const MASK_LAYERS = new WeakMap();
|
|
690
1040
|
/** No corners: a mask layer is placed in the box's rectangle, which its
|
|
691
1041
|
* radii do not round (CSS Masking 1, 7). */
|
|
@@ -826,8 +1176,9 @@ function paintContent(ctx, box, options) {
|
|
|
826
1176
|
paintStacked(ctx, child, options);
|
|
827
1177
|
}
|
|
828
1178
|
if (visible) {
|
|
829
|
-
if (box.marker)
|
|
830
|
-
paintMarker(ctx, box.marker, options);
|
|
1179
|
+
if (box.marker) {
|
|
1180
|
+
paintMarker(ctx, box.marker, options, box.style.colorScheme);
|
|
1181
|
+
}
|
|
831
1182
|
if (box.replaced === 'image')
|
|
832
1183
|
paintImage(ctx, box, options);
|
|
833
1184
|
else if (box.replaced === 'svg')
|
|
@@ -921,6 +1272,13 @@ function paintOwnBackground(ctx, box, options) {
|
|
|
921
1272
|
if (hidden(box))
|
|
922
1273
|
return;
|
|
923
1274
|
const style = box.style;
|
|
1275
|
+
if (columned.any) {
|
|
1276
|
+
const pieces = COLUMN_PIECES.get(box);
|
|
1277
|
+
if (pieces) {
|
|
1278
|
+
paintColumnPieces(ctx, box, pieces, options);
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
924
1282
|
if (style.boxShadow)
|
|
925
1283
|
paintShadows(ctx, box, options, false);
|
|
926
1284
|
if (box !== options.canvasSource) {
|
|
@@ -934,6 +1292,70 @@ function paintOwnBackground(ctx, box, options) {
|
|
|
934
1292
|
if (box.kind === 'table')
|
|
935
1293
|
paintPartBackgrounds(ctx, box, options);
|
|
936
1294
|
}
|
|
1295
|
+
/**
|
|
1296
|
+
* The background and the borders of a block a column break falls inside
|
|
1297
|
+
* (CSS Multi-column 1), a piece at a time: drawn in each column as the
|
|
1298
|
+
* whole box standing there, and cut to the piece of it that is — which is
|
|
1299
|
+
* `box-decoration-break: slice`, CSS's default (CSS Fragmentation 3, 6.1):
|
|
1300
|
+
* no border and no padding at a break, and the background going on from
|
|
1301
|
+
* one piece to the next. Its shadow is not drawn: a sliced box casts one as
|
|
1302
|
+
* a whole and this has no whole box to cast it from.
|
|
1303
|
+
*/
|
|
1304
|
+
function paintColumnPieces(ctx, box, pieces, options) {
|
|
1305
|
+
if (!ctx.save || !ctx.restore || !ctx.beginPath || !ctx.rect || !ctx.clip)
|
|
1306
|
+
return;
|
|
1307
|
+
for (const piece of pieces) {
|
|
1308
|
+
if (!(piece.height > 0 && piece.width > 0))
|
|
1309
|
+
continue;
|
|
1310
|
+
const frame = {
|
|
1311
|
+
x: piece.x,
|
|
1312
|
+
y: piece.wholeY,
|
|
1313
|
+
width: piece.width,
|
|
1314
|
+
height: piece.wholeHeight,
|
|
1315
|
+
captionTop: 0,
|
|
1316
|
+
captionBottom: 0,
|
|
1317
|
+
borderTop: box.borderTop,
|
|
1318
|
+
borderRight: box.borderRight,
|
|
1319
|
+
borderBottom: box.borderBottom,
|
|
1320
|
+
borderLeft: box.borderLeft,
|
|
1321
|
+
padTop: box.padTop,
|
|
1322
|
+
padRight: box.padRight,
|
|
1323
|
+
padBottom: box.padBottom,
|
|
1324
|
+
padLeft: box.padLeft,
|
|
1325
|
+
style: box.style,
|
|
1326
|
+
};
|
|
1327
|
+
const left = Math.round(piece.x + options.originX);
|
|
1328
|
+
const top = Math.round(piece.y + options.originY);
|
|
1329
|
+
const right = Math.round(piece.x + piece.width + options.originX);
|
|
1330
|
+
const bottom = Math.round(piece.y + piece.height + options.originY);
|
|
1331
|
+
ctx.save();
|
|
1332
|
+
ctx.beginPath();
|
|
1333
|
+
ctx.rect(left, top, right - left, bottom - top);
|
|
1334
|
+
ctx.clip();
|
|
1335
|
+
paintLayers(ctx, frame, options, frameImages(ctx, frame, options));
|
|
1336
|
+
paintBorders(ctx, frame, options);
|
|
1337
|
+
ctx.restore();
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Clip to the rows of a text's layout that are in the text's column
|
|
1342
|
+
* (`COLUMN_ROWS`), where the layout is drawn at `left` and `top`: the rest
|
|
1343
|
+
* of it is another column's, drawn there. Answers whether it clipped, and
|
|
1344
|
+
* the caller restores.
|
|
1345
|
+
*/
|
|
1346
|
+
function clipToRows(ctx, text, rows, left, top) {
|
|
1347
|
+
if (!ctx.save || !ctx.restore || !ctx.beginPath || !ctx.rect || !ctx.clip)
|
|
1348
|
+
return false;
|
|
1349
|
+
const y1 = Math.round(top + rows.top);
|
|
1350
|
+
const y2 = Math.round(top + rows.bottom);
|
|
1351
|
+
ctx.save();
|
|
1352
|
+
ctx.beginPath();
|
|
1353
|
+
ctx.rect(Math.floor(left) - ROW_REACH, y1, Math.ceil(text.layout.width) + 2 * ROW_REACH, y2 - y1);
|
|
1354
|
+
ctx.clip();
|
|
1355
|
+
return true;
|
|
1356
|
+
}
|
|
1357
|
+
/** How far to either side of its layout a column's text may draw. */
|
|
1358
|
+
const ROW_REACH = 2048;
|
|
937
1359
|
/**
|
|
938
1360
|
* How `paintLayers` draws a frame's image layers: each in the area its
|
|
939
1361
|
* `background-clip` names, from the box its `background-origin` names
|
|
@@ -1220,6 +1642,8 @@ function inFlow(parent, child, options) {
|
|
|
1220
1642
|
style.overflowY === 'visible' &&
|
|
1221
1643
|
opacityOf(child) >= 1 &&
|
|
1222
1644
|
!masked(style) &&
|
|
1645
|
+
// cut to a path with all it holds, as one group
|
|
1646
|
+
!style.clipPath &&
|
|
1223
1647
|
// containment makes it a stacking context, painted whole
|
|
1224
1648
|
!contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT));
|
|
1225
1649
|
}
|
|
@@ -1246,6 +1670,7 @@ function flowFlex(parent, child, options) {
|
|
|
1246
1670
|
style.overflowY === 'visible' &&
|
|
1247
1671
|
!(child.outOfFlow && style.clip) &&
|
|
1248
1672
|
!masked(style) &&
|
|
1673
|
+
!style.clipPath &&
|
|
1249
1674
|
!contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT));
|
|
1250
1675
|
}
|
|
1251
1676
|
/**
|
|
@@ -1319,7 +1744,7 @@ function paintFlowLines(ctx, box, options, outlines) {
|
|
|
1319
1744
|
if (!intersects(child, options))
|
|
1320
1745
|
continue;
|
|
1321
1746
|
if (child.marker && child.style.visibility === 'visible') {
|
|
1322
|
-
paintMarker(ctx, child.marker, options);
|
|
1747
|
+
paintMarker(ctx, child.marker, options, child.style.colorScheme);
|
|
1323
1748
|
}
|
|
1324
1749
|
if (child.lines)
|
|
1325
1750
|
paintLines(ctx, child, options);
|
|
@@ -1472,7 +1897,11 @@ function paintPositioned(ctx, box, options) {
|
|
|
1472
1897
|
return;
|
|
1473
1898
|
}
|
|
1474
1899
|
}
|
|
1475
|
-
|
|
1900
|
+
// through its matrix, where it turns, scales or skews
|
|
1901
|
+
if (turns(box))
|
|
1902
|
+
paintTransformed(ctx, box, atViewport(box, options));
|
|
1903
|
+
else
|
|
1904
|
+
paintBox(ctx, box, atViewport(box, options));
|
|
1476
1905
|
}
|
|
1477
1906
|
/**
|
|
1478
1907
|
* The options a box is painted with where it is fixed to the viewport
|
|
@@ -1509,13 +1938,16 @@ export function fixedToViewport(box) {
|
|
|
1509
1938
|
if (box.style.position !== 'fixed')
|
|
1510
1939
|
return false;
|
|
1511
1940
|
for (let at = box.parent; at?.parent; at = at.parent) {
|
|
1512
|
-
if (at
|
|
1513
|
-
return false;
|
|
1514
|
-
if (contained(at, CONTAIN_LAYOUT | CONTAIN_PAINT))
|
|
1941
|
+
if (holdsFixed(at))
|
|
1515
1942
|
return false;
|
|
1516
1943
|
}
|
|
1517
1944
|
return true;
|
|
1518
1945
|
}
|
|
1946
|
+
/** Whether a box is the containing block of the fixed boxes in it: a
|
|
1947
|
+
* transformed box, or one with layout or paint containment. */
|
|
1948
|
+
function holdsFixed(box) {
|
|
1949
|
+
return (transformed(box.style) || contained(box, CONTAIN_LAYOUT | CONTAIN_PAINT));
|
|
1950
|
+
}
|
|
1519
1951
|
function rootOf(box) {
|
|
1520
1952
|
let root = box;
|
|
1521
1953
|
while (root.parent)
|
|
@@ -1525,17 +1957,22 @@ function rootOf(box) {
|
|
|
1525
1957
|
/**
|
|
1526
1958
|
* The box an out-of-flow box is positioned in, whose clips are the ones it
|
|
1527
1959
|
* is under (`paintPositioned`, and the hit test's `deepestAt`): its nearest
|
|
1528
|
-
* positioned or
|
|
1529
|
-
* which is under none
|
|
1960
|
+
* positioned or transformed ancestor, or the root. Null for a box fixed to
|
|
1961
|
+
* the viewport, which is under none — one in a transformed box is fixed to
|
|
1962
|
+
* that, and under what it is under.
|
|
1530
1963
|
*/
|
|
1531
1964
|
export function containingBlockOf(box) {
|
|
1532
|
-
if (box.style.position === 'fixed')
|
|
1965
|
+
if (box.style.position === 'fixed') {
|
|
1966
|
+
for (let at = box.parent; at?.parent; at = at.parent) {
|
|
1967
|
+
if (holdsFixed(at))
|
|
1968
|
+
return at;
|
|
1969
|
+
}
|
|
1533
1970
|
return null;
|
|
1971
|
+
}
|
|
1534
1972
|
let containing = box.parent;
|
|
1535
1973
|
while (containing?.parent &&
|
|
1536
1974
|
containing.style.position === 'static' &&
|
|
1537
|
-
!containing.style
|
|
1538
|
-
!containing.style.transformTranslate) {
|
|
1975
|
+
!transformed(containing.style)) {
|
|
1539
1976
|
containing = containing.parent;
|
|
1540
1977
|
}
|
|
1541
1978
|
return containing;
|
|
@@ -1628,6 +2065,201 @@ function clipOf(box, options) {
|
|
|
1628
2065
|
h: Math.round(box.y + options.originY + bottom) - y,
|
|
1629
2066
|
};
|
|
1630
2067
|
}
|
|
2068
|
+
/** The document's own coordinates, as a space to place a clip in. */
|
|
2069
|
+
const DOCUMENT = { originX: 0, originY: 0, scale: 1, damage: null };
|
|
2070
|
+
/**
|
|
2071
|
+
* A box's `clip-path` in window coordinates (CSS Masking 1, 5.1): the
|
|
2072
|
+
* rectangle its shape is (CSS Shapes 1, 3.1), each edge on the pixel it
|
|
2073
|
+
* falls nearest as a box's are, and its corners where it has any — of no
|
|
2074
|
+
* area where the shape leaves none of the box to show. Measured in the box
|
|
2075
|
+
* the path names, the border box unless it says: `inset()` in from its
|
|
2076
|
+
* edges, `rect()` and `xywh()` from its top left, and the box itself, with
|
|
2077
|
+
* the element's own corners, where no shape is written.
|
|
2078
|
+
*/
|
|
2079
|
+
function clipPathOf(box, options) {
|
|
2080
|
+
const path = box.style.clipPath;
|
|
2081
|
+
// the box it is measured in, in from the border box — or out from it
|
|
2082
|
+
const within = path.box;
|
|
2083
|
+
const [it, ir, ib, il] = within === 'margin-box'
|
|
2084
|
+
? [-box.marginTop, -box.marginRight, -box.marginBottom, -box.marginLeft]
|
|
2085
|
+
: edgeInsets(box, within);
|
|
2086
|
+
const width = Math.max(0, box.width - il - ir);
|
|
2087
|
+
const height = Math.max(0, frameHeight(box) - it - ib);
|
|
2088
|
+
// how far in from that box's edges the rectangle's are
|
|
2089
|
+
const [a, b, c, d] = path.lengths;
|
|
2090
|
+
let top = 0;
|
|
2091
|
+
let right = 0;
|
|
2092
|
+
let bottom = 0;
|
|
2093
|
+
let left = 0;
|
|
2094
|
+
if (path.shape === 'inset') {
|
|
2095
|
+
top = resolve(a, height);
|
|
2096
|
+
right = resolve(b, width);
|
|
2097
|
+
bottom = resolve(c, height);
|
|
2098
|
+
left = resolve(d, width);
|
|
2099
|
+
// a pair that adds up to more than the box is across is reduced to
|
|
2100
|
+
// it, each in its proportion, as overlapping radii are (3.1):
|
|
2101
|
+
// `inset(50%)` and `inset(75%)` are both the box's centre, and nothing
|
|
2102
|
+
if (left + right > width && left > 0 && right > 0) {
|
|
2103
|
+
const f = Math.max(0, width) / (left + right);
|
|
2104
|
+
left *= f;
|
|
2105
|
+
right *= f;
|
|
2106
|
+
}
|
|
2107
|
+
if (top + bottom > height && top > 0 && bottom > 0) {
|
|
2108
|
+
const f = Math.max(0, height) / (top + bottom);
|
|
2109
|
+
top *= f;
|
|
2110
|
+
bottom *= f;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
else if (path.shape === 'rect') {
|
|
2114
|
+
// four edges from the top and the left, `auto` the box's own; the
|
|
2115
|
+
// right and the bottom are no less than the left and the top
|
|
2116
|
+
top = a === AUTO ? 0 : resolve(a, height);
|
|
2117
|
+
left = d === AUTO ? 0 : resolve(d, width);
|
|
2118
|
+
right = b === AUTO ? 0 : width - Math.max(left, resolve(b, width));
|
|
2119
|
+
bottom = c === AUTO ? 0 : height - Math.max(top, resolve(c, height));
|
|
2120
|
+
}
|
|
2121
|
+
else if (path.shape === 'xywh') {
|
|
2122
|
+
left = resolve(a, width);
|
|
2123
|
+
top = resolve(b, height);
|
|
2124
|
+
right = width - left - Math.max(0, resolve(c, width));
|
|
2125
|
+
bottom = height - top - Math.max(0, resolve(d, height));
|
|
2126
|
+
}
|
|
2127
|
+
const x0 = box.x + options.originX + il;
|
|
2128
|
+
const y0 = frameY(box) + options.originY + it;
|
|
2129
|
+
const x = Math.round(x0 + left);
|
|
2130
|
+
const y = Math.round(y0 + top);
|
|
2131
|
+
const rect = {
|
|
2132
|
+
x,
|
|
2133
|
+
y,
|
|
2134
|
+
w: Math.max(0, Math.round(x0 + width - right) - x),
|
|
2135
|
+
h: Math.max(0, Math.round(y0 + height - bottom) - y),
|
|
2136
|
+
};
|
|
2137
|
+
let radii = null;
|
|
2138
|
+
if (path.shape === null) {
|
|
2139
|
+
// the box alone: its edge, shaped as the element's corners shape it
|
|
2140
|
+
const corners = cornersOf(box.style, box.width, frameHeight(box));
|
|
2141
|
+
if (corners) {
|
|
2142
|
+
radii =
|
|
2143
|
+
within === 'margin-box'
|
|
2144
|
+
? spreadCorners(corners, box.width, frameHeight(box), -it, -ir, -ib, -il)
|
|
2145
|
+
: roundedOf(insetCorners(corners, it, ir, ib, il));
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
else if (path.radii) {
|
|
2149
|
+
// a percentage is of the box the shape is in, and the radii are fitted
|
|
2150
|
+
// to the rectangle they round
|
|
2151
|
+
radii = cornersFor(path.radii, path.radiiY, width, height, rect.w, rect.h);
|
|
2152
|
+
}
|
|
2153
|
+
return { rect, radii };
|
|
2154
|
+
}
|
|
2155
|
+
/** Corners, or null where every one of them is square. */
|
|
2156
|
+
function roundedOf(c) {
|
|
2157
|
+
for (let i = 0; i < 4; i += 1)
|
|
2158
|
+
if (c.x[i] > 0 && c.y[i] > 0)
|
|
2159
|
+
return c;
|
|
2160
|
+
return null;
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Whether a point of the document is in what a box's `clip-path` leaves of
|
|
2164
|
+
* it: what a hit test asks, since nothing the path cuts away — of the box
|
|
2165
|
+
* or of anything in it — is under a pointer (`deepestAt`).
|
|
2166
|
+
*/
|
|
2167
|
+
export function inClipPath(box, x, y) {
|
|
2168
|
+
const { rect, radii } = clipPathOf(box, DOCUMENT);
|
|
2169
|
+
const right = rect.x + rect.w;
|
|
2170
|
+
const bottom = rect.y + rect.h;
|
|
2171
|
+
if (x < rect.x || x >= right || y < rect.y || y >= bottom)
|
|
2172
|
+
return false;
|
|
2173
|
+
if (!radii)
|
|
2174
|
+
return true;
|
|
2175
|
+
// inside the rectangle, and past a corner's curve: each corner is a
|
|
2176
|
+
// quarter of an ellipse about a centre its radii in from the two edges
|
|
2177
|
+
for (let i = 0; i < 4; i += 1) {
|
|
2178
|
+
const rx = radii.x[i];
|
|
2179
|
+
const ry = radii.y[i];
|
|
2180
|
+
if (!(rx > 0 && ry > 0))
|
|
2181
|
+
continue;
|
|
2182
|
+
const onLeft = i === 0 || i === 3;
|
|
2183
|
+
const cx = onLeft ? rect.x + rx : right - rx;
|
|
2184
|
+
const cy = i < 2 ? rect.y + ry : bottom - ry;
|
|
2185
|
+
if (onLeft ? x >= cx : x <= cx)
|
|
2186
|
+
continue;
|
|
2187
|
+
if (i < 2 ? y >= cy : y <= cy)
|
|
2188
|
+
continue;
|
|
2189
|
+
const dx = (x - cx) / rx;
|
|
2190
|
+
const dy = (y - cy) / ry;
|
|
2191
|
+
if (dx * dx + dy * dy > 1)
|
|
2192
|
+
return false;
|
|
2193
|
+
}
|
|
2194
|
+
return true;
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Whether a point of the document is in the part of a box its `clip` shows
|
|
2198
|
+
* (CSS 2.1 11.1.2) — anywhere, for a box that is cut to none: what a hit
|
|
2199
|
+
* test asks, since what the clip cuts away is not drawn (`deepestAt`). What
|
|
2200
|
+
* it cuts away is the box and what is painted with it: all the box holds
|
|
2201
|
+
* where it is a stacking context (`stacksLayers`), and elsewhere all but a
|
|
2202
|
+
* fixed box, which the context around it paints under no clip
|
|
2203
|
+
* (`clipsFor`). Chrome cuts that one too.
|
|
2204
|
+
*/
|
|
2205
|
+
export function inClip(box, x, y) {
|
|
2206
|
+
if (!box.outOfFlow || !box.style.clip)
|
|
2207
|
+
return true;
|
|
2208
|
+
const rect = clipOf(box, DOCUMENT);
|
|
2209
|
+
return (x >= rect.x && x < rect.x + rect.w && y >= rect.y && y < rect.y + rect.h);
|
|
2210
|
+
}
|
|
2211
|
+
/**
|
|
2212
|
+
* The part of the document a box shows through, in the document's own
|
|
2213
|
+
* pixels, or null where nothing cuts it: the `clip` it is cut to (CSS 2.1
|
|
2214
|
+
* 11.1.2), and the clips of the boxes it is under — each that clips its
|
|
2215
|
+
* overflow or is cut to a `clip` of its own, from its containing block up
|
|
2216
|
+
* (11.1.1), so an absolute box is outside a box that clips where its
|
|
2217
|
+
* containing block is, and a fixed one outside them all. And every
|
|
2218
|
+
* `clip-path` from the box up, its own among them: a path cuts all its
|
|
2219
|
+
* element holds, whatever that is positioned from (CSS Masking 1, 5.1) —
|
|
2220
|
+
* to the rectangle a path's shape is, its corners left square. A rectangle
|
|
2221
|
+
* of no area where none of it shows. Painting cuts what it draws as it
|
|
2222
|
+
* walks down; this is for what is not painted here and has to be cut all
|
|
2223
|
+
* the same, a control's widget (`controlRectsOf`).
|
|
2224
|
+
*/
|
|
2225
|
+
export function clipAround(box, scale = 1) {
|
|
2226
|
+
const space = { originX: 0, originY: 0, scale, damage: null };
|
|
2227
|
+
let x0 = -Infinity;
|
|
2228
|
+
let y0 = -Infinity;
|
|
2229
|
+
let x1 = Infinity;
|
|
2230
|
+
let y1 = Infinity;
|
|
2231
|
+
let cut = false;
|
|
2232
|
+
const under = (rect) => {
|
|
2233
|
+
cut = true;
|
|
2234
|
+
x0 = Math.max(x0, rect.x);
|
|
2235
|
+
y0 = Math.max(y0, rect.y);
|
|
2236
|
+
x1 = Math.min(x1, rect.x + rect.w);
|
|
2237
|
+
y1 = Math.min(y1, rect.y + rect.h);
|
|
2238
|
+
};
|
|
2239
|
+
if (box.outOfFlow && box.style.clip)
|
|
2240
|
+
under(clipOf(box, space));
|
|
2241
|
+
// what holds a box, as clips go: its parent, or out of the flow its
|
|
2242
|
+
// containing block, and the boxes between them do not clip it
|
|
2243
|
+
const holder = (inner) => inner.outOfFlow ? containingBlockOf(inner) : inner.parent;
|
|
2244
|
+
for (let outer = holder(box); outer; outer = holder(outer)) {
|
|
2245
|
+
if (outer.outOfFlow && outer.style.clip)
|
|
2246
|
+
under(clipOf(outer, space));
|
|
2247
|
+
if (clipsOverflow(outer))
|
|
2248
|
+
under(clipEdge(outer, space).rect);
|
|
2249
|
+
}
|
|
2250
|
+
for (let at = box; at; at = at.parent) {
|
|
2251
|
+
if (pathClips(at))
|
|
2252
|
+
under(clipPathOf(at, space).rect);
|
|
2253
|
+
}
|
|
2254
|
+
if (!cut)
|
|
2255
|
+
return null;
|
|
2256
|
+
return {
|
|
2257
|
+
x: x0,
|
|
2258
|
+
y: y0,
|
|
2259
|
+
width: Math.max(0, x1 - x0),
|
|
2260
|
+
height: Math.max(0, y1 - y0),
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
1631
2263
|
/**
|
|
1632
2264
|
* The radii of a box's padding edge — its border radii less the borders —
|
|
1633
2265
|
* for a clip, or null where a rectangle clips the same: padding at least a
|
|
@@ -1686,6 +2318,16 @@ function cornersOf(style, w, h) {
|
|
|
1686
2318
|
across[3] === 0) {
|
|
1687
2319
|
return null;
|
|
1688
2320
|
}
|
|
2321
|
+
return cornersFor(across, down, w, h, w, h);
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Radii as corners in pixels: each a length or a percentage of a box `w`
|
|
2325
|
+
* by `h`, across and down, and all of them reduced together where two on a
|
|
2326
|
+
* side of the rectangle they round — `fitW` by `fitH`, which for a
|
|
2327
|
+
* `border-radius` is that box, and for a `clip-path`'s `round` is the
|
|
2328
|
+
* rectangle inside it — would overlap. Null where every corner is square.
|
|
2329
|
+
*/
|
|
2330
|
+
function cornersFor(across, down, w, h, fitW, fitH) {
|
|
1689
2331
|
const vertical = down ?? across;
|
|
1690
2332
|
const x = [0, 0, 0, 0];
|
|
1691
2333
|
const y = [0, 0, 0, 0];
|
|
@@ -1706,10 +2348,10 @@ function cornersOf(style, w, h) {
|
|
|
1706
2348
|
if (sum > side)
|
|
1707
2349
|
f = Math.min(f, Math.max(0, side) / sum);
|
|
1708
2350
|
};
|
|
1709
|
-
fit(
|
|
1710
|
-
fit(
|
|
1711
|
-
fit(
|
|
1712
|
-
fit(
|
|
2351
|
+
fit(fitW, x[0] + x[1]);
|
|
2352
|
+
fit(fitH, y[1] + y[2]);
|
|
2353
|
+
fit(fitW, x[2] + x[3]);
|
|
2354
|
+
fit(fitH, y[3] + y[0]);
|
|
1713
2355
|
if (f < 1) {
|
|
1714
2356
|
for (let i = 0; i < 4; i += 1) {
|
|
1715
2357
|
x[i] *= f;
|
|
@@ -1838,8 +2480,7 @@ function layered(parent, child) {
|
|
|
1838
2480
|
return true;
|
|
1839
2481
|
if (style.position !== 'relative' &&
|
|
1840
2482
|
style.position !== 'sticky' &&
|
|
1841
|
-
!style
|
|
1842
|
-
!style.transformTranslate) {
|
|
2483
|
+
!transformed(style)) {
|
|
1843
2484
|
return false;
|
|
1844
2485
|
}
|
|
1845
2486
|
return child.kind !== 'inline' && !onLine(parent, child);
|
|
@@ -1851,7 +2492,7 @@ function layered(parent, child) {
|
|
|
1851
2492
|
* drawn twice — darker at every antialiased edge — and a translucent
|
|
1852
2493
|
* background had its alpha doubled.
|
|
1853
2494
|
*/
|
|
1854
|
-
function onLine(parent, child) {
|
|
2495
|
+
export function onLine(parent, child) {
|
|
1855
2496
|
if (parent.lines === null && parent.kind !== 'inline')
|
|
1856
2497
|
return false;
|
|
1857
2498
|
if (child.isFloat || child.outOfFlow)
|
|
@@ -1927,24 +2568,31 @@ function settleLayers(box, list) {
|
|
|
1927
2568
|
* it itself, and the ones below its flow: positioned with a `z-index`, or a
|
|
1928
2569
|
* flex item with one; fixed or sticky with none (CSS Positioned Layout 3, as
|
|
1929
2570
|
* browsers paint them — a fixed header's box set behind its content with
|
|
1930
|
-
* `z-index: -1` went behind the page);
|
|
2571
|
+
* `z-index: -1` went behind the page); transformed (CSS Transforms 1, 3);
|
|
2572
|
+
* or under full opacity (CSS Color 4
|
|
1931
2573
|
* 3.2), its own or the opacity it takes from an inline box it broke
|
|
1932
2574
|
* (`FADED_BLOCKS`), which is painted as one group — faded with it, or at
|
|
1933
2575
|
* `opacity: 0` not at all, where its root context drew a hover menu's
|
|
1934
2576
|
* absolute children.
|
|
1935
2577
|
*/
|
|
1936
|
-
function stacksLayers(box) {
|
|
2578
|
+
export function stacksLayers(box) {
|
|
1937
2579
|
const style = box.style;
|
|
1938
2580
|
if (style.position === 'fixed' || style.position === 'sticky')
|
|
1939
2581
|
return true;
|
|
1940
2582
|
if (style.opacity < 1 || FADED_BLOCKS.has(box))
|
|
1941
2583
|
return true;
|
|
1942
|
-
// and so does a mask, which is applied to the group (CSS Masking 1, 7)
|
|
1943
|
-
|
|
2584
|
+
// and so does a mask, which is applied to the group (CSS Masking 1, 7),
|
|
2585
|
+
// and a clip path, which cuts it (5.1)
|
|
2586
|
+
if (masked(style) || pathClips(box))
|
|
1944
2587
|
return true;
|
|
1945
2588
|
// layout and paint containment make one (CSS Containment 2, 3.3, 3.5)
|
|
1946
2589
|
if (contained(box, CONTAIN_LAYOUT | CONTAIN_PAINT))
|
|
1947
2590
|
return true;
|
|
2591
|
+
// and a transform (CSS Transforms 1, 3), on a box one applies to: what it
|
|
2592
|
+
// holds is painted with it, through its matrix, and not by a context
|
|
2593
|
+
// outside it
|
|
2594
|
+
if (transformed(style) && box.kind !== 'inline')
|
|
2595
|
+
return true;
|
|
1948
2596
|
if (typeof style.zIndex !== 'number')
|
|
1949
2597
|
return false;
|
|
1950
2598
|
return style.position !== 'static' || flexItem(box);
|
|
@@ -1967,7 +2615,7 @@ export function clipsFor(box, context) {
|
|
|
1967
2615
|
const fixed = box.style.position === 'fixed';
|
|
1968
2616
|
while (from && from !== context) {
|
|
1969
2617
|
const style = from.style;
|
|
1970
|
-
if (style
|
|
2618
|
+
if (transformed(style))
|
|
1971
2619
|
break;
|
|
1972
2620
|
if (!fixed && style.position !== 'static')
|
|
1973
2621
|
break;
|
|
@@ -2067,9 +2715,19 @@ function hoistFrom(box, root) {
|
|
|
2067
2715
|
return null;
|
|
2068
2716
|
}
|
|
2069
2717
|
function byZIndex(a, b) {
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2718
|
+
return layerOf(a) - layerOf(b);
|
|
2719
|
+
}
|
|
2720
|
+
/** The `z-index` a box is ordered by among its stacking context's layers:
|
|
2721
|
+
* its own where one applies — to a positioned box, and to a flex or a grid
|
|
2722
|
+
* item (CSS 2.1 9.9.1, CSS Flexbox 5.4) — and none for a box that is
|
|
2723
|
+
* among them for its transform alone, which is painted in the document's
|
|
2724
|
+
* order whatever `z-index` it was given. */
|
|
2725
|
+
function layerOf(box) {
|
|
2726
|
+
const style = box.style;
|
|
2727
|
+
const z = style.zIndex;
|
|
2728
|
+
if (z === 'auto')
|
|
2729
|
+
return 0;
|
|
2730
|
+
return style.position !== 'static' || flexItem(box) ? z : 0;
|
|
2073
2731
|
}
|
|
2074
2732
|
/** Whether anything this box or its descendants draw is in the damage. */
|
|
2075
2733
|
function intersects(box, options) {
|
|
@@ -2147,7 +2805,7 @@ function paintBackground(ctx, box, options, style) {
|
|
|
2147
2805
|
ctx.fillStyle = inkColor(color, style.color);
|
|
2148
2806
|
fill();
|
|
2149
2807
|
}
|
|
2150
|
-
if (gradient
|
|
2808
|
+
if (gradient) {
|
|
2151
2809
|
// over the colour, as the layer an image is: the size of the padding
|
|
2152
2810
|
// box, which is where it starts, and repeated under the borders — or
|
|
2153
2811
|
// the viewport's, where it is fixed; its line runs across the whole of
|
|
@@ -2170,8 +2828,9 @@ function paintBackground(ctx, box, options, style) {
|
|
|
2170
2828
|
// with its end colours where a browser would show the next tile
|
|
2171
2829
|
if (!(at.width > 0 && at.height > 0))
|
|
2172
2830
|
return;
|
|
2173
|
-
|
|
2174
|
-
|
|
2831
|
+
const paint = gradientFill(ctx, gradient, at.x, at.y, at.width, at.height, style.color, rect);
|
|
2832
|
+
if (paint)
|
|
2833
|
+
fillGradient(ctx, paint, rect, rounded);
|
|
2175
2834
|
}
|
|
2176
2835
|
else
|
|
2177
2836
|
paintGradient(ctx, style, gradient, rect, at);
|
|
@@ -2219,8 +2878,9 @@ function paintGradient(ctx, style, gradient, area, at) {
|
|
|
2219
2878
|
MAX_TILES) {
|
|
2220
2879
|
// a sliver of a root repeated down a long canvas: the one tile, and
|
|
2221
2880
|
// its end colours on past it
|
|
2222
|
-
|
|
2223
|
-
|
|
2881
|
+
const paint = gradientFill(ctx, gradient, x0, y0, w, h, style.color, area);
|
|
2882
|
+
if (paint)
|
|
2883
|
+
fillGradient(ctx, paint, area);
|
|
2224
2884
|
return;
|
|
2225
2885
|
}
|
|
2226
2886
|
for (let y = fromY; y < toY; y += stepY) {
|
|
@@ -2234,8 +2894,9 @@ function paintGradient(ctx, style, gradient, area, at) {
|
|
|
2234
2894
|
if (right <= left)
|
|
2235
2895
|
continue;
|
|
2236
2896
|
const tile = { x: left, y: top, w: right - left, h: bottom - top };
|
|
2237
|
-
|
|
2238
|
-
|
|
2897
|
+
const paint = gradientFill(ctx, gradient, x, y, w, h, style.color, tile);
|
|
2898
|
+
if (paint)
|
|
2899
|
+
fillGradient(ctx, paint, tile);
|
|
2239
2900
|
}
|
|
2240
2901
|
}
|
|
2241
2902
|
}
|
|
@@ -2265,6 +2926,39 @@ function linearGradient(ctx, gradient, x, y, w, h, currentColor, visible = null)
|
|
|
2265
2926
|
const dy = -Math.cos(angle);
|
|
2266
2927
|
const length = Math.abs(w * dx) + Math.abs(h * dy);
|
|
2267
2928
|
const stops = gradient.stops;
|
|
2929
|
+
const at = stopOffsets(stops, length);
|
|
2930
|
+
// the context's offsets are 0 to 1, so a line that stops reach past runs
|
|
2931
|
+
// from the first to the last, and the box sees the part of it between
|
|
2932
|
+
const from = Math.min(0, at[0]);
|
|
2933
|
+
const to = Math.max(1, at[at.length - 1]);
|
|
2934
|
+
const startX = x + w / 2 - (dx * length) / 2;
|
|
2935
|
+
const startY = y + h / 2 - (dy * length) / 2;
|
|
2936
|
+
const x0 = startX + dx * length * from;
|
|
2937
|
+
const y0 = startY + dy * length * from;
|
|
2938
|
+
const x1 = startX + dx * length * to;
|
|
2939
|
+
const y1 = startY + dy * length * to;
|
|
2940
|
+
const colors = stops.map((stop) => inkColor(stop.color, currentColor));
|
|
2941
|
+
if (visible &&
|
|
2942
|
+
length > 0 &&
|
|
2943
|
+
!(Math.abs(x0) < GRADIENT_REACH &&
|
|
2944
|
+
Math.abs(y0) < GRADIENT_REACH &&
|
|
2945
|
+
Math.abs(x1) < GRADIENT_REACH &&
|
|
2946
|
+
Math.abs(y1) < GRADIENT_REACH)) {
|
|
2947
|
+
return clippedGradient(ctx, at, colors, startX, startY, dx * length, dy * length, visible);
|
|
2948
|
+
}
|
|
2949
|
+
const g = ctx.createLinearGradient(x0, y0, x1, y1);
|
|
2950
|
+
for (let i = 0; i < stops.length; i += 1) {
|
|
2951
|
+
g.addColorStop((at[i] - from) / (to - from), colors[i]);
|
|
2952
|
+
}
|
|
2953
|
+
return g;
|
|
2954
|
+
}
|
|
2955
|
+
/**
|
|
2956
|
+
* Where each of a gradient's stops is along a line `length` long, as a
|
|
2957
|
+
* fraction of it (CSS Images 3, 3.5.1): a stop without a position is
|
|
2958
|
+
* spread evenly between its neighbours', the first at 0 and the last at 1
|
|
2959
|
+
* where they have none, and none is before the one before it.
|
|
2960
|
+
*/
|
|
2961
|
+
function stopOffsets(stops, length) {
|
|
2268
2962
|
const at = stops.map((stop) => stop.at === null
|
|
2269
2963
|
? null
|
|
2270
2964
|
: length > 0
|
|
@@ -2288,30 +2982,195 @@ function linearGradient(ctx, gradient, x, y, w, h, currentColor, visible = null)
|
|
|
2288
2982
|
}
|
|
2289
2983
|
at[i] = Math.max(at[i], at[i - 1]);
|
|
2290
2984
|
}
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
const y0 = startY + dy * length * from;
|
|
2299
|
-
const x1 = startX + dx * length * to;
|
|
2300
|
-
const y1 = startY + dy * length * to;
|
|
2301
|
-
const colors = stops.map((stop) => inkColor(stop.color, currentColor));
|
|
2302
|
-
if (visible &&
|
|
2303
|
-
length > 0 &&
|
|
2304
|
-
!(Math.abs(x0) < GRADIENT_REACH &&
|
|
2305
|
-
Math.abs(y0) < GRADIENT_REACH &&
|
|
2306
|
-
Math.abs(x1) < GRADIENT_REACH &&
|
|
2307
|
-
Math.abs(y1) < GRADIENT_REACH)) {
|
|
2308
|
-
return clippedGradient(ctx, at, colors, startX, startY, dx * length, dy * length, visible);
|
|
2985
|
+
return at;
|
|
2986
|
+
}
|
|
2987
|
+
/** A gradient's fill for a box at (`x`, `y`), `w` by `h`: null where the
|
|
2988
|
+
* context draws no gradient of its kind. */
|
|
2989
|
+
function gradientFill(ctx, gradient, x, y, w, h, currentColor, visible = null) {
|
|
2990
|
+
if (gradient.kind === 'radial') {
|
|
2991
|
+
return radialGradient(ctx, gradient, x, y, w, h, currentColor, visible);
|
|
2309
2992
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2993
|
+
if (!ctx.createLinearGradient)
|
|
2994
|
+
return null;
|
|
2995
|
+
return {
|
|
2996
|
+
style: linearGradient(ctx, gradient, x, y, w, h, currentColor, visible),
|
|
2997
|
+
matrix: null,
|
|
2998
|
+
};
|
|
2999
|
+
}
|
|
3000
|
+
/**
|
|
3001
|
+
* A `radial-gradient()` across a box (CSS Images 3, 3.2.2): centred where
|
|
3002
|
+
* its position puts it, and as large as its radii, or as its keyword says
|
|
3003
|
+
* — the side or the corner of the box nearest the centre or furthest from
|
|
3004
|
+
* it, and for an ellipse through a corner, the ellipse with the shape the
|
|
3005
|
+
* sides would give it. The stops are spaced along the ray from the centre
|
|
3006
|
+
* to the ending shape's edge, which for an ellipse is its horizontal
|
|
3007
|
+
* radius; one before the centre, where the ray starts, gives the colour
|
|
3008
|
+
* there and no more, and one past the edge carries the gradient on.
|
|
3009
|
+
*
|
|
3010
|
+
* A shape with no width or no height is drawn as its last colour, as
|
|
3011
|
+
* Chrome draws each (crbug.com/635727): the spec draws a zero width as a
|
|
3012
|
+
* gradient mirrored about the centre line, which Chrome does not.
|
|
3013
|
+
*/
|
|
3014
|
+
function radialGradient(ctx, gradient, x, y, w, h, currentColor, visible = null) {
|
|
3015
|
+
const colors = gradient.stops.map((stop) => inkColor(stop.color, currentColor));
|
|
3016
|
+
const cx = resolve(gradient.at[0], w);
|
|
3017
|
+
const cy = resolve(gradient.at[1], h);
|
|
3018
|
+
let rx;
|
|
3019
|
+
let ry;
|
|
3020
|
+
if (gradient.radii) {
|
|
3021
|
+
rx = resolve(gradient.radii[0], w);
|
|
3022
|
+
ry = resolve(gradient.radii[1], h);
|
|
2313
3023
|
}
|
|
2314
|
-
|
|
3024
|
+
else {
|
|
3025
|
+
const extent = gradient.extent ?? 'farthest-corner';
|
|
3026
|
+
const side = extent.startsWith('closest') ? Math.min : Math.max;
|
|
3027
|
+
const sx = side(Math.abs(cx), Math.abs(w - cx));
|
|
3028
|
+
const sy = side(Math.abs(cy), Math.abs(h - cy));
|
|
3029
|
+
const corner = extent.endsWith('corner');
|
|
3030
|
+
if (gradient.circle) {
|
|
3031
|
+
rx = ry = corner ? Math.hypot(sx, sy) : side(sx, sy);
|
|
3032
|
+
}
|
|
3033
|
+
else {
|
|
3034
|
+
// through the corner, in the shape the sides give it
|
|
3035
|
+
rx = corner ? sx * Math.SQRT2 : sx;
|
|
3036
|
+
ry = corner ? sy * Math.SQRT2 : sy;
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
if (!(rx > 0 && ry > 0)) {
|
|
3040
|
+
return { style: colors[colors.length - 1], matrix: null };
|
|
3041
|
+
}
|
|
3042
|
+
if (!hasRadialGradients(ctx))
|
|
3043
|
+
return null;
|
|
3044
|
+
let at = stopOffsets(gradient.stops, rx);
|
|
3045
|
+
let inks = colors;
|
|
3046
|
+
if (at[0] < 0) {
|
|
3047
|
+
// the colour at the centre, between the stops either side of it
|
|
3048
|
+
const first = colorAlong(at, colors, 0);
|
|
3049
|
+
const i = at.findIndex((t) => t > 0);
|
|
3050
|
+
if (i < 0)
|
|
3051
|
+
return { style: first, matrix: null };
|
|
3052
|
+
at = [0, ...at.slice(i)];
|
|
3053
|
+
inks = [first, ...colors.slice(i)];
|
|
3054
|
+
}
|
|
3055
|
+
const ax = x + cx;
|
|
3056
|
+
const ay = y + cy;
|
|
3057
|
+
const seen = visible ?? { x, y, w, h };
|
|
3058
|
+
if (!(Math.abs(ax) < RADIAL_REACH && Math.abs(ay) < RADIAL_REACH)) {
|
|
3059
|
+
return farRadial(ctx, at, inks, ax, ay, rx, ry, seen);
|
|
3060
|
+
}
|
|
3061
|
+
let to = Math.max(1, at[at.length - 1]);
|
|
3062
|
+
if (Math.max(rx, ry) * to > GRADIENT_REACH) {
|
|
3063
|
+
// as far along the ray as what is seen of it reaches, and no further:
|
|
3064
|
+
// a circle a context cannot carry, cut to the part it draws
|
|
3065
|
+
let reach = 0;
|
|
3066
|
+
for (const px of [seen.x, seen.x + seen.w]) {
|
|
3067
|
+
for (const py of [seen.y, seen.y + seen.h]) {
|
|
3068
|
+
reach = Math.max(reach, Math.hypot((px - ax) / rx, (py - ay) / ry));
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
if (reach > 0 && reach < to) {
|
|
3072
|
+
const last = colorAlong(at, inks, reach);
|
|
3073
|
+
const kept = at.filter((t) => t < reach).length;
|
|
3074
|
+
at = [...at.slice(0, kept), reach];
|
|
3075
|
+
inks = [...inks.slice(0, kept), last];
|
|
3076
|
+
to = reach;
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
const circle = rx === ry;
|
|
3080
|
+
// a circle as wide as the ellipse is narrow, stretched along the other
|
|
3081
|
+
// axis: the matrix only ever widens, so its inverse, which a context
|
|
3082
|
+
// samples the gradient through, reaches no further than the centre does
|
|
3083
|
+
const r = Math.min(rx, ry);
|
|
3084
|
+
const g = circle
|
|
3085
|
+
? ctx.createRadialGradient(ax, ay, 0, ax, ay, r * to)
|
|
3086
|
+
: ctx.createRadialGradient(0, 0, 0, 0, 0, r * to);
|
|
3087
|
+
for (let i = 0; i < at.length; i += 1) {
|
|
3088
|
+
g.addColorStop(at[i] / to, inks[i]);
|
|
3089
|
+
}
|
|
3090
|
+
return {
|
|
3091
|
+
style: g,
|
|
3092
|
+
matrix: circle ? null : [rx / r, 0, 0, ry / r, ax, ay],
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
/**
|
|
3096
|
+
* How far from the origin a radial gradient's centre may be and still be
|
|
3097
|
+
* the context's to draw: ntk makes a gradient past what X RENDER carries at
|
|
3098
|
+
* a scale that fits, and its 16.16 matrix holds that scale to a fraction of
|
|
3099
|
+
* a pixel to here.
|
|
3100
|
+
*/
|
|
3101
|
+
const RADIAL_REACH = 1 << 18;
|
|
3102
|
+
/**
|
|
3103
|
+
* A radial gradient whose centre is further off than a context carries —
|
|
3104
|
+
* `at 0 calc(infinity * 1px)`, which comes to the largest length there is —
|
|
3105
|
+
* across the part of it that is seen: from that far its rings are as good
|
|
3106
|
+
* as straight, so it is drawn as the linear gradient that runs the way the
|
|
3107
|
+
* rings spread there, as fast as they do, cut to what is seen as a linear
|
|
3108
|
+
* gradient's line is. The rings bend away from it by the square of what is
|
|
3109
|
+
* seen over eight times the distance, under a pixel from here.
|
|
3110
|
+
*/
|
|
3111
|
+
function farRadial(ctx, at, colors, ax, ay, rx, ry, seen) {
|
|
3112
|
+
if (!ctx.createLinearGradient)
|
|
3113
|
+
return null;
|
|
3114
|
+
// the middle of what is seen, in the circle's own space, and how fast
|
|
3115
|
+
// the gradient runs there, a pixel
|
|
3116
|
+
const px = seen.x + seen.w / 2;
|
|
3117
|
+
const py = seen.y + seen.h / 2;
|
|
3118
|
+
const ux = (px - ax) / rx;
|
|
3119
|
+
const uy = (py - ay) / ry;
|
|
3120
|
+
const t = Math.hypot(ux, uy);
|
|
3121
|
+
const gx = ux / rx / t;
|
|
3122
|
+
const gy = uy / ry / t;
|
|
3123
|
+
const g2 = gx * gx + gy * gy;
|
|
3124
|
+
if (!(t > 0 && g2 > 0 && Number.isFinite(t / g2))) {
|
|
3125
|
+
return { style: colorAlong(at, colors, t), matrix: null };
|
|
3126
|
+
}
|
|
3127
|
+
// the line it runs along, from where it would start to where it is 1
|
|
3128
|
+
const lx = gx / g2;
|
|
3129
|
+
const ly = gy / g2;
|
|
3130
|
+
return {
|
|
3131
|
+
style: clippedGradient(ctx, at, colors, px - lx * t, py - ly * t, lx, ly, seen),
|
|
3132
|
+
matrix: null,
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
3135
|
+
/**
|
|
3136
|
+
* Whether a context draws radial gradients. react-x11's macOS and Windows
|
|
3137
|
+
* contexts have the method and paint what it returns flat, in one colour
|
|
3138
|
+
* — over whatever a vignette was meant to let through — and theirs takes
|
|
3139
|
+
* no circles, which is how it is told from one that draws them: ntk's and
|
|
3140
|
+
* the Wayland context's take the six numbers canvas gives it. There a
|
|
3141
|
+
* radial gradient is drawn as nothing, as every one was.
|
|
3142
|
+
*/
|
|
3143
|
+
function hasRadialGradients(ctx) {
|
|
3144
|
+
return (ctx.createRadialGradient?.length ?? 0) >= 6;
|
|
3145
|
+
}
|
|
3146
|
+
/**
|
|
3147
|
+
* Fills a rectangle, rounded where `corners` says, with a gradient. An
|
|
3148
|
+
* ellipse's shape is laid down first and filled under its matrix, which a
|
|
3149
|
+
* context applies to the fill's paint and not to a path it already has.
|
|
3150
|
+
*/
|
|
3151
|
+
function fillGradient(ctx, fill, r, corners = null) {
|
|
3152
|
+
ctx.fillStyle = fill.style;
|
|
3153
|
+
if (!fill.matrix) {
|
|
3154
|
+
if (corners) {
|
|
3155
|
+
ctx.beginPath();
|
|
3156
|
+
roundedRect(ctx, r.x, r.y, r.w, r.h, corners);
|
|
3157
|
+
ctx.fill();
|
|
3158
|
+
}
|
|
3159
|
+
else
|
|
3160
|
+
ctx.fillRect(r.x, r.y, r.w, r.h);
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3163
|
+
if (!ctx.transform || !ctx.beginPath || !ctx.rect || !ctx.fill)
|
|
3164
|
+
return;
|
|
3165
|
+
ctx.save();
|
|
3166
|
+
ctx.beginPath();
|
|
3167
|
+
if (corners)
|
|
3168
|
+
roundedRect(ctx, r.x, r.y, r.w, r.h, corners);
|
|
3169
|
+
else
|
|
3170
|
+
ctx.rect(r.x, r.y, r.w, r.h);
|
|
3171
|
+
ctx.transform(...fill.matrix);
|
|
3172
|
+
ctx.fill();
|
|
3173
|
+
ctx.restore();
|
|
2315
3174
|
}
|
|
2316
3175
|
/**
|
|
2317
3176
|
* How far from the origin a gradient's line may end. X RENDER takes the
|
|
@@ -2353,29 +3212,30 @@ function clippedGradient(ctx, at, colors, sx, sy, lx, ly, visible) {
|
|
|
2353
3212
|
const cy = (top + bottom) / 2;
|
|
2354
3213
|
const tc = along(cx, cy);
|
|
2355
3214
|
const g = ctx.createLinearGradient(cx + (t0 - tc) * lx, cy + (t0 - tc) * ly, cx + (t1 - tc) * lx, cy + (t1 - tc) * ly);
|
|
2356
|
-
|
|
2357
|
-
const colorAt = (t) => {
|
|
2358
|
-
if (!(t > at[0]))
|
|
2359
|
-
return colors[0];
|
|
2360
|
-
for (let i = 1; i < at.length; i += 1) {
|
|
2361
|
-
if (t > at[i])
|
|
2362
|
-
continue;
|
|
2363
|
-
const span = at[i] - at[i - 1];
|
|
2364
|
-
const f = span > 0 ? (t - at[i - 1]) / span : 1;
|
|
2365
|
-
return (blend(colors[i - 1], colors[i], f) ??
|
|
2366
|
-
(f < 0.5 ? colors[i - 1] : colors[i]));
|
|
2367
|
-
}
|
|
2368
|
-
return colors[colors.length - 1];
|
|
2369
|
-
};
|
|
2370
|
-
g.addColorStop(0, colorAt(t0));
|
|
3215
|
+
g.addColorStop(0, colorAlong(at, colors, t0));
|
|
2371
3216
|
for (let i = 0; i < at.length; i += 1) {
|
|
2372
3217
|
if (at[i] > t0 && at[i] < t1) {
|
|
2373
3218
|
g.addColorStop((at[i] - t0) / (t1 - t0), colors[i]);
|
|
2374
3219
|
}
|
|
2375
3220
|
}
|
|
2376
|
-
g.addColorStop(1,
|
|
3221
|
+
g.addColorStop(1, colorAlong(at, colors, t1));
|
|
2377
3222
|
return g;
|
|
2378
3223
|
}
|
|
3224
|
+
/** The colour at a place along a gradient whose stops are at `at`: a
|
|
3225
|
+
* stop's, or between two. */
|
|
3226
|
+
function colorAlong(at, colors, t) {
|
|
3227
|
+
if (!(t > at[0]))
|
|
3228
|
+
return colors[0];
|
|
3229
|
+
for (let i = 1; i < at.length; i += 1) {
|
|
3230
|
+
if (t > at[i])
|
|
3231
|
+
continue;
|
|
3232
|
+
const span = at[i] - at[i - 1];
|
|
3233
|
+
const f = span > 0 ? (t - at[i - 1]) / span : 1;
|
|
3234
|
+
return (blend(colors[i - 1], colors[i], f) ??
|
|
3235
|
+
(f < 0.5 ? colors[i - 1] : colors[i]));
|
|
3236
|
+
}
|
|
3237
|
+
return colors[colors.length - 1];
|
|
3238
|
+
}
|
|
2379
3239
|
/**
|
|
2380
3240
|
* The backgrounds of a table's column groups, columns, row groups and rows
|
|
2381
3241
|
* (CSS 2.1 17.5.1), in that order: over the table's own and under its
|
|
@@ -2568,12 +3428,13 @@ function paintBackgroundImage(ctx, style, area, at, options, corners = null) {
|
|
|
2568
3428
|
// a drawing is drawn a tile at a time, at the size it was given
|
|
2569
3429
|
if (tiles <= MAX_TILES) {
|
|
2570
3430
|
for (let y = fromY; y < toY; y += stepY) {
|
|
2571
|
-
for (let x = fromX; x < toX; x += stepX)
|
|
2572
|
-
svg.
|
|
3431
|
+
for (let x = fromX; x < toX; x += stepX) {
|
|
3432
|
+
svg.drawImage(ctx, Math.round(x), Math.round(y), iw, ih, scale, style.colorScheme);
|
|
3433
|
+
}
|
|
2573
3434
|
}
|
|
2574
3435
|
}
|
|
2575
3436
|
else {
|
|
2576
|
-
svg.
|
|
3437
|
+
svg.drawImage(ctx, x0, y0, iw, ih, scale, style.colorScheme);
|
|
2577
3438
|
}
|
|
2578
3439
|
}
|
|
2579
3440
|
else if (tiles > 1 &&
|
|
@@ -2721,16 +3582,6 @@ function tileSize(size, area, scale) {
|
|
|
2721
3582
|
}
|
|
2722
3583
|
return [area.width, area.height];
|
|
2723
3584
|
}
|
|
2724
|
-
/**
|
|
2725
|
-
* Borders, as four rectangles.
|
|
2726
|
-
*
|
|
2727
|
-
* Not as a stroked path, for the reason richtext gives about its underlines:
|
|
2728
|
-
* the mock backend has no path API, and a 1px border on a pixel grid is a
|
|
2729
|
-
* rectangle rather than something an antialiased stroke improves. Corners are
|
|
2730
|
-
* mitred by drawing the top and bottom full-width and the sides between them,
|
|
2731
|
-
* which is right whenever the two sides share a colour and close enough when
|
|
2732
|
-
* they do not.
|
|
2733
|
-
*/
|
|
2734
3585
|
/**
|
|
2735
3586
|
* A box's outline (CSS 2.1 18.4, CSS UI 4 5): a border of its own width,
|
|
2736
3587
|
* style and colour round the border box grown by `outline-offset` — which
|
|
@@ -2782,6 +3633,26 @@ function paintOutline(ctx, box, options) {
|
|
|
2782
3633
|
style: ring,
|
|
2783
3634
|
}, options);
|
|
2784
3635
|
}
|
|
3636
|
+
/**
|
|
3637
|
+
* Borders, a side at a time.
|
|
3638
|
+
*
|
|
3639
|
+
* As rectangles where they can be, the top and the bottom full width and
|
|
3640
|
+
* the sides between them, and not as a stroked path, for the reason
|
|
3641
|
+
* richtext gives about its underlines: the mock backend has no path API, and
|
|
3642
|
+
* a 1px border on a pixel grid is a rectangle rather than something an
|
|
3643
|
+
* antialiased stroke improves. That is every border whose sides share a
|
|
3644
|
+
* colour, where a corner looks the same whichever side it is given to.
|
|
3645
|
+
*
|
|
3646
|
+
* Where two sides that meet differ in colour — one of them transparent
|
|
3647
|
+
* included — the corner is cut on the diagonal from its outer point to its
|
|
3648
|
+
* inner one and each side takes its half (CSS Backgrounds 3, 4.3), so a side
|
|
3649
|
+
* is a trapezoid, and a triangle where the box inside the borders has no
|
|
3650
|
+
* width: the CSS triangle a dropdown's caret and a tooltip's arrow are
|
|
3651
|
+
* drawn with, which came out as the rectangle around it. Only a solid side
|
|
3652
|
+
* is cut, and only against a side that is solid or paints nothing; dots,
|
|
3653
|
+
* dashes and a double border's two lines keep their rectangles, the lines
|
|
3654
|
+
* joined where two double sides of one colour meet (`fillSide`).
|
|
3655
|
+
*/
|
|
2785
3656
|
function paintBorders(ctx, box, options) {
|
|
2786
3657
|
// most boxes have none, and are asked on every paint
|
|
2787
3658
|
if (!(box.borderTop || box.borderRight || box.borderBottom || box.borderLeft))
|
|
@@ -2799,27 +3670,145 @@ function paintBorders(ctx, box, options) {
|
|
|
2799
3670
|
return;
|
|
2800
3671
|
if (sculpted(s) && paintSculpted(ctx, box, x, y, w, h, options))
|
|
2801
3672
|
return;
|
|
2802
|
-
const
|
|
2803
|
-
|
|
2804
|
-
|
|
3673
|
+
const t = box.borderTop;
|
|
3674
|
+
const r = box.borderRight;
|
|
3675
|
+
const b = box.borderBottom;
|
|
3676
|
+
const l = box.borderLeft;
|
|
3677
|
+
/** What a side is painted in, null where it paints nothing. */
|
|
3678
|
+
const ink = (width, color) => width > 0 && !isTransparent(color) ? inkColor(color, s.color) : null;
|
|
3679
|
+
const topInk = ink(t, s.borderTopColor);
|
|
3680
|
+
const rightInk = ink(r, s.borderRightColor);
|
|
3681
|
+
const bottomInk = ink(b, s.borderBottomColor);
|
|
3682
|
+
const leftInk = ink(l, s.borderLeftColor);
|
|
3683
|
+
// the corners cut on the diagonal; none where the context draws no path,
|
|
3684
|
+
// and the sides are rectangles
|
|
3685
|
+
const paths = !!(ctx.beginPath && ctx.moveTo && ctx.lineTo && ctx.fill);
|
|
3686
|
+
const topStyle = s.borderTopStyle;
|
|
3687
|
+
const rightStyle = s.borderRightStyle;
|
|
3688
|
+
const bottomStyle = s.borderBottomStyle;
|
|
3689
|
+
const leftStyle = s.borderLeftStyle;
|
|
3690
|
+
const tl = paths && mitred(t, l, topInk, leftInk, topStyle, leftStyle);
|
|
3691
|
+
const tr = paths && mitred(t, r, topInk, rightInk, topStyle, rightStyle);
|
|
3692
|
+
const br = paths && mitred(b, r, bottomInk, rightInk, bottomStyle, rightStyle);
|
|
3693
|
+
const bl = paths && mitred(b, l, bottomInk, leftInk, bottomStyle, leftStyle);
|
|
3694
|
+
const widths = [t, r, b, l];
|
|
3695
|
+
const joins = doubleJoins(box);
|
|
3696
|
+
/** A side with square corners, the top first and clockwise. */
|
|
3697
|
+
const edge = (at, style, color) => {
|
|
3698
|
+
ctx.fillStyle = color;
|
|
3699
|
+
fillSide(ctx, options, at, x, y, w, h, widths, style, joins);
|
|
3700
|
+
};
|
|
3701
|
+
/** A side with a cut corner: its outer edge and then its inner one. */
|
|
3702
|
+
const side = (points, color) => {
|
|
3703
|
+
const area = clampRect(options, x, y, w, h);
|
|
3704
|
+
if (!area)
|
|
2805
3705
|
return;
|
|
2806
|
-
ctx.fillStyle =
|
|
2807
|
-
|
|
2808
|
-
// not crawl as the viewport moves along a long edge.
|
|
2809
|
-
fillEdge(ctx, rect, horizontal ? ex : ey, style, horizontal);
|
|
3706
|
+
ctx.fillStyle = color;
|
|
3707
|
+
fillPolygon(ctx, points, area);
|
|
2810
3708
|
};
|
|
2811
|
-
|
|
2812
|
-
|
|
3709
|
+
// the border box's far edges, and the padding box inside it
|
|
3710
|
+
const x1 = x + w;
|
|
3711
|
+
const y1 = y + h;
|
|
3712
|
+
const px0 = x + l;
|
|
3713
|
+
const py0 = y + t;
|
|
3714
|
+
const px1 = Math.max(px0, x1 - r);
|
|
3715
|
+
const py1 = Math.max(py0, y1 - b);
|
|
3716
|
+
// The left and the right are drawn after the top and the bottom. Where
|
|
3717
|
+
// one of them is opaque it draws its half of a cut corner over the side
|
|
3718
|
+
// drawn before, which keeps the whole corner: two halves each
|
|
3719
|
+
// antialiased along the diagonal would let what is under the border
|
|
3720
|
+
// show through between them.
|
|
3721
|
+
const over = (color) => color !== null && alphaOf(color) === 1;
|
|
3722
|
+
if (topInk !== null) {
|
|
3723
|
+
const cutLeft = tl && !over(leftInk);
|
|
3724
|
+
const cutRight = tr && !over(rightInk);
|
|
3725
|
+
if (cutLeft || cutRight) {
|
|
3726
|
+
side([x, y, x1, y, cutRight ? px1 : x1, py0, cutLeft ? px0 : x, py0], topInk);
|
|
3727
|
+
}
|
|
3728
|
+
else
|
|
3729
|
+
edge(0, topStyle, topInk);
|
|
2813
3730
|
}
|
|
2814
|
-
if (
|
|
2815
|
-
|
|
3731
|
+
if (bottomInk !== null) {
|
|
3732
|
+
const cutLeft = bl && !over(leftInk);
|
|
3733
|
+
const cutRight = br && !over(rightInk);
|
|
3734
|
+
if (cutLeft || cutRight) {
|
|
3735
|
+
side([x1, y1, x, y1, cutLeft ? px0 : x, py1, cutRight ? px1 : x1, py1], bottomInk);
|
|
3736
|
+
}
|
|
3737
|
+
else
|
|
3738
|
+
edge(2, bottomStyle, bottomInk);
|
|
2816
3739
|
}
|
|
2817
|
-
if (
|
|
2818
|
-
|
|
3740
|
+
if (leftInk !== null) {
|
|
3741
|
+
if (tl || bl) {
|
|
3742
|
+
side([x, bl ? y1 : py1, x, tl ? y : py0, px0, py0, px0, py1], leftInk);
|
|
3743
|
+
}
|
|
3744
|
+
else
|
|
3745
|
+
edge(3, leftStyle, leftInk);
|
|
2819
3746
|
}
|
|
2820
|
-
if (
|
|
2821
|
-
|
|
3747
|
+
if (rightInk !== null) {
|
|
3748
|
+
if (tr || br) {
|
|
3749
|
+
side([x1, tr ? y : py0, x1, br ? y1 : py1, px1, py1, px1, py0], rightInk);
|
|
3750
|
+
}
|
|
3751
|
+
else
|
|
3752
|
+
edge(1, rightStyle, rightInk);
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
/**
|
|
3756
|
+
* Whether the corner two sides meet at is cut on its diagonal, each side
|
|
3757
|
+
* taking its half (CSS Backgrounds 3, 4.3): where both have a width, they
|
|
3758
|
+
* are painted in different colours or only one is painted at all, and each
|
|
3759
|
+
* is solid or unpainted. Sides of one colour make the same corner either
|
|
3760
|
+
* way, and so does a corner of a single pixel, which is left to the side
|
|
3761
|
+
* that has it.
|
|
3762
|
+
*/
|
|
3763
|
+
function mitred(a, b, inkA, inkB, styleA, styleB) {
|
|
3764
|
+
if (!(a > 0 && b > 0) || (a <= 1 && b <= 1) || inkA === inkB)
|
|
3765
|
+
return false;
|
|
3766
|
+
return ((inkA === null || styleA === 'solid') &&
|
|
3767
|
+
(inkB === null || styleB === 'solid'));
|
|
3768
|
+
}
|
|
3769
|
+
/**
|
|
3770
|
+
* Fill a convex polygon, its corners as `x, y` pairs, cut to `area`: the
|
|
3771
|
+
* part of it near what is painted, which is what keeps a side thousands of
|
|
3772
|
+
* pixels long inside X's coordinates. Cut edge by edge rather than by
|
|
3773
|
+
* moving its corners in, which would turn a diagonal that crosses the
|
|
3774
|
+
* painted area: a slanted divider, `border-left: 100vw solid transparent`,
|
|
3775
|
+
* has a corner far outside it.
|
|
3776
|
+
*/
|
|
3777
|
+
function fillPolygon(ctx, points, area) {
|
|
3778
|
+
const bounds = [area.x, area.y, area.x + area.w, area.y + area.h];
|
|
3779
|
+
let cut = points;
|
|
3780
|
+
// each edge of the area in turn: what is inside it is kept, and an edge
|
|
3781
|
+
// of the polygon that crosses it ends on it (Sutherland–Hodgman)
|
|
3782
|
+
for (let edge = 0; edge < 4 && cut.length; edge += 1) {
|
|
3783
|
+
const axis = edge & 1;
|
|
3784
|
+
const bound = bounds[edge];
|
|
3785
|
+
const sign = edge < 2 ? 1 : -1;
|
|
3786
|
+
const from = cut;
|
|
3787
|
+
cut = [];
|
|
3788
|
+
for (let i = 0; i < from.length; i += 2) {
|
|
3789
|
+
const j = (i + 2) % from.length;
|
|
3790
|
+
const da = sign * (from[i + axis] - bound);
|
|
3791
|
+
const db = sign * (from[j + axis] - bound);
|
|
3792
|
+
if (da >= 0)
|
|
3793
|
+
cut.push(from[i], from[i + 1]);
|
|
3794
|
+
if (da < 0 !== db < 0) {
|
|
3795
|
+
const k = da / (da - db);
|
|
3796
|
+
const other = from[i + 1 - axis] + (from[j + 1 - axis] - from[i + 1 - axis]) * k;
|
|
3797
|
+
if (axis)
|
|
3798
|
+
cut.push(other, bound);
|
|
3799
|
+
else
|
|
3800
|
+
cut.push(bound, other);
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
2822
3803
|
}
|
|
3804
|
+
if (cut.length < 6)
|
|
3805
|
+
return;
|
|
3806
|
+
ctx.beginPath();
|
|
3807
|
+
ctx.moveTo(cut[0], cut[1]);
|
|
3808
|
+
for (let i = 2; i < cut.length; i += 2)
|
|
3809
|
+
ctx.lineTo(cut[i], cut[i + 1]);
|
|
3810
|
+
ctx.closePath?.();
|
|
3811
|
+
ctx.fill();
|
|
2823
3812
|
}
|
|
2824
3813
|
/**
|
|
2825
3814
|
* A box's border image in place of its border's style (CSS Backgrounds 3,
|
|
@@ -2871,12 +3860,14 @@ function paintBorderImage(ctx, box, options) {
|
|
|
2871
3860
|
const svg = image instanceof SvgDrawing ? image : null;
|
|
2872
3861
|
const w = Math.max(1, Math.round(cw));
|
|
2873
3862
|
const h = Math.max(1, Math.round(ch));
|
|
2874
|
-
const
|
|
3863
|
+
const scheme = box.style.colorScheme;
|
|
3864
|
+
const drawn = options.cached?.(`border-image|${key}|${w}x${h}|${scheme}`, w, h, (sctx) => {
|
|
2875
3865
|
if (svg)
|
|
2876
|
-
svg.
|
|
2877
|
-
else if (typeof source !== 'string'
|
|
2878
|
-
|
|
2879
|
-
|
|
3866
|
+
svg.drawImage(sctx, 0, 0, w, h, scale, scheme);
|
|
3867
|
+
else if (typeof source !== 'string') {
|
|
3868
|
+
const paint = gradientFill(sctx, source, 0, 0, w, h, box.style.color);
|
|
3869
|
+
if (paint)
|
|
3870
|
+
fillGradient(sctx, paint, { x: 0, y: 0, w, h });
|
|
2880
3871
|
}
|
|
2881
3872
|
});
|
|
2882
3873
|
if (!drawn)
|
|
@@ -3074,22 +4065,107 @@ function roundedRing(ctx, box, x, y, w, h, options) {
|
|
|
3074
4065
|
return true;
|
|
3075
4066
|
if (isTransparent(color))
|
|
3076
4067
|
return true;
|
|
3077
|
-
const
|
|
3078
|
-
if (!
|
|
4068
|
+
const ring = cutRing(options, x, y, w, h, corners, box.borderTop, box.borderRight, box.borderBottom, box.borderLeft);
|
|
4069
|
+
if (!ring)
|
|
3079
4070
|
return true;
|
|
3080
|
-
const
|
|
3081
|
-
const right = box.borderRight;
|
|
3082
|
-
const bottom = box.borderBottom;
|
|
3083
|
-
const left = box.borderLeft;
|
|
4071
|
+
const { outer, inner } = ring;
|
|
3084
4072
|
ctx.fillStyle = inkColor(color, s.color);
|
|
3085
|
-
fillRing(ctx, { x:
|
|
3086
|
-
x: rect.x + left,
|
|
3087
|
-
y: rect.y + top,
|
|
3088
|
-
width: Math.max(0, rect.w - left - right),
|
|
3089
|
-
height: Math.max(0, rect.h - top - bottom),
|
|
3090
|
-
}, insetCorners(corners, top, right, bottom, left));
|
|
4073
|
+
fillRing(ctx, outer.rect, outer.corners, inner ? inner.rect : { x: 0, y: 0, width: 0, height: 0 }, inner ? inner.corners : outer.corners);
|
|
3091
4074
|
return true;
|
|
3092
4075
|
}
|
|
4076
|
+
/**
|
|
4077
|
+
* A rounded box's ring — its border edge and its padding edge, the box
|
|
4078
|
+
* inside its borders — cut to the neighbourhood of what is being painted,
|
|
4079
|
+
* as `clampRect` cuts a rectangle. Both edges are the box's own, cut to
|
|
4080
|
+
* one window, so what is left is the part of the true ring in it. Cutting
|
|
4081
|
+
* the box first and insetting the cut by the borders put the hole a
|
|
4082
|
+
* border's width in from the cut rather than from the box's edge, and left
|
|
4083
|
+
* none where a border was wider than `CLAMP_PAD`: the ring filled whatever
|
|
4084
|
+
* of the box a paint reached. A cut is straight, so a side of the window
|
|
4085
|
+
* is moved out past any corner's curve it would cross, and a corner on a
|
|
4086
|
+
* side the window cut is square, its curve being outside. Null where the
|
|
4087
|
+
* ring has nothing in the window.
|
|
4088
|
+
*/
|
|
4089
|
+
function cutRing(options, x, y, w, h, corners, top, right, bottom, left) {
|
|
4090
|
+
const { x: cx, y: cy } = corners;
|
|
4091
|
+
// where each corner's curve runs, across and down — the padding edge's
|
|
4092
|
+
// run inside the border edge's
|
|
4093
|
+
const across = [
|
|
4094
|
+
[x, x + cx[0]],
|
|
4095
|
+
[x + w - cx[1], x + w],
|
|
4096
|
+
[x + w - cx[2], x + w],
|
|
4097
|
+
[x, x + cx[3]],
|
|
4098
|
+
];
|
|
4099
|
+
const down = [
|
|
4100
|
+
[y, y + cy[0]],
|
|
4101
|
+
[y, y + cy[1]],
|
|
4102
|
+
[y + h - cy[2], y + h],
|
|
4103
|
+
[y + h - cy[3], y + h],
|
|
4104
|
+
];
|
|
4105
|
+
const d = options.damage;
|
|
4106
|
+
const side = (at, spans, towards) => Math.max(-COORD_LIMIT, Math.min(COORD_LIMIT, outOfSpans(at, spans, towards)));
|
|
4107
|
+
const x0 = side(d ? d.x - CLAMP_PAD : -COORD_LIMIT, across, -1);
|
|
4108
|
+
const y0 = side(d ? d.y - CLAMP_PAD : -COORD_LIMIT, down, -1);
|
|
4109
|
+
const x1 = side(d ? d.x + d.width + CLAMP_PAD : COORD_LIMIT, across, 1);
|
|
4110
|
+
const y1 = side(d ? d.y + d.height + CLAMP_PAD : COORD_LIMIT, down, 1);
|
|
4111
|
+
const cut = (rx, ry, rw, rh, c) => {
|
|
4112
|
+
const l = Math.max(rx, x0);
|
|
4113
|
+
const t = Math.max(ry, y0);
|
|
4114
|
+
const r = Math.min(rx + rw, x1);
|
|
4115
|
+
const b = Math.min(ry + rh, y1);
|
|
4116
|
+
if (r <= l || b <= t)
|
|
4117
|
+
return null;
|
|
4118
|
+
// the corners of the sides the window left where they were
|
|
4119
|
+
const kept = [
|
|
4120
|
+
t === ry && l === rx,
|
|
4121
|
+
t === ry && r === rx + rw,
|
|
4122
|
+
b === ry + rh && r === rx + rw,
|
|
4123
|
+
b === ry + rh && l === rx,
|
|
4124
|
+
];
|
|
4125
|
+
return {
|
|
4126
|
+
rect: { x: l, y: t, width: r - l, height: b - t },
|
|
4127
|
+
corners: {
|
|
4128
|
+
x: c.x.map((v, i) => (kept[i] ? v : 0)),
|
|
4129
|
+
y: c.y.map((v, i) => (kept[i] ? v : 0)),
|
|
4130
|
+
},
|
|
4131
|
+
};
|
|
4132
|
+
};
|
|
4133
|
+
const outer = cut(x, y, w, h, corners);
|
|
4134
|
+
if (!outer)
|
|
4135
|
+
return null;
|
|
4136
|
+
const iw = w - left - right;
|
|
4137
|
+
const ih = h - top - bottom;
|
|
4138
|
+
const inner = iw > 0 && ih > 0
|
|
4139
|
+
? cut(x + left, y + top, iw, ih, insetCorners(corners, top, right, bottom, left))
|
|
4140
|
+
: null;
|
|
4141
|
+
// a window inside the padding edge, where the ring has nothing
|
|
4142
|
+
if (inner && sameRounded(outer, inner))
|
|
4143
|
+
return null;
|
|
4144
|
+
return { outer, inner };
|
|
4145
|
+
}
|
|
4146
|
+
/** A position moved out of any of `spans` it is inside, towards their
|
|
4147
|
+
* starts where `towards` is negative and their ends where it is not. */
|
|
4148
|
+
function outOfSpans(at, spans, towards) {
|
|
4149
|
+
for (let moved = true; moved;) {
|
|
4150
|
+
moved = false;
|
|
4151
|
+
for (const [from, to] of spans) {
|
|
4152
|
+
if (at > from && at < to) {
|
|
4153
|
+
at = towards < 0 ? from : to;
|
|
4154
|
+
moved = true;
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
return at;
|
|
4159
|
+
}
|
|
4160
|
+
/** Whether two rounded rectangles are one shape. */
|
|
4161
|
+
function sameRounded(a, b) {
|
|
4162
|
+
return (a.rect.x === b.rect.x &&
|
|
4163
|
+
a.rect.y === b.rect.y &&
|
|
4164
|
+
a.rect.width === b.rect.width &&
|
|
4165
|
+
a.rect.height === b.rect.height &&
|
|
4166
|
+
a.corners.x.every((v, i) => v === b.corners.x[i]) &&
|
|
4167
|
+
a.corners.y.every((v, i) => v === b.corners.y[i]));
|
|
4168
|
+
}
|
|
3093
4169
|
/**
|
|
3094
4170
|
* Clip to what a box's border paints, for a layer of `background-clip:
|
|
3095
4171
|
* border-area` (CSS Backgrounds 4, 2.1): each side's width and style, and
|
|
@@ -3097,12 +4173,17 @@ function roundedRing(ctx, box, x, y, w, h, options) {
|
|
|
3097
4173
|
* the value is for. The shapes are the border painter's own, so that the
|
|
3098
4174
|
* background shows exactly where that border would: the ring `roundedRing`
|
|
3099
4175
|
* fills where a rounded border is solid, the ring the trapezoids of a 3D
|
|
3100
|
-
* style make, and otherwise the rectangles `
|
|
3101
|
-
* time — the dots, the dashes, the two lines of a double border
|
|
4176
|
+
* style make, and otherwise the rectangles `fillSide` fills a side at a
|
|
4177
|
+
* time — the dots, the dashes, the two lines of a double border joined as
|
|
4178
|
+
* they are painted, by the sides' colours though neither is drawn. All of
|
|
3102
4179
|
* them run clockwise and a ring's inside the other way, so the clip is the
|
|
3103
|
-
* union by the non-zero rule, the one every context's `clip` takes.
|
|
3104
|
-
*
|
|
3105
|
-
*
|
|
4180
|
+
* union by the non-zero rule, the one every context's `clip` takes. Each
|
|
4181
|
+
* is placed from the box's edges and then cut to the painted area: placed
|
|
4182
|
+
* from the cut, a border wider than `CLAMP_PAD` put its bands and its
|
|
4183
|
+
* ring's hole a border's width in from wherever the paint was cut, and
|
|
4184
|
+
* the background was painted over the content of a box repainted in part.
|
|
4185
|
+
* False where the border paints nothing in the painted area or the context
|
|
4186
|
+
* cannot clip, and nothing is pushed.
|
|
3106
4187
|
*/
|
|
3107
4188
|
function pushBorderArea(ctx, box, options) {
|
|
3108
4189
|
const t = box.borderTop;
|
|
@@ -3121,44 +4202,53 @@ function pushBorderArea(ctx, box, options) {
|
|
|
3121
4202
|
const h = Math.round(top + frameHeight(box)) - y;
|
|
3122
4203
|
if (w <= 0 || h <= 0)
|
|
3123
4204
|
return false;
|
|
3124
|
-
|
|
3125
|
-
if (!area)
|
|
4205
|
+
if (!clampRect(options, x, y, w, h))
|
|
3126
4206
|
return false;
|
|
3127
4207
|
const corners = cornersOf(s, w, h);
|
|
3128
4208
|
ctx.save();
|
|
3129
4209
|
ctx.beginPath();
|
|
4210
|
+
// how many shapes the path has: none, and the border paints nothing here
|
|
4211
|
+
let shapes = 0;
|
|
3130
4212
|
if (corners && canCurve(ctx) && solidBorder(box)) {
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
4213
|
+
const ring = cutRing(options, x, y, w, h, corners, t, r, b, l);
|
|
4214
|
+
if (ring) {
|
|
4215
|
+
const add = ({ rect, corners: c }, hole) => roundedRect(ctx, rect.x, rect.y, rect.width, rect.height, c, true, hole);
|
|
4216
|
+
add(ring.outer, false);
|
|
4217
|
+
// the padding edge run the other way, which cuts it out
|
|
4218
|
+
if (ring.inner)
|
|
4219
|
+
add(ring.inner, true);
|
|
4220
|
+
shapes = 1;
|
|
3137
4221
|
}
|
|
3138
4222
|
}
|
|
3139
|
-
else if (sculpted(s)) {
|
|
3140
|
-
// a band a side, which the trapezoids cover between them
|
|
3141
|
-
ctx.rect(area.x, area.y, area.w, t);
|
|
3142
|
-
ctx.rect(area.x, area.y + area.h - b, area.w, b);
|
|
3143
|
-
ctx.rect(area.x, area.y + t, l, area.h - t - b);
|
|
3144
|
-
ctx.rect(area.x + area.w - r, area.y + t, r, area.h - t - b);
|
|
3145
|
-
}
|
|
3146
4223
|
else {
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
4224
|
+
// A side at a time, from the box's edges and cut to the painted area
|
|
4225
|
+
// after: a 3D side as a band, which the trapezoids cover between them,
|
|
4226
|
+
// and any other as `fillSide` fills it.
|
|
4227
|
+
const path = {
|
|
4228
|
+
fillRect(ex, ey, ew, eh) {
|
|
4229
|
+
ctx.rect(ex, ey, ew, eh);
|
|
4230
|
+
shapes += 1;
|
|
4231
|
+
},
|
|
3152
4232
|
};
|
|
3153
|
-
const
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
4233
|
+
const widths = [t, r, b, l];
|
|
4234
|
+
const joins = doubleJoins(box);
|
|
4235
|
+
const styles = sculpted(s)
|
|
4236
|
+
? ['solid', 'solid', 'solid', 'solid']
|
|
4237
|
+
: [
|
|
4238
|
+
s.borderTopStyle,
|
|
4239
|
+
s.borderRightStyle,
|
|
4240
|
+
s.borderBottomStyle,
|
|
4241
|
+
s.borderLeftStyle,
|
|
4242
|
+
];
|
|
4243
|
+
for (let at = 0; at < 4; at += 1) {
|
|
4244
|
+
if (widths[at] > 0) {
|
|
4245
|
+
fillSide(path, options, at, x, y, w, h, widths, styles[at], joins);
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
if (!shapes) {
|
|
4250
|
+
ctx.restore();
|
|
4251
|
+
return false;
|
|
3162
4252
|
}
|
|
3163
4253
|
ctx.clip();
|
|
3164
4254
|
return true;
|
|
@@ -3187,10 +4277,12 @@ function sculpted(s) {
|
|
|
3187
4277
|
* colour's shadow and `outset` its bottom and right; `groove` is `inset`
|
|
3188
4278
|
* outside `outset`, a band each, and `ridge` the other way round
|
|
3189
4279
|
* (`borderShades`). A side of any other style takes its trapezoid in its
|
|
3190
|
-
* colour. The trapezoids are
|
|
3191
|
-
*
|
|
3192
|
-
*
|
|
3193
|
-
*
|
|
4280
|
+
* colour. The trapezoids are cut to the painted area (`fillPolygon`), which
|
|
4281
|
+
* keeps a long box's far corners out of X's coordinates and a join on its
|
|
4282
|
+
* diagonal: its corners clamped in, a border wider than `CLAMP_PAD` turned
|
|
4283
|
+
* the join to cross the damage at 45° wherever it was, and a scrolled
|
|
4284
|
+
* strip smeared the corners of a thick one. False where the context cannot
|
|
4285
|
+
* draw a path, and the sides are drawn straight.
|
|
3194
4286
|
*/
|
|
3195
4287
|
function paintSculpted(ctx, box, x, y, w, h, options) {
|
|
3196
4288
|
if (!ctx.beginPath || !ctx.moveTo || !ctx.lineTo || !ctx.fill)
|
|
@@ -3203,8 +4295,6 @@ function paintSculpted(ctx, box, x, y, w, h, options) {
|
|
|
3203
4295
|
const r = box.borderRight;
|
|
3204
4296
|
const b = box.borderBottom;
|
|
3205
4297
|
const l = box.borderLeft;
|
|
3206
|
-
const cx = (v) => Math.max(area.x - 1, Math.min(v, area.x + area.w + 1));
|
|
3207
|
-
const cy = (v) => Math.max(area.y - 1, Math.min(v, area.y + area.h + 1));
|
|
3208
4298
|
/** The band between the edge `outer` of the way in and `inner`, for one
|
|
3209
4299
|
* side: 0 is the border box, 1 the padding box. */
|
|
3210
4300
|
const band = (side, outer, inner, color) => {
|
|
@@ -3224,12 +4314,7 @@ function paintSculpted(ctx, box, x, y, w, h, options) {
|
|
|
3224
4314
|
? [o.x1, o.y1, o.x0, o.y1, i.x0, i.y1, i.x1, i.y1]
|
|
3225
4315
|
: [o.x0, o.y1, o.x0, o.y0, i.x0, i.y0, i.x0, i.y1];
|
|
3226
4316
|
ctx.fillStyle = color;
|
|
3227
|
-
ctx
|
|
3228
|
-
ctx.moveTo(cx(points[0]), cy(points[1]));
|
|
3229
|
-
for (let k = 2; k < 8; k += 2)
|
|
3230
|
-
ctx.lineTo(cx(points[k]), cy(points[k + 1]));
|
|
3231
|
-
ctx.closePath?.();
|
|
3232
|
-
ctx.fill();
|
|
4317
|
+
fillPolygon(ctx, points, area);
|
|
3233
4318
|
};
|
|
3234
4319
|
const sides = [
|
|
3235
4320
|
[t, s.borderTopStyle, s.borderTopColor],
|
|
@@ -3341,6 +4426,91 @@ function paintCollapsedBorders(ctx, table, options) {
|
|
|
3341
4426
|
fillEdge(ctx, rect, s.horizontal ? s.x : s.y, s.border.style, s.horizontal);
|
|
3342
4427
|
}
|
|
3343
4428
|
}
|
|
4429
|
+
/**
|
|
4430
|
+
* One side of a border box at `x, y`, `w` by `h` — `at` counts the top
|
|
4431
|
+
* first and clockwise — as rectangles cut to what the paint reaches: the
|
|
4432
|
+
* top and the bottom the width of the box and the left and the right
|
|
4433
|
+
* between them, so a corner is a square of one side or the other.
|
|
4434
|
+
*
|
|
4435
|
+
* A double side is two lines, a band each along its outer and its inner
|
|
4436
|
+
* edge. Where it meets another double side of its colour (`joins`, the
|
|
4437
|
+
* top left corner first) the lines of the two meet as two frames do, one
|
|
4438
|
+
* inside the other (CSS Backgrounds 3, 4.2 and 4.3): the outer line of the
|
|
4439
|
+
* left or the right runs on to the outer line of the top or the bottom,
|
|
4440
|
+
* and the inner line of the top or the bottom stops at the inner line of
|
|
4441
|
+
* the side. With square corners the outer frame was open at each of them
|
|
4442
|
+
* and the inner line of the top ran on past the side's to the border edge.
|
|
4443
|
+
* Each line is cut to the paint on its own, so a border wider than the
|
|
4444
|
+
* reach around the paint keeps its lines at its edges, and not at the
|
|
4445
|
+
* edges of the part of it cut out.
|
|
4446
|
+
*/
|
|
4447
|
+
function fillSide(ctx, options, at, x, y, w, h, widths, style, joins) {
|
|
4448
|
+
const [t, r, b, l] = widths;
|
|
4449
|
+
const width = widths[at];
|
|
4450
|
+
const horizontal = (at & 1) === 0;
|
|
4451
|
+
if (style === 'double' && width >= 3) {
|
|
4452
|
+
const band = doubleBand(width);
|
|
4453
|
+
const line = (lx, ly, lw, lh) => {
|
|
4454
|
+
const rect = clampRect(options, lx, ly, lw, lh);
|
|
4455
|
+
if (rect)
|
|
4456
|
+
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
4457
|
+
};
|
|
4458
|
+
if (horizontal) {
|
|
4459
|
+
// the outer line the width of the box, and the inner one from the
|
|
4460
|
+
// inner line of a side it joins, or from the edge of the box
|
|
4461
|
+
const from = joins[at === 0 ? 0 : 3] ? x + l - doubleBand(l) : x;
|
|
4462
|
+
const to = joins[at === 0 ? 1 : 2] ? x + w - r + doubleBand(r) : x + w;
|
|
4463
|
+
line(x, at === 0 ? y : y + h - band, w, band);
|
|
4464
|
+
line(from, at === 0 ? y + t - band : y + h - b, to - from, band);
|
|
4465
|
+
}
|
|
4466
|
+
else {
|
|
4467
|
+
// the outer line from the outer line of a side it joins, or from
|
|
4468
|
+
// that side's inner edge, and the inner one between the two sides
|
|
4469
|
+
const from = joins[at === 3 ? 0 : 1] ? y + doubleBand(t) : y + t;
|
|
4470
|
+
const to = joins[at === 3 ? 3 : 2] ? y + h - doubleBand(b) : y + h - b;
|
|
4471
|
+
line(at === 3 ? x : x + w - band, from, band, to - from);
|
|
4472
|
+
line(at === 3 ? x + l - band : x + w - r, y + t, band, h - t - b);
|
|
4473
|
+
}
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4476
|
+
const ex = at === 1 ? x + w - r : x;
|
|
4477
|
+
const ey = at === 0 ? y : at === 2 ? y + h - b : y + t;
|
|
4478
|
+
const rect = horizontal
|
|
4479
|
+
? clampRect(options, ex, ey, w, width)
|
|
4480
|
+
: clampRect(options, ex, ey, width, h - t - b);
|
|
4481
|
+
// The un-clamped start is the dash phase's origin, so the pattern does
|
|
4482
|
+
// not crawl as the viewport moves along a long edge.
|
|
4483
|
+
if (rect)
|
|
4484
|
+
fillEdge(ctx, rect, horizontal ? ex : ey, style, horizontal);
|
|
4485
|
+
}
|
|
4486
|
+
/**
|
|
4487
|
+
* The corners of a box where two double sides of one colour meet, the top
|
|
4488
|
+
* left first and clockwise, and whose lines `fillSide` joins. By the
|
|
4489
|
+
* colours as written, a transparent one included, which is how a clip to
|
|
4490
|
+
* the border's area draws the border it does not paint.
|
|
4491
|
+
*/
|
|
4492
|
+
function doubleJoins(box) {
|
|
4493
|
+
const s = box.style;
|
|
4494
|
+
const color = (width, style, value) => style === 'double' && width >= 3 ? inkColor(value, s.color) : null;
|
|
4495
|
+
const top = color(box.borderTop, s.borderTopStyle, s.borderTopColor);
|
|
4496
|
+
const bottom = color(box.borderBottom, s.borderBottomStyle, s.borderBottomColor);
|
|
4497
|
+
if (top === null && bottom === null)
|
|
4498
|
+
return UNJOINED;
|
|
4499
|
+
const right = color(box.borderRight, s.borderRightStyle, s.borderRightColor);
|
|
4500
|
+
const left = color(box.borderLeft, s.borderLeftStyle, s.borderLeftColor);
|
|
4501
|
+
return [
|
|
4502
|
+
top !== null && top === left,
|
|
4503
|
+
top !== null && top === right,
|
|
4504
|
+
bottom !== null && bottom === right,
|
|
4505
|
+
bottom !== null && bottom === left,
|
|
4506
|
+
];
|
|
4507
|
+
}
|
|
4508
|
+
const UNJOINED = [false, false, false, false];
|
|
4509
|
+
/** How wide each of a double border's two lines is, the gap between them
|
|
4510
|
+
* what is left of the side. */
|
|
4511
|
+
function doubleBand(thickness) {
|
|
4512
|
+
return Math.max(1, Math.floor(thickness / 3));
|
|
4513
|
+
}
|
|
3344
4514
|
function fillEdge(ctx, rect, phaseOrigin, style, horizontal) {
|
|
3345
4515
|
const { x, y, w, h } = rect;
|
|
3346
4516
|
const length = horizontal ? w : h;
|
|
@@ -3367,7 +4537,7 @@ function fillEdge(ctx, rect, phaseOrigin, style, horizontal) {
|
|
|
3367
4537
|
return;
|
|
3368
4538
|
}
|
|
3369
4539
|
if (style === 'double' && thickness >= 3) {
|
|
3370
|
-
const band =
|
|
4540
|
+
const band = doubleBand(thickness);
|
|
3371
4541
|
if (horizontal) {
|
|
3372
4542
|
ctx.fillRect(x, y, length, band);
|
|
3373
4543
|
ctx.fillRect(x, y + thickness - band, length, band);
|
|
@@ -3381,7 +4551,9 @@ function fillEdge(ctx, rect, phaseOrigin, style, horizontal) {
|
|
|
3381
4551
|
ctx.fillRect(x, y, w, h);
|
|
3382
4552
|
}
|
|
3383
4553
|
/** A list item's bullet or number, in the margin. */
|
|
3384
|
-
function paintMarker(ctx, marker, options
|
|
4554
|
+
function paintMarker(ctx, marker, options,
|
|
4555
|
+
/** The item's colour scheme, which an SVG image in it is drawn in. */
|
|
4556
|
+
scheme) {
|
|
3385
4557
|
const x = marker.x + options.originX;
|
|
3386
4558
|
const y = marker.y + options.originY;
|
|
3387
4559
|
if (marker.image) {
|
|
@@ -3391,7 +4563,7 @@ function paintMarker(ctx, marker, options) {
|
|
|
3391
4563
|
if (!loaded || !(width > 0 && height > 0))
|
|
3392
4564
|
return;
|
|
3393
4565
|
if (loaded.image instanceof SvgDrawing) {
|
|
3394
|
-
loaded.image.
|
|
4566
|
+
loaded.image.drawImage(ctx, x, y, width, height, options.scale ?? 1, scheme);
|
|
3395
4567
|
}
|
|
3396
4568
|
else
|
|
3397
4569
|
ctx.drawImage?.(loaded.image, x, y, width, height);
|
|
@@ -3423,7 +4595,7 @@ function paintImage(ctx, box, options) {
|
|
|
3423
4595
|
const past = at.x < x || at.y < y || at.x + at.w > x + w || at.y + at.h > y + h;
|
|
3424
4596
|
const clipped = (!!corners || past) && pushClip(ctx, { x, y, w, h }, corners);
|
|
3425
4597
|
if (image instanceof SvgDrawing) {
|
|
3426
|
-
image.
|
|
4598
|
+
image.drawImage(ctx, at.x, at.y, at.w, at.h, options.scale ?? 1, box.style.colorScheme);
|
|
3427
4599
|
}
|
|
3428
4600
|
else
|
|
3429
4601
|
ctx.drawImage(image, at.x, at.y, at.w, at.h);
|
|
@@ -3509,7 +4681,10 @@ function fitted(box, x, y, w, h) {
|
|
|
3509
4681
|
};
|
|
3510
4682
|
}
|
|
3511
4683
|
/** An inline `<svg>`, drawn in its content box. Its `currentColor` is the
|
|
3512
|
-
* box's `color`, as an icon's is the text's around it
|
|
4684
|
+
* box's `color`, as an icon's is the text's around it, and what it is
|
|
4685
|
+
* painted with the box's `fill` and `stroke`, where the document's style
|
|
4686
|
+
* sheets set them: an icon set's `.icon { fill: currentColor }`. What
|
|
4687
|
+
* they give a shape inside it is asked for here (`BoxTree.shapeStyler`). */
|
|
3513
4688
|
function paintSvg(ctx, box, options) {
|
|
3514
4689
|
if (!box.el)
|
|
3515
4690
|
return;
|
|
@@ -3517,7 +4692,11 @@ function paintSvg(ctx, box, options) {
|
|
|
3517
4692
|
const y = Math.round(box.contentY + options.originY);
|
|
3518
4693
|
const w = Math.round(box.contentX + options.originX + box.contentWidth) - x;
|
|
3519
4694
|
const h = Math.round(box.contentY + options.originY + box.contentHeight) - y;
|
|
3520
|
-
|
|
4695
|
+
const style = box.style;
|
|
4696
|
+
const paint = (value) => value === null || value === 'currentColor' || value === 'none'
|
|
4697
|
+
? value
|
|
4698
|
+
: inkColor(value, style.color);
|
|
4699
|
+
inlineDrawing(box.el).draw(ctx, x, y, w, h, options.scale ?? 1, style.color, paint(style.fill), paint(style.stroke), options.shapeStyler?.(box) ?? null);
|
|
3521
4700
|
}
|
|
3522
4701
|
/**
|
|
3523
4702
|
* The X protocol carries glyph positions as Int16, so anything drawn past
|
|
@@ -3557,7 +4736,9 @@ function paintLines(ctx, box, options) {
|
|
|
3557
4736
|
const above = ink?.above ?? 0;
|
|
3558
4737
|
const below = ink?.below ?? 0;
|
|
3559
4738
|
const slack = top - box.maxLineHeight - below;
|
|
3560
|
-
|
|
4739
|
+
// the lines columns took apart are in no order down the page
|
|
4740
|
+
const apart = columned.any && COLUMN_LINES.has(lines);
|
|
4741
|
+
while (!apart && lo < hi) {
|
|
3561
4742
|
const mid = (lo + hi) >> 1;
|
|
3562
4743
|
if (lines[mid].y > slack)
|
|
3563
4744
|
hi = mid;
|
|
@@ -3566,8 +4747,11 @@ function paintLines(ctx, box, options) {
|
|
|
3566
4747
|
}
|
|
3567
4748
|
for (let i = lo; i < lines.length; i += 1) {
|
|
3568
4749
|
const line = lines[i];
|
|
3569
|
-
if (line.y - above >= bottom)
|
|
4750
|
+
if (line.y - above >= bottom) {
|
|
4751
|
+
if (apart)
|
|
4752
|
+
continue;
|
|
3570
4753
|
break;
|
|
4754
|
+
}
|
|
3571
4755
|
if (line.y < bottom && line.y + line.height > top)
|
|
3572
4756
|
visible.push(line);
|
|
3573
4757
|
else if (ink && inkInto(line, top, bottom)) {
|
|
@@ -3601,8 +4785,13 @@ function paintLines(ctx, box, options) {
|
|
|
3601
4785
|
paintInlineBoxes(ctx, line, lines, options, line === lines[0] ? null : bleeds);
|
|
3602
4786
|
for (const text of line.texts) {
|
|
3603
4787
|
const natural = text.layout.lines[text.layoutLine];
|
|
3604
|
-
if (natural)
|
|
3605
|
-
|
|
4788
|
+
if (!natural)
|
|
4789
|
+
continue;
|
|
4790
|
+
for (const laid of [natural, trailOf(text, natural)]) {
|
|
4791
|
+
if (!laid)
|
|
4792
|
+
continue;
|
|
4793
|
+
paintRunBackgrounds(ctx, laid, text.drawX + dx, text.drawY + dy, options.scale ?? 1);
|
|
4794
|
+
}
|
|
3606
4795
|
}
|
|
3607
4796
|
paintSelection(ctx, line, options);
|
|
3608
4797
|
}
|
|
@@ -3623,9 +4812,11 @@ function paintLines(ctx, box, options) {
|
|
|
3623
4812
|
: null;
|
|
3624
4813
|
for (const line of visible) {
|
|
3625
4814
|
for (const text of line.texts) {
|
|
3626
|
-
|
|
4815
|
+
// once for each column its lines are in, where they are in several
|
|
4816
|
+
const rows = columned.any ? COLUMN_ROWS.get(text) : undefined;
|
|
4817
|
+
if (drawn.has(rows ?? text.layout))
|
|
3627
4818
|
continue;
|
|
3628
|
-
drawn.add(text.layout);
|
|
4819
|
+
drawn.add(rows ?? text.layout);
|
|
3629
4820
|
const top = text.drawY + dy;
|
|
3630
4821
|
if (top < -COORD_LIMIT || top + text.layout.height > COORD_LIMIT) {
|
|
3631
4822
|
// A single layout so tall its own lines overflow the Int16 envelope
|
|
@@ -3636,10 +4827,13 @@ function paintLines(ctx, box, options) {
|
|
|
3636
4827
|
continue;
|
|
3637
4828
|
}
|
|
3638
4829
|
const bands = recolored?.get(text.layout);
|
|
4830
|
+
const clipped = rows !== undefined && clipToRows(ctx, text, rows, text.drawX + dx, top);
|
|
3639
4831
|
if (bands)
|
|
3640
4832
|
drawRecolored(ctx, text.layout, text.drawX + dx, top, bands);
|
|
3641
4833
|
else
|
|
3642
4834
|
text.layout.draw(ctx, text.drawX + dx, top);
|
|
4835
|
+
if (clipped)
|
|
4836
|
+
ctx.restore();
|
|
3643
4837
|
}
|
|
3644
4838
|
}
|
|
3645
4839
|
paintRules(ctx, visible, dx, dy, options.scale ?? 1, 'over');
|
|
@@ -3653,7 +4847,10 @@ function paintLines(ctx, box, options) {
|
|
|
3653
4847
|
for (const placed of line.atomics) {
|
|
3654
4848
|
if (hoisted && HOISTED.has(placed.box))
|
|
3655
4849
|
continue;
|
|
3656
|
-
|
|
4850
|
+
if (turns(placed.box))
|
|
4851
|
+
paintTransformed(ctx, placed.box, options);
|
|
4852
|
+
else
|
|
4853
|
+
paintBox(ctx, placed.box, options);
|
|
3657
4854
|
}
|
|
3658
4855
|
}
|
|
3659
4856
|
}
|
|
@@ -3797,7 +4994,9 @@ function paintClippedText(ctx, block, lines, options) {
|
|
|
3797
4994
|
(style.backgroundImages?.find((image) => image !== null && typeof image !== 'string') ||
|
|
3798
4995
|
null);
|
|
3799
4996
|
let fill = null;
|
|
3800
|
-
|
|
4997
|
+
// where the fill is a picture of the gradient, the corner it starts at
|
|
4998
|
+
let origin = null;
|
|
4999
|
+
if (gradient) {
|
|
3801
5000
|
if (!(area.width > 0 && area.height > 0))
|
|
3802
5001
|
continue;
|
|
3803
5002
|
// the text it shows through: what the fill is drawn over
|
|
@@ -3813,9 +5012,31 @@ function paintClippedText(ctx, block, lines, options) {
|
|
|
3813
5012
|
y1 = Math.max(y1, r.y + r.height);
|
|
3814
5013
|
}
|
|
3815
5014
|
}
|
|
3816
|
-
|
|
5015
|
+
const paint = gradientFill(ctx, gradient, area.x, area.y, area.width, area.height, style.color, x1 > x0 ? { x: x0, y: y0, w: x1 - x0, h: y1 - y0 } : null);
|
|
5016
|
+
if (paint?.matrix) {
|
|
5017
|
+
// An ellipse is a circle filled through a matrix, which would draw
|
|
5018
|
+
// the glyphs through it too: they are filled with a picture of the
|
|
5019
|
+
// gradient instead, drawn once at the area's size
|
|
5020
|
+
const left = Math.round(area.x);
|
|
5021
|
+
const top = Math.round(area.y);
|
|
5022
|
+
const w = Math.max(1, Math.round(area.x + area.width) - left);
|
|
5023
|
+
const h = Math.max(1, Math.round(area.y + area.height) - top);
|
|
5024
|
+
const picture = ctx.createPattern && ctx.translate
|
|
5025
|
+
? options.cached?.(`clip-text|${gradientKey(gradient, style.color)}|${w}x${h}`, w, h, (sctx) => {
|
|
5026
|
+
const p = gradientFill(sctx, gradient, 0, 0, w, h, style.color);
|
|
5027
|
+
if (p)
|
|
5028
|
+
fillGradient(sctx, p, { x: 0, y: 0, w, h });
|
|
5029
|
+
})
|
|
5030
|
+
: null;
|
|
5031
|
+
if (!picture)
|
|
5032
|
+
continue;
|
|
5033
|
+
fill = ctx.createPattern(picture, 'no-repeat');
|
|
5034
|
+
origin = { x: left, y: top };
|
|
5035
|
+
}
|
|
5036
|
+
else if (paint)
|
|
5037
|
+
fill = paint.style;
|
|
3817
5038
|
}
|
|
3818
|
-
|
|
5039
|
+
if (fill === null && !isTransparent(style.backgroundColor)) {
|
|
3819
5040
|
fill = inkColor(style.backgroundColor, style.color);
|
|
3820
5041
|
}
|
|
3821
5042
|
if (fill === null)
|
|
@@ -3830,7 +5051,13 @@ function paintClippedText(ctx, block, lines, options) {
|
|
|
3830
5051
|
ctx.rect(r.x, r.y, r.width, r.height);
|
|
3831
5052
|
ctx.clip();
|
|
3832
5053
|
ctx.fillStyle = fill;
|
|
3833
|
-
|
|
5054
|
+
if (origin) {
|
|
5055
|
+
// a pattern is placed from the origin of the space it fills
|
|
5056
|
+
ctx.translate(origin.x, origin.y);
|
|
5057
|
+
inkless.draw(ctx, part.x - origin.x, part.y - origin.y);
|
|
5058
|
+
}
|
|
5059
|
+
else
|
|
5060
|
+
inkless.draw(ctx, part.x, part.y);
|
|
3834
5061
|
ctx.restore();
|
|
3835
5062
|
}
|
|
3836
5063
|
}
|
|
@@ -3861,11 +5088,16 @@ function paintTextShadows(ctx, lines, options) {
|
|
|
3861
5088
|
continue;
|
|
3862
5089
|
const left = text.drawX + dx;
|
|
3863
5090
|
if (cast !== MIXED) {
|
|
3864
|
-
// every run casts the same: the layout's once for each shadow
|
|
3865
|
-
|
|
5091
|
+
// every run casts the same: the layout's once for each shadow, in
|
|
5092
|
+
// each column its lines are in
|
|
5093
|
+
const rows = columned.any ? COLUMN_ROWS.get(text) : undefined;
|
|
5094
|
+
if (whole.has(rows ?? layout))
|
|
3866
5095
|
continue;
|
|
3867
|
-
whole.add(layout);
|
|
5096
|
+
whole.add(rows ?? layout);
|
|
5097
|
+
const clipped = rows !== undefined && clipToRows(ctx, text, rows, left, top);
|
|
3868
5098
|
castShadows(ctx, text, left, top, cast, null);
|
|
5099
|
+
if (clipped)
|
|
5100
|
+
ctx.restore();
|
|
3869
5101
|
continue;
|
|
3870
5102
|
}
|
|
3871
5103
|
const natural = layout.lines[text.layoutLine];
|
|
@@ -3976,12 +5208,37 @@ function paintRules(ctx, lines, dx, dy, scale, rules) {
|
|
|
3976
5208
|
const shifted = SHIFTED_LINES.has(line);
|
|
3977
5209
|
for (const text of line.texts) {
|
|
3978
5210
|
const natural = text.layout.lines[text.layoutLine];
|
|
3979
|
-
if (natural)
|
|
3980
|
-
|
|
5211
|
+
if (!natural)
|
|
5212
|
+
continue;
|
|
5213
|
+
for (const laid of [natural, trailOf(text, natural)]) {
|
|
5214
|
+
if (!laid)
|
|
5215
|
+
continue;
|
|
5216
|
+
paintRunRules(ctx, laid, text.drawX + dx, text.drawY + dy + (shifted ? ruleDrop(text) : 0), scale, rules);
|
|
3981
5217
|
}
|
|
3982
5218
|
}
|
|
3983
5219
|
}
|
|
3984
5220
|
}
|
|
5221
|
+
/**
|
|
5222
|
+
* The spaces a text ends on where its line goes on after them
|
|
5223
|
+
* (`LineText.trail`), as a line of one run to draw the decorations of: the
|
|
5224
|
+
* run they are the end of, as wide as they are, after the line's content.
|
|
5225
|
+
* The engine stripped them from the piece it laid out as a line, and a
|
|
5226
|
+
* link's underline stopped at its last letter, a space short of where a
|
|
5227
|
+
* browser draws it. Null where the text has none, or reads right to left.
|
|
5228
|
+
*/
|
|
5229
|
+
function trailOf(text, natural) {
|
|
5230
|
+
if (!text.trail || !text.trailRun)
|
|
5231
|
+
return null;
|
|
5232
|
+
const last = natural.runs[natural.runs.length - 1];
|
|
5233
|
+
if (!last || last.run?.direction === 'rtl')
|
|
5234
|
+
return null;
|
|
5235
|
+
return {
|
|
5236
|
+
...natural,
|
|
5237
|
+
runs: [
|
|
5238
|
+
{ ...last, x: natural.width, width: text.trail, span: text.trailRun },
|
|
5239
|
+
],
|
|
5240
|
+
};
|
|
5241
|
+
}
|
|
3985
5242
|
/**
|
|
3986
5243
|
* How far below a raised text its rules go: to the baseline of the element
|
|
3987
5244
|
* that decorates it. `vertical-align` moves the text and not the lines an
|