ntk 8.21.0 → 8.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/widgets/svgview.js +403 -25
- package/package.json +1 -1
package/lib/widgets/svgview.js
CHANGED
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
// <use>, <switch>, linear/radial gradients (objectBoundingBox and
|
|
19
19
|
// userSpaceOnUse), transform lists, opacity/fill-opacity/stroke-opacity,
|
|
20
20
|
// display, visibility, nested <svg> viewports, fill-rule, <text> with its
|
|
21
|
-
// <tspan>s, and <
|
|
22
|
-
// limitations.
|
|
21
|
+
// <tspan>s, <mask> and <clipPath>. See docs/svg.md for the full surface
|
|
22
|
+
// and limitations.
|
|
23
23
|
|
|
24
24
|
import { parseDocument } from 'htmlparser2';
|
|
25
25
|
|
|
26
26
|
import { cssColorStraight } from '../color.js';
|
|
27
|
-
import { Path2D, flattenPath } from '../path.js';
|
|
27
|
+
import { MAT_IDENTITY, Path2D, flattenPath, matIsIdentity, matMultiply } from '../path.js';
|
|
28
28
|
|
|
29
29
|
const INHERITED = {
|
|
30
30
|
fill: '#000',
|
|
@@ -34,6 +34,7 @@ const INHERITED = {
|
|
|
34
34
|
lineJoin: 'miter',
|
|
35
35
|
miterLimit: 4,
|
|
36
36
|
fillRule: 'nonzero',
|
|
37
|
+
clipRule: 'nonzero',
|
|
37
38
|
fillOpacity: 1,
|
|
38
39
|
strokeOpacity: 1,
|
|
39
40
|
color: '#000',
|
|
@@ -61,6 +62,7 @@ const STYLE_ATTRS = {
|
|
|
61
62
|
'stroke-linejoin': 'lineJoin',
|
|
62
63
|
'stroke-miterlimit': 'miterLimit',
|
|
63
64
|
'fill-rule': 'fillRule',
|
|
65
|
+
'clip-rule': 'clipRule',
|
|
64
66
|
'fill-opacity': 'fillOpacity',
|
|
65
67
|
'stroke-opacity': 'strokeOpacity',
|
|
66
68
|
color: 'color',
|
|
@@ -145,7 +147,7 @@ function displayNone(node) {
|
|
|
145
147
|
// author's fallback is what to draw instead
|
|
146
148
|
const UNDRAWN_FEATURES = new Set(
|
|
147
149
|
[
|
|
148
|
-
'Extensibility', 'Image', '
|
|
150
|
+
'Extensibility', 'Image', 'Filter', 'BasicFilter',
|
|
149
151
|
'Pattern', 'Marker', 'Font', 'BasicFont', 'Script', 'Animation', 'ColorProfile',
|
|
150
152
|
'Cursor', 'View'
|
|
151
153
|
].map((name) => `http://www.w3.org/TR/SVG11/feature#${name}`)
|
|
@@ -540,6 +542,11 @@ function resolveStyle(node, parent) {
|
|
|
540
542
|
if (VISIBILITY.has(k)) value = k;
|
|
541
543
|
break;
|
|
542
544
|
}
|
|
545
|
+
case 'clipRule': {
|
|
546
|
+
const k = v.toLowerCase();
|
|
547
|
+
if (k === 'nonzero' || k === 'evenodd') value = k;
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
543
550
|
case 'fontSize':
|
|
544
551
|
value = fontSizeValue(v, parent.fontSize);
|
|
545
552
|
break;
|
|
@@ -684,7 +691,7 @@ function scanPaints(root, ids, languages) {
|
|
|
684
691
|
if (MARKABLE.has(name)) {
|
|
685
692
|
for (const id of markerIds(style)) {
|
|
686
693
|
const marker = ids.get(id);
|
|
687
|
-
if (marker && tag(marker) === 'marker' && !inMarker.has(marker)) {
|
|
694
|
+
if (marker && tag(marker) === 'marker' && !inMarker.has(marker) && !hiddenAbove(marker, root)) {
|
|
688
695
|
inMarker.add(marker);
|
|
689
696
|
kids(marker, styleWhereItStands(marker, root), depth);
|
|
690
697
|
inMarker.delete(marker);
|
|
@@ -704,8 +711,8 @@ function scanPaints(root, ids, languages) {
|
|
|
704
711
|
|
|
705
712
|
/**
|
|
706
713
|
* Widget rendering a static SVG document into a window (or any 2d context
|
|
707
|
-
* via `draw()`). Scripting, CSS stylesheets, filters
|
|
708
|
-
*
|
|
714
|
+
* via `draw()`). Scripting, CSS stylesheets, filters and external
|
|
715
|
+
* references are not supported — see docs/svg.md.
|
|
709
716
|
*/
|
|
710
717
|
export default class SvgView {
|
|
711
718
|
constructor(window, opts = {}) {
|
|
@@ -910,6 +917,13 @@ export default class SvgView {
|
|
|
910
917
|
*/
|
|
911
918
|
_ink(ctx, kind, value, style, path, alpha, draw) {
|
|
912
919
|
const key = kind === 'fill' ? 'fillStyle' : 'strokeStyle';
|
|
920
|
+
if (this._silhouetting) {
|
|
921
|
+
// a text in a clip path, which counts in its glyphs (`_silhouette`)
|
|
922
|
+
ctx[key] = '#000';
|
|
923
|
+
ctx.globalAlpha = 1;
|
|
924
|
+
draw();
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
913
927
|
if (!this._luminance) {
|
|
914
928
|
const paint = this._paint(ctx, value, style, path);
|
|
915
929
|
if (!paint) return;
|
|
@@ -1045,6 +1059,9 @@ export default class SvgView {
|
|
|
1045
1059
|
* `viewBox` is fitted in as `preserveAspectRatio` says, and what it draws
|
|
1046
1060
|
* is clipped to it unless its `overflow` is `visible` or `auto`. A zero
|
|
1047
1061
|
* or negative size, or a zero-sized `viewBox`, draws nothing.
|
|
1062
|
+
*
|
|
1063
|
+
* Its `clip-path` is set in the user space of what it holds, inside the
|
|
1064
|
+
* `viewBox`, as Chrome and WebKit set it.
|
|
1048
1065
|
*/
|
|
1049
1066
|
_renderViewport(node, ctx, style, alpha, depth) {
|
|
1050
1067
|
const outer = this._viewport;
|
|
@@ -1074,7 +1091,14 @@ export default class SvgView {
|
|
|
1074
1091
|
if (x || y) ctx.translate(x, y);
|
|
1075
1092
|
this._viewport = [w, h];
|
|
1076
1093
|
}
|
|
1077
|
-
this.
|
|
1094
|
+
const clip = this._clipOf(node);
|
|
1095
|
+
if (clip) {
|
|
1096
|
+
this._renderClipped(node, clip, ctx, style, alpha, depth, (c, a) =>
|
|
1097
|
+
this._renderChildren(node, c, style, a, depth + 1)
|
|
1098
|
+
);
|
|
1099
|
+
} else {
|
|
1100
|
+
this._renderChildren(node, ctx, style, alpha, depth + 1);
|
|
1101
|
+
}
|
|
1078
1102
|
this._viewport = outer;
|
|
1079
1103
|
ctx.restore();
|
|
1080
1104
|
}
|
|
@@ -1089,7 +1113,7 @@ export default class SvgView {
|
|
|
1089
1113
|
const nodeAlpha = alpha * (ownProperty(node, 'opacity', alphaValue) ?? 1);
|
|
1090
1114
|
if (nodeAlpha <= 0) return;
|
|
1091
1115
|
|
|
1092
|
-
const transforms =
|
|
1116
|
+
const transforms = ownTransforms(node);
|
|
1093
1117
|
const needsCtxState = transforms.length > 0;
|
|
1094
1118
|
if (needsCtxState) {
|
|
1095
1119
|
ctx.save();
|
|
@@ -1097,8 +1121,12 @@ export default class SvgView {
|
|
|
1097
1121
|
}
|
|
1098
1122
|
|
|
1099
1123
|
const mask = this._maskOf(node);
|
|
1100
|
-
|
|
1101
|
-
|
|
1124
|
+
const paint = (c, a) =>
|
|
1125
|
+
mask ? this._renderMasked(node, mask, c, style, a, depth) : this._renderElement(node, c, style, a, depth);
|
|
1126
|
+
// a nested <svg> is cut inside its viewport (`_renderViewport`)
|
|
1127
|
+
const clip = name === 'svg' ? null : this._clipOf(node);
|
|
1128
|
+
if (clip) this._renderClipped(node, clip, ctx, style, nodeAlpha, depth, paint);
|
|
1129
|
+
else paint(ctx, nodeAlpha);
|
|
1102
1130
|
|
|
1103
1131
|
if (needsCtxState) ctx.restore();
|
|
1104
1132
|
}
|
|
@@ -1122,15 +1150,11 @@ export default class SvgView {
|
|
|
1122
1150
|
break;
|
|
1123
1151
|
}
|
|
1124
1152
|
case 'use': {
|
|
1153
|
+
// its `x` and `y` are in its transforms (`ownTransforms`)
|
|
1125
1154
|
const target = this._useTarget(node);
|
|
1126
1155
|
if (target) {
|
|
1127
|
-
const ux = attrNum(node, 'x');
|
|
1128
|
-
const uy = attrNum(node, 'y');
|
|
1129
|
-
ctx.save();
|
|
1130
|
-
if (ux || uy) ctx.translate(ux, uy);
|
|
1131
1156
|
if (tag(target) === 'symbol') this._renderChildren(target, ctx, style, nodeAlpha, depth + 1);
|
|
1132
1157
|
else this._renderNode(target, ctx, style, nodeAlpha, depth + 1);
|
|
1133
|
-
ctx.restore();
|
|
1134
1158
|
}
|
|
1135
1159
|
break;
|
|
1136
1160
|
}
|
|
@@ -1192,7 +1216,7 @@ export default class SvgView {
|
|
|
1192
1216
|
|
|
1193
1217
|
_renderMarker(id, vertex, first, ctx, style, alpha, depth) {
|
|
1194
1218
|
const marker = this._ids.get(id);
|
|
1195
|
-
if (!marker || tag(marker) !== 'marker' ||
|
|
1219
|
+
if (!marker || tag(marker) !== 'marker' || hiddenAbove(marker, this._root)) return;
|
|
1196
1220
|
if ((this._marking ??= new Set()).has(marker)) return;
|
|
1197
1221
|
const mw = viewportLength(marker, 'markerWidth', 0, 3);
|
|
1198
1222
|
const mh = viewportLength(marker, 'markerHeight', 0, 3);
|
|
@@ -1347,6 +1371,299 @@ export default class SvgView {
|
|
|
1347
1371
|
ctx.restore();
|
|
1348
1372
|
}
|
|
1349
1373
|
|
|
1374
|
+
/** The `<clipPath>` an element's `clip-path` names, or null for none. One
|
|
1375
|
+
* that is not there, that names something else, or whose own `display`
|
|
1376
|
+
* is `none` clips nothing, as Chrome, Firefox and WebKit all have it,
|
|
1377
|
+
* where one inside something that is `display: none` clips as any other
|
|
1378
|
+
* does. */
|
|
1379
|
+
_clipOf(node) {
|
|
1380
|
+
const id = ownProperty(node, 'clip-path', urlReference);
|
|
1381
|
+
if (!id) return null;
|
|
1382
|
+
const clip = this._ids.get(id);
|
|
1383
|
+
return clip && tag(clip) === 'clippath' && !displayNone(clip) ? clip : null;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/** The clipPath a clip path's own `clip-path` names, or one on what is in
|
|
1387
|
+
* it, as `_clipOf` — and null for one that reaches itself through the
|
|
1388
|
+
* clip paths on it and in it, which Chrome and WebKit leave out, keeping
|
|
1389
|
+
* the clip that named it. */
|
|
1390
|
+
_nestedClip(node) {
|
|
1391
|
+
const clip = this._clipOf(node);
|
|
1392
|
+
return clip && !this._reachesItself(clip) ? clip : null;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
_reachesItself(clip) {
|
|
1396
|
+
const seen = new Set();
|
|
1397
|
+
const stack = [clip];
|
|
1398
|
+
while (stack.length) {
|
|
1399
|
+
const at = stack.pop();
|
|
1400
|
+
const next = [this._clipOf(at)];
|
|
1401
|
+
for (const child of at.children || []) {
|
|
1402
|
+
if (child.type !== 'tag') continue;
|
|
1403
|
+
next.push(this._clipOf(child));
|
|
1404
|
+
if (tag(child) === 'use') {
|
|
1405
|
+
const target = this._useTarget(child);
|
|
1406
|
+
if (target) next.push(this._clipOf(target));
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
for (const n of next) {
|
|
1410
|
+
if (n === clip) return true;
|
|
1411
|
+
if (n && !seen.has(n)) {
|
|
1412
|
+
seen.add(n);
|
|
1413
|
+
stack.push(n);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return false;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* An element cut to the `<clipPath>` its `clip-path` names (CSS Masking
|
|
1422
|
+
* 1, 6), in the user space the element draws in: after its `transform`,
|
|
1423
|
+
* and a `use`'s `x` and `y`. `paint(ctx, alpha)` draws the element.
|
|
1424
|
+
*/
|
|
1425
|
+
_renderClipped(node, clip, ctx, style, alpha, depth, paint) {
|
|
1426
|
+
const region = this._clipRegion(clip, node, style, MAT_IDENTITY, ctx, depth, 0);
|
|
1427
|
+
if (region) this._drawCut(ctx, [region], alpha, paint);
|
|
1428
|
+
else paint(ctx, alpha);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* What a `<clipPath>` lets show of `ref`, in the user space `m` maps
|
|
1433
|
+
* `ref`'s to: `{ parts, own }`, the union of the parts, each cut to the
|
|
1434
|
+
* regions in its `clips`, and all of that cut to `own`, the region of the
|
|
1435
|
+
* clipPath's own `clip-path`, where it has one.
|
|
1436
|
+
*
|
|
1437
|
+
* In `objectBoundingBox` units the clipPath's user space is `ref`'s
|
|
1438
|
+
* bounding box, and a box of no area is a region of nothing. The
|
|
1439
|
+
* clipPath's `transform` goes outside that, and its own `clip-path` is set
|
|
1440
|
+
* in `ref`'s user space, as Chrome and Firefox set it.
|
|
1441
|
+
*
|
|
1442
|
+
* `depth` is how deep `ref` is drawn, and `nest` how many clip paths in
|
|
1443
|
+
* this one is: null past 32 of them, which cuts nothing.
|
|
1444
|
+
*/
|
|
1445
|
+
_clipRegion(clip, ref, refStyle, m, ctx, depth, nest) {
|
|
1446
|
+
if (nest > 32) return null;
|
|
1447
|
+
const region = { parts: [], own: null };
|
|
1448
|
+
let base = matMultiply(m, matrixOf(parseSvgTransform(attr(clip, 'transform'))));
|
|
1449
|
+
if ((attr(clip, 'clipPathUnits') || '').trim() === 'objectBoundingBox') {
|
|
1450
|
+
const bbox = this._bbox(ref, ctx, refStyle, depth);
|
|
1451
|
+
if (!bbox || !(bbox.w > 0 && bbox.h > 0)) return region;
|
|
1452
|
+
base = matMultiply(base, [bbox.w, 0, 0, bbox.h, bbox.x, bbox.y]);
|
|
1453
|
+
}
|
|
1454
|
+
const inherited = this._inheritedStyle(clip);
|
|
1455
|
+
for (const child of clip.children || []) {
|
|
1456
|
+
if (child.type !== 'tag') continue;
|
|
1457
|
+
const part = this._clipPart(child, inherited, base, ctx, depth, nest);
|
|
1458
|
+
if (part) region.parts.push(part);
|
|
1459
|
+
}
|
|
1460
|
+
const own = this._nestedClip(clip);
|
|
1461
|
+
if (own) region.own = this._clipRegion(own, ref, refStyle, m, ctx, depth, nest + 1);
|
|
1462
|
+
return region;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/**
|
|
1466
|
+
* A child of a `<clipPath>` as a part of its region (CSS Masking 1, 6.3):
|
|
1467
|
+
* a shape — `{ path, rule, m }` — or a text — `{ text, style, m }` — with
|
|
1468
|
+
* `m` the matrix from its user space to the region's, `box` where it lies
|
|
1469
|
+
* there, and `clips` the regions its own `clip-path` cuts it to. A `use`
|
|
1470
|
+
* is the shape or text it names, through the use's transforms and its
|
|
1471
|
+
* own, and cut by both their clip paths.
|
|
1472
|
+
*
|
|
1473
|
+
* What counts is the shape as it is filled, whatever its paint, by its
|
|
1474
|
+
* `clip-rule`, which it inherits from the clipPath and what the clipPath
|
|
1475
|
+
* is in, and not from what is clipped. Anything else — a group, a `use`
|
|
1476
|
+
* of a group or a symbol — is no part, as Chrome and WebKit leave it,
|
|
1477
|
+
* and nor is one that is `display: none`, whose conditions do not hold,
|
|
1478
|
+
* or that is not visible.
|
|
1479
|
+
*/
|
|
1480
|
+
_clipPart(child, inherited, base, ctx, depth, nest) {
|
|
1481
|
+
if (!CLIPPING.has(tag(child)) || displayNone(child) || !conditionsHold(child, this.languages)) return null;
|
|
1482
|
+
let style = this._style(child, inherited);
|
|
1483
|
+
let m = matMultiply(base, matrixOf(ownTransforms(child)));
|
|
1484
|
+
const clips = [];
|
|
1485
|
+
const cutBy = (node, st, at) => {
|
|
1486
|
+
const clip = this._nestedClip(node);
|
|
1487
|
+
const region = clip && this._clipRegion(clip, node, st, at, ctx, depth, nest + 1);
|
|
1488
|
+
if (region) clips.push(region);
|
|
1489
|
+
};
|
|
1490
|
+
cutBy(child, style, m);
|
|
1491
|
+
let shape = child;
|
|
1492
|
+
if (tag(child) === 'use') {
|
|
1493
|
+
shape = this._useTarget(child);
|
|
1494
|
+
if (!shape || tag(shape) === 'use' || !CLIPPING.has(tag(shape))) return null;
|
|
1495
|
+
if (displayNone(shape) || !conditionsHold(shape, this.languages)) return null;
|
|
1496
|
+
style = this._style(shape, style);
|
|
1497
|
+
m = matMultiply(m, matrixOf(ownTransforms(shape)));
|
|
1498
|
+
cutBy(shape, style, m);
|
|
1499
|
+
}
|
|
1500
|
+
if (style.visibility !== 'visible') return null;
|
|
1501
|
+
if (tag(shape) === 'text') {
|
|
1502
|
+
const box = this._bbox(shape, ctx, style, depth);
|
|
1503
|
+
if (!box) return null;
|
|
1504
|
+
// and room for what a glyph draws past its advance and the font's
|
|
1505
|
+
// extent
|
|
1506
|
+
const pad = style.fontSize / 2;
|
|
1507
|
+
const inked = { x: box.x - pad, y: box.y - pad, w: box.w + 2 * pad, h: box.h + 2 * pad };
|
|
1508
|
+
return { text: shape, style, m, clips, box: transformedBox(inked, [m]) };
|
|
1509
|
+
}
|
|
1510
|
+
const path = shapePath(shape);
|
|
1511
|
+
if (!path) return null;
|
|
1512
|
+
return { path, rule: style.clipRule, m, clips, box: transformedBox(pathBBox(path), [m]) };
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* Draw with `paint(ctx, alpha)` cut to every one of `regions`: with the
|
|
1517
|
+
* context's own clip, each that is one shape and what cuts it, and each
|
|
1518
|
+
* that is not through surfaces (`_drawThrough`).
|
|
1519
|
+
*/
|
|
1520
|
+
_drawCut(ctx, regions, alpha, paint) {
|
|
1521
|
+
ctx.save();
|
|
1522
|
+
try {
|
|
1523
|
+
const rest = [];
|
|
1524
|
+
for (const region of regions) {
|
|
1525
|
+
const cut = this._clipTo(ctx, region);
|
|
1526
|
+
if (cut === false) return;
|
|
1527
|
+
if (cut === null) rest.push(region);
|
|
1528
|
+
}
|
|
1529
|
+
if (!rest.length) paint(ctx, alpha);
|
|
1530
|
+
else this._drawThrough(ctx, rest[0], alpha, (c, a) => this._drawCut(c, rest.slice(1), a, paint));
|
|
1531
|
+
} finally {
|
|
1532
|
+
ctx.restore();
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Cut the context to a region with its own clip, where that can: true
|
|
1538
|
+
* where it did, false where nothing of the region shows, and null —
|
|
1539
|
+
* leaving the context as it was — where the region, or one that cuts it,
|
|
1540
|
+
* is more than one part, or a text, which a context's clip is not.
|
|
1541
|
+
*/
|
|
1542
|
+
_clipTo(ctx, region) {
|
|
1543
|
+
if (!region.parts.length) return false;
|
|
1544
|
+
if (!clipsAsOne(region)) return null;
|
|
1545
|
+
const cut = (r) => {
|
|
1546
|
+
const [part] = r.parts;
|
|
1547
|
+
if (!part) return false;
|
|
1548
|
+
ctx.clip(placedPath(part.path, part.m), part.rule);
|
|
1549
|
+
return part.clips.every(cut) && (!r.own || cut(r.own));
|
|
1550
|
+
};
|
|
1551
|
+
return cut(region);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* Draw with `paint` cut to a region the context's clip cannot make, as a
|
|
1556
|
+
* mask cuts (`_renderMasked`): on a surface, and the region's coverage on
|
|
1557
|
+
* a second, through the same transform — device pixels, the part of the
|
|
1558
|
+
* region the drawing can show — the second cutting the first with
|
|
1559
|
+
* `destination-in`, and the first composited at `alpha`.
|
|
1560
|
+
*
|
|
1561
|
+
* With no surface to draw on, or no `destination-in`, it is cut to the
|
|
1562
|
+
* outlines of the region's shapes as one path, which is their union
|
|
1563
|
+
* wherever they do not overlap, and to a text's box.
|
|
1564
|
+
*/
|
|
1565
|
+
_drawThrough(ctx, region, alpha, paint) {
|
|
1566
|
+
const extent = region.parts.reduce((box, part) => unionBox(box, part.box), null);
|
|
1567
|
+
const m = ctx.getTransform?.();
|
|
1568
|
+
const make = this._surface;
|
|
1569
|
+
const box = m && make && ctx.drawImage && ctx.setTransform ? surfaceBox(m, extent, this._bounds) : null;
|
|
1570
|
+
if (box === EMPTY) return;
|
|
1571
|
+
let content = null;
|
|
1572
|
+
let cover = null;
|
|
1573
|
+
let cctx = null;
|
|
1574
|
+
let mctx = null;
|
|
1575
|
+
let done = false;
|
|
1576
|
+
const bounds = this._bounds;
|
|
1577
|
+
const luminance = this._luminance;
|
|
1578
|
+
try {
|
|
1579
|
+
if (box) {
|
|
1580
|
+
content = make(box.w, box.h);
|
|
1581
|
+
cover = content && make(box.w, box.h);
|
|
1582
|
+
}
|
|
1583
|
+
if (cover) {
|
|
1584
|
+
cctx = content.getContext('2d');
|
|
1585
|
+
cctx.globalCompositeOperation = 'destination-in';
|
|
1586
|
+
if (cctx.globalCompositeOperation === 'destination-in') {
|
|
1587
|
+
cctx.globalCompositeOperation = 'source-over';
|
|
1588
|
+
const place = [m.a, m.b, m.c, m.d, m.e - box.x, m.f - box.y];
|
|
1589
|
+
// what the surfaces show is all of them
|
|
1590
|
+
this._bounds = { x: 0, y: 0, w: box.w, h: box.h };
|
|
1591
|
+
cctx.transform(...place);
|
|
1592
|
+
paint(cctx, 1);
|
|
1593
|
+
|
|
1594
|
+
mctx = cover.getContext('2d');
|
|
1595
|
+
mctx.transform(...place);
|
|
1596
|
+
this._luminance = false;
|
|
1597
|
+
this._cover(mctx, region);
|
|
1598
|
+
|
|
1599
|
+
cctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
1600
|
+
cctx.globalAlpha = 1;
|
|
1601
|
+
cctx.globalCompositeOperation = 'destination-in';
|
|
1602
|
+
cctx.drawImage(cover, 0, 0);
|
|
1603
|
+
ctx.save();
|
|
1604
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
1605
|
+
ctx.globalAlpha = alpha;
|
|
1606
|
+
ctx.drawImage(content, box.x, box.y);
|
|
1607
|
+
ctx.restore();
|
|
1608
|
+
done = true;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
} finally {
|
|
1612
|
+
this._bounds = bounds;
|
|
1613
|
+
this._luminance = luminance;
|
|
1614
|
+
mctx?.destroy?.();
|
|
1615
|
+
cctx?.destroy?.();
|
|
1616
|
+
cover?.destroy?.();
|
|
1617
|
+
content?.destroy?.();
|
|
1618
|
+
}
|
|
1619
|
+
if (done) return;
|
|
1620
|
+
const outline = new Path2D();
|
|
1621
|
+
const rules = new Set();
|
|
1622
|
+
for (const part of region.parts) {
|
|
1623
|
+
if (part.text) {
|
|
1624
|
+
outline.rect(part.box.x, part.box.y, part.box.w, part.box.h);
|
|
1625
|
+
rules.add('nonzero');
|
|
1626
|
+
} else {
|
|
1627
|
+
outline.addPath(part.path, part.m);
|
|
1628
|
+
rules.add(part.rule);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
ctx.save();
|
|
1632
|
+
ctx.clip(outline, rules.size === 1 && rules.has('evenodd') ? 'evenodd' : 'nonzero');
|
|
1633
|
+
paint(ctx, alpha);
|
|
1634
|
+
ctx.restore();
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/** A region's coverage, drawn opaque: each part, cut to the regions that
|
|
1638
|
+
* cut it, and all of them to the region's own. */
|
|
1639
|
+
_cover(ctx, region) {
|
|
1640
|
+
const parts = (c) => {
|
|
1641
|
+
for (const part of region.parts) this._drawCut(c, part.clips, 1, (cc) => this._silhouette(cc, part));
|
|
1642
|
+
};
|
|
1643
|
+
if (region.own) this._drawCut(ctx, [region.own], 1, parts);
|
|
1644
|
+
else parts(ctx);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
/** A part of a clip region filled opaque, whatever its paint: a shape by
|
|
1648
|
+
* its clip rule, a text in its glyphs. */
|
|
1649
|
+
_silhouette(ctx, part) {
|
|
1650
|
+
if (part.text) {
|
|
1651
|
+
ctx.save();
|
|
1652
|
+
ctx.transform(...part.m);
|
|
1653
|
+
this._silhouetting = true;
|
|
1654
|
+
try {
|
|
1655
|
+
this._renderText(part.text, ctx, part.style, 1);
|
|
1656
|
+
} finally {
|
|
1657
|
+
this._silhouetting = false;
|
|
1658
|
+
ctx.restore();
|
|
1659
|
+
}
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
ctx.fillStyle = '#000';
|
|
1663
|
+
ctx.globalAlpha = 1;
|
|
1664
|
+
ctx.fill(placedPath(part.path, part.m), part.rule);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1350
1667
|
/** The style an element has where it stands in the document: what is in a
|
|
1351
1668
|
* `<mask>` inherits from the mask's ancestors, not from what it masks. */
|
|
1352
1669
|
_inheritedStyle(node) {
|
|
@@ -1359,9 +1676,10 @@ export default class SvgView {
|
|
|
1359
1676
|
|
|
1360
1677
|
/**
|
|
1361
1678
|
* An element's bounding box in its own user space (SVG 2, 8.10): the
|
|
1362
|
-
* geometry of its shapes and text, without strokes
|
|
1363
|
-
* transforms of what is in it but not its own
|
|
1364
|
-
*
|
|
1679
|
+
* geometry of its shapes and text, without strokes or what clips them,
|
|
1680
|
+
* through the transforms of what is in it but not its own, which for a
|
|
1681
|
+
* `use` are its `x` and `y` too (`ownTransforms`). Null for one that
|
|
1682
|
+
* has none.
|
|
1365
1683
|
*/
|
|
1366
1684
|
_bbox(node, ctx, style, depth) {
|
|
1367
1685
|
if (depth > 32) return null;
|
|
@@ -1377,7 +1695,7 @@ export default class SvgView {
|
|
|
1377
1695
|
if (!child || child.type !== 'tag') continue;
|
|
1378
1696
|
if (NON_RENDERED.has(tag(child)) || displayNone(child) || !conditionsHold(child, this.languages)) continue;
|
|
1379
1697
|
const inner = this._bbox(child, ctx, this._style(child, style), depth + 1);
|
|
1380
|
-
if (inner) box = unionBox(box, transformedBox(inner,
|
|
1698
|
+
if (inner) box = unionBox(box, transformedBox(inner, ownTransforms(child)));
|
|
1381
1699
|
}
|
|
1382
1700
|
return box;
|
|
1383
1701
|
}
|
|
@@ -1389,13 +1707,14 @@ export default class SvgView {
|
|
|
1389
1707
|
for (const child of target.children || []) {
|
|
1390
1708
|
if (child.type !== 'tag' || NON_RENDERED.has(tag(child)) || displayNone(child)) continue;
|
|
1391
1709
|
const inner = this._bbox(child, ctx, this._style(child, style), depth + 1);
|
|
1392
|
-
if (inner) box = unionBox(box, transformedBox(inner,
|
|
1710
|
+
if (inner) box = unionBox(box, transformedBox(inner, ownTransforms(child)));
|
|
1393
1711
|
}
|
|
1394
1712
|
} else {
|
|
1395
1713
|
const inner = this._bbox(target, ctx, this._style(target, style), depth + 1);
|
|
1396
|
-
if (inner) box = transformedBox(inner,
|
|
1714
|
+
if (inner) box = transformedBox(inner, ownTransforms(target));
|
|
1397
1715
|
}
|
|
1398
|
-
|
|
1716
|
+
// in the space its `x` and `y` move it to, which is its own
|
|
1717
|
+
return box;
|
|
1399
1718
|
}
|
|
1400
1719
|
case 'text': {
|
|
1401
1720
|
ctx.save();
|
|
@@ -1578,7 +1897,7 @@ export default class SvgView {
|
|
|
1578
1897
|
let bounds = null;
|
|
1579
1898
|
for (const p of pieces) {
|
|
1580
1899
|
const st = p.span.style;
|
|
1581
|
-
if (st.visibility !== 'visible' || st.fill === 'none' || !p.text.trim()) continue;
|
|
1900
|
+
if (st.visibility !== 'visible' || (st.fill === 'none' && !this._silhouetting) || !p.text.trim()) continue;
|
|
1582
1901
|
if (!bounds) {
|
|
1583
1902
|
bounds = new Path2D();
|
|
1584
1903
|
bounds.rect(box.x, box.y, box.w, box.h);
|
|
@@ -1685,9 +2004,68 @@ function urlReference(raw) {
|
|
|
1685
2004
|
return m ? m[1] : undefined;
|
|
1686
2005
|
}
|
|
1687
2006
|
|
|
2007
|
+
/**
|
|
2008
|
+
* The transforms an element sets for itself and what it holds: its
|
|
2009
|
+
* `transform`, and after that a `use`'s `x` and `y`, which SVG 1.1 puts at
|
|
2010
|
+
* the end of the transform of the group a use stands for (5.6), the group
|
|
2011
|
+
* its `clip-path` and its `mask` go on. So both are set in the space the
|
|
2012
|
+
* use's `x` and `y` move it to, as Chrome, Firefox and WebKit set them.
|
|
2013
|
+
*/
|
|
2014
|
+
function ownTransforms(node) {
|
|
2015
|
+
const list = parseSvgTransform(node.attribs?.transform);
|
|
2016
|
+
if (tag(node) === 'use') {
|
|
2017
|
+
const x = attrNum(node, 'x');
|
|
2018
|
+
const y = attrNum(node, 'y');
|
|
2019
|
+
if (x || y) list.push([1, 0, 0, 1, x, y]);
|
|
2020
|
+
}
|
|
2021
|
+
return list;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
/** A transform list as the one matrix it comes to. */
|
|
2025
|
+
function matrixOf(transforms) {
|
|
2026
|
+
let m = MAT_IDENTITY;
|
|
2027
|
+
for (const t of transforms) m = matMultiply(m, t);
|
|
2028
|
+
return m;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
/** What a `<clipPath>` may hold that is part of its region: the shapes,
|
|
2032
|
+
* text, and a `use` of either (CSS Masking 1, 6.3). */
|
|
2033
|
+
const CLIPPING = new Set(['path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'text', 'use']);
|
|
2034
|
+
|
|
2035
|
+
/** Whether a clip region is one shape at most, cut by regions that are one
|
|
2036
|
+
* at most themselves: what a context's own clip makes. */
|
|
2037
|
+
function clipsAsOne(region) {
|
|
2038
|
+
if (region.parts.length > 1) return false;
|
|
2039
|
+
const [part] = region.parts;
|
|
2040
|
+
if (part && (part.text || !part.clips.every(clipsAsOne))) return false;
|
|
2041
|
+
return !region.own || clipsAsOne(region.own);
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
/** A path in the space `m` maps its own to. */
|
|
2045
|
+
function placedPath(path, m) {
|
|
2046
|
+
if (matIsIdentity(m)) return path;
|
|
2047
|
+
const out = new Path2D();
|
|
2048
|
+
out.addPath(path, m);
|
|
2049
|
+
return out;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
1688
2052
|
/** The shapes that take markers (SVG 2, 11.6): the ones with vertices. */
|
|
1689
2053
|
const MARKABLE = new Set(['path', 'line', 'polyline', 'polygon']);
|
|
1690
2054
|
|
|
2055
|
+
/**
|
|
2056
|
+
* Whether something around an element is `display: none`: a marker in such
|
|
2057
|
+
* a subtree is drawn nowhere, by Chrome, Firefox or WebKit alike. Its own
|
|
2058
|
+
* `display` is not asked: the property does not apply to a `<marker>`
|
|
2059
|
+
* (SVG 2, 11.6.2), and Chrome and WebKit draw one that says `none`.
|
|
2060
|
+
*/
|
|
2061
|
+
function hiddenAbove(node, root) {
|
|
2062
|
+
for (let at = node.parent; at && at.type === 'tag'; at = at.parent) {
|
|
2063
|
+
if (displayNone(at)) return true;
|
|
2064
|
+
if (at === root) break;
|
|
2065
|
+
}
|
|
2066
|
+
return false;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
1691
2069
|
/** The ids of the markers a style names, each once. */
|
|
1692
2070
|
function markerIds(style) {
|
|
1693
2071
|
const ids = new Set([style.markerStart, style.markerMid, style.markerEnd]);
|