domotion-svg 0.2.2 → 0.3.2
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/FEATURES.md +1 -0
- package/README.md +29 -0
- package/dist/animation/animator.js +25 -14
- package/dist/animation/animator.test.js +54 -21
- package/dist/animation/cursor-overlay.js +0 -2
- package/dist/capture/emoji.js +29 -18
- package/dist/capture/index.js +5 -4
- package/dist/capture/script/color-norm.d.ts +1 -0
- package/dist/capture/script/color-norm.js +43 -1
- package/dist/capture/script/emoji-detect.js +14 -0
- package/dist/capture/script/index.js +593 -65
- package/dist/capture/script/walker/borders-backgrounds.d.ts +24 -17
- package/dist/capture/script/walker/borders-backgrounds.js +123 -7
- package/dist/capture/script/walker/counter-style-resolver.d.ts +7 -0
- package/dist/capture/script/walker/counter-style-resolver.js +218 -0
- package/dist/capture/script/walker/input-value.js +14 -1
- package/dist/capture/script/walker/lists-counters.d.ts +3 -1
- package/dist/capture/script/walker/lists-counters.js +22 -2
- package/dist/capture/script/walker/masks-clips.d.ts +2 -0
- package/dist/capture/script/walker/masks-clips.js +41 -1
- package/dist/capture/script/walker/pseudo-content.d.ts +14 -1
- package/dist/capture/script/walker/pseudo-content.js +301 -61
- package/dist/capture/script/walker/pseudo-inject.js +20 -0
- package/dist/capture/script/walker/text-segments.js +98 -4
- package/dist/capture/script/walker/transforms.d.ts +1 -0
- package/dist/capture/script/walker/transforms.js +16 -0
- package/dist/capture/script.generated.js +1 -1
- package/dist/capture/types.d.ts +213 -2
- package/dist/cli/animate.js +151 -15
- package/dist/mask.test.js +12 -7
- package/dist/render/borders.d.ts +9 -13
- package/dist/render/borders.js +379 -14
- package/dist/render/element-tree-to-svg.d.ts +11 -12
- package/dist/render/element-tree-to-svg.js +2046 -241
- package/dist/render/embedded-font-builder.d.ts +49 -0
- package/dist/render/embedded-font-builder.js +149 -0
- package/dist/render/form-controls.js +45 -24
- package/dist/render/gradients.d.ts +15 -0
- package/dist/render/gradients.js +103 -2
- package/dist/render/gradients.test.js +34 -0
- package/dist/render/text-to-path.d.ts +38 -1
- package/dist/render/text-to-path.js +654 -29
- package/dist/render/text-to-path.test.js +230 -9
- package/dist/render/text.d.ts +14 -0
- package/dist/render/text.js +344 -40
- package/dist/scroll/composer.d.ts +26 -0
- package/dist/scroll/composer.js +199 -11
- package/dist/scroll/composer.test.js +293 -16
- package/dist/scroll/executor.d.ts +3 -1
- package/dist/scroll/executor.js +15 -6
- package/dist/scroll/executor.test.js +25 -0
- package/dist/scroll/hoist-fixed.d.ts +48 -0
- package/dist/scroll/hoist-fixed.js +85 -0
- package/dist/scroll/hoist-fixed.test.d.ts +1 -0
- package/dist/scroll/hoist-fixed.test.js +103 -0
- package/dist/scroll/hoist-sticky.d.ts +45 -0
- package/dist/scroll/hoist-sticky.js +157 -0
- package/dist/scroll/hoist-sticky.test.d.ts +1 -0
- package/dist/scroll/hoist-sticky.test.js +154 -0
- package/dist/scroll/pattern.d.ts +22 -5
- package/dist/scroll/pattern.js +55 -7
- package/dist/scroll/pattern.test.js +48 -1
- package/dist/tree-ops/frame-merge.d.ts +10 -0
- package/dist/tree-ops/frame-merge.js +23 -5
- package/dist/tree-ops/frame-merge.test.js +45 -0
- package/dist/tree-ops/tree-diff.js +1 -1
- package/dist/tree-ops/viewbox-culling.js +32 -18
- package/dist/tree-ops/viewbox-culling.test.js +40 -6
- package/package.json +8 -2
- package/src/animation/animator.test.ts +56 -21
- package/src/animation/animator.ts +25 -14
- package/src/animation/cursor-overlay.ts +0 -2
- package/src/capture/emoji.ts +28 -18
- package/src/capture/index.ts +15 -14
- package/src/capture/script/color-norm.ts +38 -1
- package/src/capture/script/emoji-detect.ts +14 -0
- package/src/capture/script/index.ts +555 -48
- package/src/capture/script/walker/borders-backgrounds.ts +114 -7
- package/src/capture/script/walker/counter-style-resolver.ts +184 -0
- package/src/capture/script/walker/input-value.ts +14 -1
- package/src/capture/script/walker/lists-counters.ts +24 -2
- package/src/capture/script/walker/masks-clips.ts +40 -1
- package/src/capture/script/walker/pseudo-content.ts +297 -55
- package/src/capture/script/walker/pseudo-inject.ts +20 -0
- package/src/capture/script/walker/text-segments.ts +93 -4
- package/src/capture/script/walker/transforms.ts +14 -0
- package/src/capture/script.generated.ts +1 -1
- package/src/capture/types.ts +202 -2
- package/src/cli/animate.ts +135 -15
- package/src/mask.test.ts +12 -7
- package/src/render/borders.ts +383 -17
- package/src/render/element-tree-to-svg.ts +2051 -238
- package/src/render/embedded-font-builder.ts +221 -0
- package/src/render/form-controls.ts +45 -24
- package/src/render/gradients.test.ts +46 -0
- package/src/render/gradients.ts +94 -2
- package/src/render/opentype.js.d.ts +7 -0
- package/src/render/text-to-path.test.ts +246 -9
- package/src/render/text-to-path.ts +702 -31
- package/src/render/text.ts +344 -40
- package/src/scroll/composer.test.ts +322 -16
- package/src/scroll/composer.ts +246 -13
- package/src/scroll/executor.test.ts +27 -0
- package/src/scroll/executor.ts +19 -10
- package/src/scroll/hoist-fixed.test.ts +117 -0
- package/src/scroll/hoist-fixed.ts +95 -0
- package/src/scroll/hoist-sticky.test.ts +173 -0
- package/src/scroll/hoist-sticky.ts +193 -0
- package/src/scroll/pattern.test.ts +58 -1
- package/src/scroll/pattern.ts +71 -8
- package/src/tree-ops/frame-merge.test.ts +51 -0
- package/src/tree-ops/frame-merge.ts +24 -6
- package/src/tree-ops/tree-diff.ts +3 -1
- package/src/tree-ops/viewbox-culling.test.ts +42 -6
- package/src/tree-ops/viewbox-culling.ts +32 -18
package/dist/render/borders.js
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
import { r, esc } from "./format.js";
|
|
7
7
|
import { parseColor } from "./colors.js";
|
|
8
8
|
import { embedResizedDataUri } from "../capture/embed.js";
|
|
9
|
+
import { parseGradient, buildLinearGradientDef, buildRadialGradientDef } from "./gradients.js";
|
|
10
|
+
const BORDER_IMAGE_REPEATS = new Set(["stretch", "repeat", "round", "space"]);
|
|
11
|
+
function normalizeBorderImageRepeat(raw) {
|
|
12
|
+
if (raw != null && BORDER_IMAGE_REPEATS.has(raw))
|
|
13
|
+
return raw;
|
|
14
|
+
return "stretch";
|
|
15
|
+
}
|
|
9
16
|
/** Parse a captured "h v" axis-pair (e.g. "30px 30px" or "50px 20px"). */
|
|
10
17
|
function _parsePair(v) {
|
|
11
18
|
if (!v)
|
|
@@ -73,6 +80,27 @@ export function insetCornerRadii(c, top, right, bottom, left) {
|
|
|
73
80
|
&& tl.h === br.h && tl.h === br.v && tl.h === bl.h && tl.h === bl.v;
|
|
74
81
|
return { tl, tr, br, bl, uniform };
|
|
75
82
|
}
|
|
83
|
+
/** Grow each corner radius outward by `spread` for an OUTSET box-shadow shape.
|
|
84
|
+
* Per CSS Backgrounds 3 §6.4 and Chromium's `FloatRoundedRect::Outset`, a
|
|
85
|
+
* corner whose source radius is zero STAYS sharp through any spread — only
|
|
86
|
+
* pre-curved corners grow. A naive `corner + spread` produces visibly
|
|
87
|
+
* rounded shadow corners on a sharp-cornered box (e.g. concentric outlines
|
|
88
|
+
* built from `box-shadow: 0 0 0 Npx`). Use this for outset shadow shapes;
|
|
89
|
+
* the dual inset case is already covered by `insetCornerRadii` shrinking to
|
|
90
|
+
* zero when the border eats past the radius. */
|
|
91
|
+
export function outsetCornerRadiiForShadow(c, spread) {
|
|
92
|
+
const grow = (p) => ({
|
|
93
|
+
h: p.h > 0 ? Math.max(0, p.h + spread) : 0,
|
|
94
|
+
v: p.v > 0 ? Math.max(0, p.v + spread) : 0,
|
|
95
|
+
});
|
|
96
|
+
const tl = grow(c.tl);
|
|
97
|
+
const tr = grow(c.tr);
|
|
98
|
+
const br = grow(c.br);
|
|
99
|
+
const bl = grow(c.bl);
|
|
100
|
+
const uniform = tl.h === tl.v && tl.h === tr.h && tl.h === tr.v
|
|
101
|
+
&& tl.h === br.h && tl.h === br.v && tl.h === bl.h && tl.h === bl.v;
|
|
102
|
+
return { tl, tr, br, bl, uniform };
|
|
103
|
+
}
|
|
76
104
|
/** Emit an SVG path `d` attribute for a rounded rectangle with per-corner radii.
|
|
77
105
|
* Path goes clockwise from the top-left, using elliptical arc commands at each
|
|
78
106
|
* corner. Zero-radius corners collapse to a sharp 90° join. */
|
|
@@ -168,13 +196,239 @@ export function injectSvgSize(svgHtml, w, h) {
|
|
|
168
196
|
* Returns { svg, usedIds }. usedIds indicates how many clipIdx values were
|
|
169
197
|
* consumed so the caller can keep its own counter in sync.
|
|
170
198
|
*/
|
|
199
|
+
/**
|
|
200
|
+
* Render a `border-image-source` that's a CSS gradient as a proper 9-slice.
|
|
201
|
+
*
|
|
202
|
+
* Per CSS Images 3, a gradient used as `border-image-source` has the size of
|
|
203
|
+
* the border-image-area (= border-box ± `border-image-outset`). The 9-slice
|
|
204
|
+
* algorithm then applies just like for a raster source: corners stretched,
|
|
205
|
+
* edges tiled per `border-image-repeat`, optional fill center.
|
|
206
|
+
*
|
|
207
|
+
* Implementation: build a single `<linearGradient>` / `<radialGradient>` def
|
|
208
|
+
* positioned in source space `(0, 0) - (natW, natH)` where natW = boxW,
|
|
209
|
+
* natH = boxH. Each slot emits an inner `<svg x dx y dy width dw height dh
|
|
210
|
+
* viewBox="sx sy sw sh" preserveAspectRatio="none">` containing a `<rect
|
|
211
|
+
* width="natW" height="natH" fill="url(#g)" />`. The viewBox maps the source
|
|
212
|
+
* slice rect onto the destination slot; the gradient comes along because its
|
|
213
|
+
* `userSpaceOnUse` coordinates are interpreted in the viewBox space. Tiled
|
|
214
|
+
* edges (`repeat` / `round` / `space`) wrap that inner `<svg>` in a
|
|
215
|
+
* `<pattern>`. The single-def-per-element keeps SVG output small and matches
|
|
216
|
+
* how the URL path reuses one source asset.
|
|
217
|
+
*/
|
|
218
|
+
function renderBorderImageGradient(el, indent, idPrefix, defsParts, clipIdx, src) {
|
|
219
|
+
const grad = parseGradient(src);
|
|
220
|
+
if (grad == null)
|
|
221
|
+
return { svg: "", usedIds: 0 };
|
|
222
|
+
if (grad.kind !== "linear" && grad.kind !== "radial")
|
|
223
|
+
return { svg: "", usedIds: 0 };
|
|
224
|
+
const sliceRaw = el.styles.borderImageSlice ?? "100%";
|
|
225
|
+
const fillCenter = /\bfill\b/i.test(sliceRaw);
|
|
226
|
+
const bwTop = parseFloat(el.styles.borderTopWidth ?? "0") || 0;
|
|
227
|
+
const bwRight = parseFloat(el.styles.borderRightWidth ?? "0") || 0;
|
|
228
|
+
const bwBottom = parseFloat(el.styles.borderBottomWidth ?? "0") || 0;
|
|
229
|
+
const bwLeft = parseFloat(el.styles.borderLeftWidth ?? "0") || 0;
|
|
230
|
+
// Outsets: default 0. Same parsing as URL path.
|
|
231
|
+
const outsetTokens = (el.styles.borderImageOutset ?? "0").trim().split(/\s+/);
|
|
232
|
+
const parseOutset = (tok, basis, borderW) => {
|
|
233
|
+
if (tok == null || tok === "")
|
|
234
|
+
return 0;
|
|
235
|
+
if (/%$/.test(tok))
|
|
236
|
+
return (parseFloat(tok) / 100) * basis;
|
|
237
|
+
if (/(px|em|rem|pt|pc|cm|mm|in|Q)$/.test(tok))
|
|
238
|
+
return parseFloat(tok) || 0;
|
|
239
|
+
const n = parseFloat(tok);
|
|
240
|
+
return Number.isFinite(n) ? n * borderW : 0;
|
|
241
|
+
};
|
|
242
|
+
const ot = parseOutset(outsetTokens[0], el.height, bwTop);
|
|
243
|
+
const or_ = parseOutset(outsetTokens[1] ?? outsetTokens[0], el.width, bwRight);
|
|
244
|
+
const ob = parseOutset(outsetTokens[2] ?? outsetTokens[0], el.height, bwBottom);
|
|
245
|
+
const ol = parseOutset(outsetTokens[3] ?? outsetTokens[1] ?? outsetTokens[0], el.width, bwLeft);
|
|
246
|
+
// Border-image-width per side; default = element's border-width. Same as URL path.
|
|
247
|
+
const parseBorderImageLen = (tok, basis, borderW) => {
|
|
248
|
+
if (tok == null || tok === "" || tok === "auto")
|
|
249
|
+
return borderW;
|
|
250
|
+
if (/%$/.test(tok))
|
|
251
|
+
return (parseFloat(tok) / 100) * basis;
|
|
252
|
+
if (/(px|em|rem|pt|pc|cm|mm|in|Q)$/.test(tok))
|
|
253
|
+
return parseFloat(tok) || 0;
|
|
254
|
+
const n = parseFloat(tok);
|
|
255
|
+
return Number.isFinite(n) ? n * borderW : borderW;
|
|
256
|
+
};
|
|
257
|
+
const widthTokens = (el.styles.borderImageWidth ?? "").trim().split(/\s+/);
|
|
258
|
+
const wt = parseBorderImageLen(widthTokens[0], el.height, bwTop);
|
|
259
|
+
const wr = parseBorderImageLen(widthTokens[1] ?? widthTokens[0], el.width, bwRight);
|
|
260
|
+
const wb = parseBorderImageLen(widthTokens[2] ?? widthTokens[0], el.height, bwBottom);
|
|
261
|
+
const wl = parseBorderImageLen(widthTokens[3] ?? widthTokens[1] ?? widthTokens[0], el.width, bwLeft);
|
|
262
|
+
const boxX = el.x - ol;
|
|
263
|
+
const boxY = el.y - ot;
|
|
264
|
+
const boxW = el.width + ol + or_;
|
|
265
|
+
const boxH = el.height + ot + ob;
|
|
266
|
+
if (boxW <= 0 || boxH <= 0)
|
|
267
|
+
return { svg: "", usedIds: 0 };
|
|
268
|
+
// Gradient sources have the size of the border-image-area.
|
|
269
|
+
const natW = boxW;
|
|
270
|
+
const natH = boxH;
|
|
271
|
+
// Slice: numbers = source pixels, percentages = of source dims, optional `fill`.
|
|
272
|
+
const sliceTokens = sliceRaw.replace(/\bfill\b/i, "").trim().split(/\s+/);
|
|
273
|
+
const sliceNums = sliceTokens.map((t) => {
|
|
274
|
+
if (/%$/.test(t))
|
|
275
|
+
return { pct: parseFloat(t) };
|
|
276
|
+
return { px: parseFloat(t) };
|
|
277
|
+
});
|
|
278
|
+
const resolveSlice = (tok, basis) => {
|
|
279
|
+
if (tok.pct != null)
|
|
280
|
+
return (tok.pct / 100) * basis;
|
|
281
|
+
return tok.px ?? 0;
|
|
282
|
+
};
|
|
283
|
+
const st = resolveSlice(sliceNums[0] ?? { px: 0 }, natH);
|
|
284
|
+
const sr = resolveSlice(sliceNums[1] ?? sliceNums[0] ?? { px: 0 }, natW);
|
|
285
|
+
const sb = resolveSlice(sliceNums[2] ?? sliceNums[0] ?? { px: 0 }, natH);
|
|
286
|
+
const sl = resolveSlice(sliceNums[3] ?? sliceNums[1] ?? sliceNums[0] ?? { px: 0 }, natW);
|
|
287
|
+
// Repeat policy per axis.
|
|
288
|
+
const repeatTokens = (el.styles.borderImageRepeat ?? "stretch").trim().split(/\s+/);
|
|
289
|
+
const rH = normalizeBorderImageRepeat((repeatTokens[0] ?? "stretch").toLowerCase());
|
|
290
|
+
const rV = repeatTokens[1] != null && repeatTokens[1] !== "" ? normalizeBorderImageRepeat(repeatTokens[1].toLowerCase()) : rH;
|
|
291
|
+
// Gradient def in source space (0, 0) - (natW, natH). Positioned at the
|
|
292
|
+
// border-image-area's element-absolute origin (boxX, boxY) so the inner
|
|
293
|
+
// <svg viewBox> remap below lands the gradient on the correct destination
|
|
294
|
+
// coordinates. Each <rect> inside an inner <svg viewBox="sx sy sw sh">
|
|
295
|
+
// paints the slice region by drawing the full natW × natH rect — the
|
|
296
|
+
// viewBox + preserveAspectRatio="none" map source slice → destination slot.
|
|
297
|
+
const gid = `${idPrefix}big${clipIdx}`;
|
|
298
|
+
let usedIds = 1;
|
|
299
|
+
const gradRect = { x: boxX, y: boxY, w: natW, h: natH };
|
|
300
|
+
const def = grad.kind === "linear"
|
|
301
|
+
? buildLinearGradientDef(grad, gid, gradRect)
|
|
302
|
+
: buildRadialGradientDef(grad, gid, gradRect);
|
|
303
|
+
defsParts.push(def);
|
|
304
|
+
// Slot geometry in element-absolute coords.
|
|
305
|
+
const x0 = boxX, x1 = boxX + wl, x2 = boxX + boxW - wr, x3 = boxX + boxW;
|
|
306
|
+
const y0 = boxY, y1 = boxY + wt, y2 = boxY + boxH - wb, y3 = boxY + boxH;
|
|
307
|
+
// Source regions in source pixels (NB: corner rects + edge / center rects).
|
|
308
|
+
const sxL = 0, sxR = natW - sr, sxC = sl, sxW_C = natW - sl - sr;
|
|
309
|
+
const syT = 0, syB = natH - sb, syC = st, syH_C = natH - st - sb;
|
|
310
|
+
const parts = [];
|
|
311
|
+
// Inner <svg viewBox> that paints the source slice rect (sx, sy, sw, sh)
|
|
312
|
+
// into the destination slot (dx, dy, dw, dh). The gradient is positioned
|
|
313
|
+
// in source-space coords (boxX..boxX+natW, boxY..boxY+natH); to keep it
|
|
314
|
+
// aligned through the viewBox mapping, the viewBox is offset to start at
|
|
315
|
+
// (boxX + sx, boxY + sy) — so the gradient's userSpaceOnUse coordinates
|
|
316
|
+
// line up with the source rect we're sampling. Then a single <rect>
|
|
317
|
+
// covering (boxX, boxY) - (boxX+natW, boxY+natH) lets the gradient
|
|
318
|
+
// evaluate across the full source space; the viewBox crops to the slice.
|
|
319
|
+
const innerSvgForSlot = (dx, dy, dw, dh, sx, sy, sw, sh) => {
|
|
320
|
+
return `<svg x="${r(dx)}" y="${r(dy)}" width="${r(dw)}" height="${r(dh)}" viewBox="${r(boxX + sx)} ${r(boxY + sy)} ${r(sw)} ${r(sh)}" preserveAspectRatio="none"><rect x="${r(boxX)}" y="${r(boxY)}" width="${r(natW)}" height="${r(natH)}" fill="url(#${gid})" /></svg>`;
|
|
321
|
+
};
|
|
322
|
+
const emitStretchedSlot = (dx, dy, dw, dh, sx, sy, sw, sh) => {
|
|
323
|
+
if (dw <= 0 || dh <= 0 || sw <= 0 || sh <= 0)
|
|
324
|
+
return;
|
|
325
|
+
parts.push(`${indent}${innerSvgForSlot(dx, dy, dw, dh, sx, sy, sw, sh)}`);
|
|
326
|
+
};
|
|
327
|
+
// Tiled edges: wrap the inner <svg> in a <pattern> sized to one tile, then
|
|
328
|
+
// fill the destination rect with that pattern. round / space tile-count
|
|
329
|
+
// logic mirrors the URL path's `emitTiledSliceEdge`. For `space`, the
|
|
330
|
+
// pattern cell is the slot/N and the inner <svg> is centered inside the
|
|
331
|
+
// cell so each end has a half-gap; transparent gap is automatic because
|
|
332
|
+
// the inner <svg> is smaller than the pattern cell.
|
|
333
|
+
const emitTiledEdgeSlot = (dx, dy, dw, dh, sx, sy, sw, sh, axis, mode) => {
|
|
334
|
+
if (dw <= 0 || dh <= 0 || sw <= 0 || sh <= 0)
|
|
335
|
+
return;
|
|
336
|
+
let tileW, tileH;
|
|
337
|
+
if (axis === "x") {
|
|
338
|
+
tileH = dh;
|
|
339
|
+
tileW = sw * (dh / sh);
|
|
340
|
+
if (mode === "round") {
|
|
341
|
+
const count = Math.max(1, Math.round(dw / tileW));
|
|
342
|
+
tileW = dw / count;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
tileW = dw;
|
|
347
|
+
tileH = sh * (dw / sw);
|
|
348
|
+
if (mode === "round") {
|
|
349
|
+
const count = Math.max(1, Math.round(dh / tileH));
|
|
350
|
+
tileH = dh / count;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
let patternW = tileW, patternH = tileH;
|
|
354
|
+
let tileOffX = 0, tileOffY = 0;
|
|
355
|
+
if (mode === "space") {
|
|
356
|
+
if (axis === "x") {
|
|
357
|
+
const count = Math.floor(dw / tileW);
|
|
358
|
+
if (count <= 0)
|
|
359
|
+
return;
|
|
360
|
+
patternW = dw / count;
|
|
361
|
+
tileOffX = (patternW - tileW) / 2;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
const count = Math.floor(dh / tileH);
|
|
365
|
+
if (count <= 0)
|
|
366
|
+
return;
|
|
367
|
+
patternH = dh / count;
|
|
368
|
+
tileOffY = (patternH - tileH) / 2;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const patId = `${idPrefix}bip${clipIdx + usedIds}`;
|
|
372
|
+
usedIds++;
|
|
373
|
+
const inner = innerSvgForSlot(tileOffX, tileOffY, tileW, tileH, sx, sy, sw, sh);
|
|
374
|
+
defsParts.push(`<pattern id="${patId}" patternUnits="userSpaceOnUse" x="${r(dx)}" y="${r(dy)}" width="${r(patternW)}" height="${r(patternH)}">${inner}</pattern>`);
|
|
375
|
+
parts.push(`${indent}<rect x="${r(dx)}" y="${r(dy)}" width="${r(dw)}" height="${r(dh)}" fill="url(#${patId})" />`);
|
|
376
|
+
};
|
|
377
|
+
// 4 corners — always stretched.
|
|
378
|
+
emitStretchedSlot(x0, y0, wl, wt, sxL, syT, sl, st); // NW
|
|
379
|
+
emitStretchedSlot(x2, y0, wr, wt, sxR, syT, sr, st); // NE
|
|
380
|
+
emitStretchedSlot(x0, y2, wl, wb, sxL, syB, sl, sb); // SW
|
|
381
|
+
emitStretchedSlot(x2, y2, wr, wb, sxR, syB, sr, sb); // SE
|
|
382
|
+
// Top + Bottom edges.
|
|
383
|
+
if (rH === "stretch") {
|
|
384
|
+
emitStretchedSlot(x1, y0, x2 - x1, wt, sxC, syT, sxW_C, st);
|
|
385
|
+
emitStretchedSlot(x1, y2, x2 - x1, wb, sxC, syB, sxW_C, sb);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
emitTiledEdgeSlot(x1, y0, x2 - x1, wt, sxC, syT, sxW_C, st, "x", rH);
|
|
389
|
+
emitTiledEdgeSlot(x1, y2, x2 - x1, wb, sxC, syB, sxW_C, sb, "x", rH);
|
|
390
|
+
}
|
|
391
|
+
// Left + Right edges.
|
|
392
|
+
if (rV === "stretch") {
|
|
393
|
+
emitStretchedSlot(x0, y1, wl, y2 - y1, sxL, syC, sl, syH_C);
|
|
394
|
+
emitStretchedSlot(x2, y1, wr, y2 - y1, sxR, syC, sr, syH_C);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
emitTiledEdgeSlot(x0, y1, wl, y2 - y1, sxL, syC, sl, syH_C, "y", rV);
|
|
398
|
+
emitTiledEdgeSlot(x2, y1, wr, y2 - y1, sxR, syC, sr, syH_C, "y", rV);
|
|
399
|
+
}
|
|
400
|
+
// Center — only when `fill`.
|
|
401
|
+
if (fillCenter) {
|
|
402
|
+
emitStretchedSlot(x1, y1, x2 - x1, y2 - y1, sxC, syC, sxW_C, syH_C);
|
|
403
|
+
}
|
|
404
|
+
if (parts.length === 0)
|
|
405
|
+
return { svg: "", usedIds: 0 };
|
|
406
|
+
return { svg: parts.join("\n"), usedIds };
|
|
407
|
+
}
|
|
171
408
|
export function renderBorderImage(el, indent, idPrefix, defsParts, clipIdx) {
|
|
172
409
|
const src = el.styles.borderImageSource;
|
|
173
410
|
if (src == null || src === "none" || src === "")
|
|
174
411
|
return { svg: "", usedIds: 0 };
|
|
412
|
+
// DM-722: CSS gradient as `border-image-source`. The 9-slice machinery
|
|
413
|
+
// below is built around a fixed-size raster source. Per CSS Images 3, a
|
|
414
|
+
// gradient used as `border-image-source` resolves to a concrete-size image
|
|
415
|
+
// equal to the border-image-area (= border-box ± `border-image-outset`).
|
|
416
|
+
// For the common `border-image: <grad> 1` case (slice 1, stretch — the
|
|
417
|
+
// fixture's `.gradient-border` panel), emit a single "border ring" path
|
|
418
|
+
// (outer rect minus inner rect via even-odd fill rule) filled with the
|
|
419
|
+
// gradient scoped to the full border-image-area. This matches Chrome's
|
|
420
|
+
// paint because slice 1 + stretch effectively maps a continuous gradient
|
|
421
|
+
// along all four sides — exactly what painting the whole area with the
|
|
422
|
+
// gradient and clipping to the border donut produces. Slice values other
|
|
423
|
+
// than `1` or `1 fill` (with non-degenerate edge tiling) fall through
|
|
424
|
+
// unsupported for gradient sources; the rasterise-during-capture path is
|
|
425
|
+
// tracked separately for that.
|
|
175
426
|
const urlMatch = /^url\((?:"|')?([^"')]+)(?:"|')?\)$/i.exec(src);
|
|
176
|
-
if (urlMatch == null)
|
|
177
|
-
|
|
427
|
+
if (urlMatch == null) {
|
|
428
|
+
if (!/-gradient\(/i.test(src))
|
|
429
|
+
return { svg: "", usedIds: 0 };
|
|
430
|
+
return renderBorderImageGradient(el, indent, idPrefix, defsParts, clipIdx, src);
|
|
431
|
+
}
|
|
178
432
|
const url = urlMatch[1];
|
|
179
433
|
const natW = el.styles.borderImageIntrinsicWidth ?? 0;
|
|
180
434
|
const natH = el.styles.borderImageIntrinsicHeight ?? 0;
|
|
@@ -246,8 +500,8 @@ export function renderBorderImage(el, indent, idPrefix, defsParts, clipIdx) {
|
|
|
246
500
|
const boxH = el.height + ot + ob;
|
|
247
501
|
// Repeat policy per axis (tokens order: H V; fallback: single token applies to both).
|
|
248
502
|
const repeatTokens = (el.styles.borderImageRepeat ?? "stretch").trim().split(/\s+/);
|
|
249
|
-
const rH = (repeatTokens[0]
|
|
250
|
-
const rV =
|
|
503
|
+
const rH = normalizeBorderImageRepeat((repeatTokens[0] ?? "stretch").toLowerCase());
|
|
504
|
+
const rV = repeatTokens[1] != null && repeatTokens[1] !== "" ? normalizeBorderImageRepeat(repeatTokens[1].toLowerCase()) : rH;
|
|
251
505
|
// Slot geometry (in element-absolute coords).
|
|
252
506
|
const x0 = boxX, x1 = boxX + wl, x2 = boxX + boxW - wr, x3 = boxX + boxW;
|
|
253
507
|
const y0 = boxY, y1 = boxY + wt, y2 = boxY + boxH - wb, y3 = boxY + boxH;
|
|
@@ -304,17 +558,32 @@ export function renderBorderImage(el, indent, idPrefix, defsParts, clipIdx) {
|
|
|
304
558
|
tileH = dhSlot / count;
|
|
305
559
|
}
|
|
306
560
|
}
|
|
307
|
-
//
|
|
308
|
-
//
|
|
561
|
+
// DM-795: `space` tiles the source N whole times with equal gaps between
|
|
562
|
+
// tiles AND half-gaps at each end (CSS Images 3 §6.1.3). Compute N =
|
|
563
|
+
// floor(slot / tile); if N === 0 the tile is too big for the slot and
|
|
564
|
+
// the spec says no border is drawn for that side, so bail. Otherwise set
|
|
565
|
+
// `patternW = dwSlot / N` so cells span the slot evenly, and offset the
|
|
566
|
+
// pattern start by half a gap. The `<image>` inside the cell needs a
|
|
567
|
+
// `<clipPath>` clipped to the slice region (0, 0, tileW, tileH) —
|
|
568
|
+
// otherwise the image extends past the slice into the gap, painting
|
|
569
|
+
// source pixels beyond the slice region instead of transparent gap.
|
|
309
570
|
let patternW = tileW, patternH = tileH;
|
|
571
|
+
let patternX = dxSlot, patternY = dySlot;
|
|
310
572
|
if (mode === "space") {
|
|
311
573
|
if (axis === "x") {
|
|
312
|
-
const count = Math.
|
|
574
|
+
const count = Math.floor(dwSlot / tileW);
|
|
575
|
+
if (count <= 0)
|
|
576
|
+
return;
|
|
313
577
|
patternW = dwSlot / count;
|
|
578
|
+
// Per spec, half-gap at each end: shift pattern start by `(patternW − tileW) / 2`.
|
|
579
|
+
patternX = dxSlot + (patternW - tileW) / 2;
|
|
314
580
|
}
|
|
315
581
|
else {
|
|
316
|
-
const count = Math.
|
|
582
|
+
const count = Math.floor(dhSlot / tileH);
|
|
583
|
+
if (count <= 0)
|
|
584
|
+
return;
|
|
317
585
|
patternH = dhSlot / count;
|
|
586
|
+
patternY = dySlot + (patternH - tileH) / 2;
|
|
318
587
|
}
|
|
319
588
|
}
|
|
320
589
|
const patId = `${idPrefix}bip${clipIdx + usedIds}`;
|
|
@@ -326,7 +595,17 @@ export function renderBorderImage(el, indent, idPrefix, defsParts, clipIdx) {
|
|
|
326
595
|
const inImgY = -sy * imgScaleY;
|
|
327
596
|
const inImgW = natW * imgScaleX;
|
|
328
597
|
const inImgH = natH * imgScaleY;
|
|
329
|
-
|
|
598
|
+
// DM-795: clip the image to the slice region so `space` mode shows
|
|
599
|
+
// transparent gaps between tiles instead of bleeding adjacent source
|
|
600
|
+
// pixels into the gap. The clipPath is scoped to the pattern cell at
|
|
601
|
+
// (0, 0) - (tileW, tileH) and references the image inside the pattern.
|
|
602
|
+
const clipBgId = `${idPrefix}bic${clipIdx + usedIds}`;
|
|
603
|
+
usedIds++;
|
|
604
|
+
const clipDef = mode === "space"
|
|
605
|
+
? `<clipPath id="${clipBgId}"><rect x="0" y="0" width="${r(tileW)}" height="${r(tileH)}" /></clipPath>`
|
|
606
|
+
: "";
|
|
607
|
+
const imgClip = mode === "space" ? ` clip-path="url(#${clipBgId})"` : "";
|
|
608
|
+
defsParts.push(`<pattern id="${patId}" patternUnits="userSpaceOnUse" x="${r(patternX)}" y="${r(patternY)}" width="${r(patternW)}" height="${r(patternH)}">${clipDef}<image href="${esc(embedResizedDataUri(url, inImgW, inImgH))}" x="${r(inImgX)}" y="${r(inImgY)}" width="${r(inImgW)}" height="${r(inImgH)}" preserveAspectRatio="none"${imgClip} /></pattern>`);
|
|
330
609
|
parts.push(`${indent}<rect x="${r(dxSlot)}" y="${r(dySlot)}" width="${r(dwSlot)}" height="${r(dhSlot)}" fill="url(#${patId})" />`);
|
|
331
610
|
};
|
|
332
611
|
// Corners: always stretched (CSS spec).
|
|
@@ -352,14 +631,100 @@ export function renderBorderImage(el, indent, idPrefix, defsParts, clipIdx) {
|
|
|
352
631
|
emitTiledSliceEdge(x0, y1, wl, y2 - y1, sxL, syC, sl, syH_C, "y", rV);
|
|
353
632
|
emitTiledSliceEdge(x2, y1, wr, y2 - y1, sxR, syC, sr, syH_C, "y", rV);
|
|
354
633
|
}
|
|
355
|
-
// Center (only if
|
|
634
|
+
// Center (only if `fill`). Per CSS Backgrounds 3 §6.1.3 the middle slice
|
|
635
|
+
// is tiled in both directions when `border-image-repeat` is non-stretch,
|
|
636
|
+
// using the SAME tile sizing as the corresponding edge — horizontal axis
|
|
637
|
+
// matches the top edge derivation (tileW_natural = sxW_C × wt / st),
|
|
638
|
+
// vertical matches the left edge derivation (tileH_natural = syH_C × wl / sl).
|
|
639
|
+
// Single stretched <image> for stretch×stretch; otherwise a 2D <pattern>.
|
|
356
640
|
if (fillCenter) {
|
|
641
|
+
const dwCenter = x2 - x1;
|
|
642
|
+
const dhCenter = y2 - y1;
|
|
357
643
|
if (rH === "stretch" && rV === "stretch") {
|
|
358
|
-
emitStretchedSlice(x1, y1,
|
|
644
|
+
emitStretchedSlice(x1, y1, dwCenter, dhCenter, sxC, syC, sxW_C, syH_C);
|
|
359
645
|
}
|
|
360
|
-
else {
|
|
361
|
-
//
|
|
362
|
-
|
|
646
|
+
else if (dwCenter > 0 && dhCenter > 0 && sxW_C > 0 && syH_C > 0 && st > 0 && sl > 0) {
|
|
647
|
+
// Per-axis tile size.
|
|
648
|
+
const tileWNatural = sxW_C * (wt / st);
|
|
649
|
+
const tileHNatural = syH_C * (wl / sl);
|
|
650
|
+
let tileW, tileH;
|
|
651
|
+
let patternW, patternH;
|
|
652
|
+
let tileOffX = 0, tileOffY = 0;
|
|
653
|
+
// Horizontal.
|
|
654
|
+
if (rH === "stretch") {
|
|
655
|
+
tileW = dwCenter;
|
|
656
|
+
patternW = dwCenter;
|
|
657
|
+
}
|
|
658
|
+
else if (rH === "round") {
|
|
659
|
+
const count = Math.max(1, Math.round(dwCenter / tileWNatural));
|
|
660
|
+
tileW = dwCenter / count;
|
|
661
|
+
patternW = tileW;
|
|
662
|
+
}
|
|
663
|
+
else if (rH === "space") {
|
|
664
|
+
const count = Math.floor(dwCenter / tileWNatural);
|
|
665
|
+
if (count <= 0) {
|
|
666
|
+
tileW = 0;
|
|
667
|
+
patternW = 0;
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
tileW = tileWNatural;
|
|
671
|
+
patternW = dwCenter / count;
|
|
672
|
+
tileOffX = (patternW - tileW) / 2;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
else { // "repeat"
|
|
676
|
+
tileW = tileWNatural;
|
|
677
|
+
patternW = tileWNatural;
|
|
678
|
+
}
|
|
679
|
+
// Vertical.
|
|
680
|
+
if (rV === "stretch") {
|
|
681
|
+
tileH = dhCenter;
|
|
682
|
+
patternH = dhCenter;
|
|
683
|
+
}
|
|
684
|
+
else if (rV === "round") {
|
|
685
|
+
const count = Math.max(1, Math.round(dhCenter / tileHNatural));
|
|
686
|
+
tileH = dhCenter / count;
|
|
687
|
+
patternH = tileH;
|
|
688
|
+
}
|
|
689
|
+
else if (rV === "space") {
|
|
690
|
+
const count = Math.floor(dhCenter / tileHNatural);
|
|
691
|
+
if (count <= 0) {
|
|
692
|
+
tileH = 0;
|
|
693
|
+
patternH = 0;
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
tileH = tileHNatural;
|
|
697
|
+
patternH = dhCenter / count;
|
|
698
|
+
tileOffY = (patternH - tileH) / 2;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
tileH = tileHNatural;
|
|
703
|
+
patternH = tileHNatural;
|
|
704
|
+
}
|
|
705
|
+
if (tileW > 0 && tileH > 0 && patternW > 0 && patternH > 0) {
|
|
706
|
+
const imgScaleX = tileW / sxW_C;
|
|
707
|
+
const imgScaleY = tileH / syH_C;
|
|
708
|
+
const inImgX = -sxC * imgScaleX + tileOffX;
|
|
709
|
+
const inImgY = -syC * imgScaleY + tileOffY;
|
|
710
|
+
const inImgW = natW * imgScaleX;
|
|
711
|
+
const inImgH = natH * imgScaleY;
|
|
712
|
+
const patId = `${idPrefix}bipc${clipIdx + usedIds}`;
|
|
713
|
+
usedIds++;
|
|
714
|
+
// For `space` mode, clip the image to the visible tile region so
|
|
715
|
+
// gaps stay transparent (mirrors the edge-tile fix from DM-795).
|
|
716
|
+
const needsClip = rH === "space" || rV === "space";
|
|
717
|
+
let clipDef = "";
|
|
718
|
+
let imgClip = "";
|
|
719
|
+
if (needsClip) {
|
|
720
|
+
const clipBgId = `${idPrefix}bicc${clipIdx + usedIds}`;
|
|
721
|
+
usedIds++;
|
|
722
|
+
clipDef = `<clipPath id="${clipBgId}"><rect x="${r(tileOffX)}" y="${r(tileOffY)}" width="${r(tileW)}" height="${r(tileH)}" /></clipPath>`;
|
|
723
|
+
imgClip = ` clip-path="url(#${clipBgId})"`;
|
|
724
|
+
}
|
|
725
|
+
defsParts.push(`<pattern id="${patId}" patternUnits="userSpaceOnUse" x="${r(x1)}" y="${r(y1)}" width="${r(patternW)}" height="${r(patternH)}">${clipDef}<image href="${esc(embedResizedDataUri(url, inImgW, inImgH))}" x="${r(inImgX)}" y="${r(inImgY)}" width="${r(inImgW)}" height="${r(inImgH)}" preserveAspectRatio="none"${imgClip} /></pattern>`);
|
|
726
|
+
parts.push(`${indent}<rect x="${r(x1)}" y="${r(y1)}" width="${r(dwCenter)}" height="${r(dhCenter)}" fill="url(#${patId})" />`);
|
|
727
|
+
}
|
|
363
728
|
}
|
|
364
729
|
}
|
|
365
730
|
return { svg: parts.join("\n"), usedIds };
|
|
@@ -93,18 +93,6 @@ export declare function rewriteFragmentMaskDef(outerHTML: string, outputId: stri
|
|
|
93
93
|
* DM-493.
|
|
94
94
|
*/
|
|
95
95
|
export declare function positionFragmentMaskDef(rewrittenOuterHTML: string, elX: number, elY: number, elW: number, elH: number): string;
|
|
96
|
-
/**
|
|
97
|
-
* Translate a CSS mask-image value + mask-* siblings into an SVG <mask>.
|
|
98
|
-
* Handles single-layer gradients and url() sources. Position/size/repeat are
|
|
99
|
-
* applied via an internal <pattern> for url sources; gradients use direct
|
|
100
|
-
* gradient fills sized to the element box.
|
|
101
|
-
*
|
|
102
|
-
* SVG <mask> uses luminance by default (bright pixels visible). CSS mask-mode
|
|
103
|
-
* 'alpha' makes the alpha channel control visibility. We set mask-type on the
|
|
104
|
-
* <mask> element accordingly. Note: Chromium may render mask-mode:'match-source'
|
|
105
|
-
* differently depending on the source; we pick alpha for gradients and url()
|
|
106
|
-
* (common case) and respect explicit mask-mode when given.
|
|
107
|
-
*/
|
|
108
96
|
export declare function buildMaskDef(id: string, maskImage: string, elX: number, elY: number, w: number, h: number, maskMode: string, sizeCss: string, posCss: string, repeatCss: string, compositeCss: string,
|
|
109
97
|
/** DM-494: lookup table for `mask-image: element(#id)` references. Optional —
|
|
110
98
|
* callers without element() refs can omit it. The renderer's main caller
|
|
@@ -129,4 +117,15 @@ elementRasters?: ReadonlyMap<string, MaskRasterRef>): {
|
|
|
129
117
|
* map to xMin/xMid/xMax + yMin/yMid/yMax. Percentages are bucketed to thirds
|
|
130
118
|
* since SVG has no finer-grained alignment.
|
|
131
119
|
*/
|
|
120
|
+
/**
|
|
121
|
+
* DM-819: rewrite an SVG-source data URI so its top-level `<svg>` declares
|
|
122
|
+
* `width=consumerW height=consumerH preserveAspectRatio="<par>"`. Chrome
|
|
123
|
+
* ignores the outer `<image>`'s `preserveAspectRatio` when the source is
|
|
124
|
+
* SVG (paints at the SVG's own intrinsic size), but it does honor the
|
|
125
|
+
* embedded SVG's own preserveAspectRatio. Baking the alignment into the
|
|
126
|
+
* inner SVG lets `object-fit: cover` on an `<img>` referencing an SVG file
|
|
127
|
+
* actually slice. Returns the input unchanged for raster sources or when
|
|
128
|
+
* the inner SVG can't be parsed.
|
|
129
|
+
*/
|
|
130
|
+
export declare function rewriteSvgDataUriPreserveAspectRatio(dataUri: string, w: number, h: number, par: string): string;
|
|
132
131
|
export declare function preserveAspectRatioFor(fit: string | undefined, pos: string | undefined): string;
|