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/capture/types.d.ts
CHANGED
|
@@ -114,13 +114,51 @@ export interface TextSegment {
|
|
|
114
114
|
/** Pre-resolved srgb-form CSS color (or "transparent"). Renderer parses
|
|
115
115
|
* via parseColor and emits as the rect fill. */
|
|
116
116
|
backgroundColor?: string;
|
|
117
|
+
/** Raw CSS `background-image` value (gradient / url() / multiple
|
|
118
|
+
* layers) when the pseudo paints a gradient or image instead of (or in
|
|
119
|
+
* addition to) a flat color. DM-767: `.corner::after` accent stripes
|
|
120
|
+
* with `background: linear-gradient(...)` need this — without it the
|
|
121
|
+
* pseudoBox emit was skipped entirely. Renderer threads through
|
|
122
|
+
* `buildBackgroundLayerDef` the same way the regular-element
|
|
123
|
+
* background-image path does. */
|
|
124
|
+
backgroundImage?: string;
|
|
117
125
|
/** Resolved px border-radius (CSS shorthand — single value, four-corner
|
|
118
126
|
* symmetric). */
|
|
119
127
|
borderRadius?: number;
|
|
120
|
-
/** Uniform border (single side-width + color).
|
|
121
|
-
*
|
|
128
|
+
/** Uniform border (single side-width + color). When set, the renderer
|
|
129
|
+
* emits `<rect stroke=... stroke-width=...>` around the box. */
|
|
122
130
|
borderWidth?: number;
|
|
123
131
|
borderColor?: string;
|
|
132
|
+
/** Per-side border widths (px). Always populated; zero when the side
|
|
133
|
+
* carries no border. Renderer reads these together with the per-side
|
|
134
|
+
* colors below when no `borderWidth` (uniform) is set. */
|
|
135
|
+
borL?: number;
|
|
136
|
+
borR?: number;
|
|
137
|
+
borT?: number;
|
|
138
|
+
borB?: number;
|
|
139
|
+
/** Per-side border colors. Set ONLY when the pseudo has a non-uniform
|
|
140
|
+
* border (e.g. `border-bottom: 1px solid …` only). For uniform borders
|
|
141
|
+
* the renderer uses `borderColor` + `borderWidth` and the per-side
|
|
142
|
+
* fields are undefined. Slashdot's `.carouselHeading::after` is the
|
|
143
|
+
* motivating fixture — a 1px translucent-white border-bottom on the
|
|
144
|
+
* italic "Most Discussed" heading. */
|
|
145
|
+
borderTopColor?: string;
|
|
146
|
+
borderRightColor?: string;
|
|
147
|
+
borderBottomColor?: string;
|
|
148
|
+
borderLeftColor?: string;
|
|
149
|
+
/** DM-783: pseudo's own `transform` (rotate/scale/translate/matrix/skew).
|
|
150
|
+
* Captured verbatim from `getComputedStyle(host, '::before').transform`,
|
|
151
|
+
* which Chrome returns in resolved `matrix(a,b,c,d,e,f)` form — pasteable
|
|
152
|
+
* directly into an SVG `<g transform="…">` wrapper. Renderer wraps the
|
|
153
|
+
* pseudoBox rect + glyph emit so rotate(45deg) on a `::before { border-
|
|
154
|
+
* right; border-bottom }` paints as a check-mark (the rotation pivots
|
|
155
|
+
* around the box center per `transformOrigin`). */
|
|
156
|
+
transform?: string;
|
|
157
|
+
/** Resolved px transform-origin (e.g. `"50px 50px"` for a 100×100 box's
|
|
158
|
+
* default `50% 50%`). Renderer pre-bakes a translate-transform-translate
|
|
159
|
+
* matrix so the rotation/scale pivots around the captured origin instead
|
|
160
|
+
* of (0, 0). When undefined, renderer defaults to the box center. */
|
|
161
|
+
transformOrigin?: string;
|
|
124
162
|
};
|
|
125
163
|
}
|
|
126
164
|
export interface CapturedElement {
|
|
@@ -194,6 +232,14 @@ export interface CapturedElement {
|
|
|
194
232
|
borderCollapse: string;
|
|
195
233
|
overflowX: string;
|
|
196
234
|
overflowY: string;
|
|
235
|
+
/**
|
|
236
|
+
* CSS `overflow-clip-margin` shorthand value as computed by Chrome
|
|
237
|
+
* (e.g. `"20px"`, `"content-box 12px"`, or the empty string when the
|
|
238
|
+
* default `0px` resolves and the element doesn't paint outside its
|
|
239
|
+
* reference box). Only takes effect when `overflow: clip` (DM-761) —
|
|
240
|
+
* `hidden` ignores it per CSS Overflow 3.
|
|
241
|
+
*/
|
|
242
|
+
overflowClipMargin?: string;
|
|
197
243
|
scrollbarGutter: string;
|
|
198
244
|
/** el.scrollHeight / scrollWidth vs client* — used to decide whether to paint a scrollbar. */
|
|
199
245
|
scrollWidth: number;
|
|
@@ -226,6 +272,36 @@ export interface CapturedElement {
|
|
|
226
272
|
maskPosition: string;
|
|
227
273
|
maskRepeat: string;
|
|
228
274
|
maskComposite: string;
|
|
275
|
+
/**
|
|
276
|
+
* CSS `mask-clip` — the box (border-box / padding-box / content-box / etc.)
|
|
277
|
+
* the mask painted area is clipped to. Defaults to `border-box`. Captured
|
|
278
|
+
* separately from `mask-origin` because Chromium retains both verbatim
|
|
279
|
+
* (mask-clip controls visibility, mask-origin controls layer positioning).
|
|
280
|
+
*/
|
|
281
|
+
maskClip?: string;
|
|
282
|
+
/**
|
|
283
|
+
* CSS `mask-border-source` / legacy `-webkit-mask-box-image` source.
|
|
284
|
+
* Chrome exposes this only via the legacy webkit name (DM-758). The
|
|
285
|
+
* renderer routes it through the mask-image pipeline ONLY for the
|
|
286
|
+
* "simple" 9-slice cases (`slice 0 fill / 0 / 0` and `slice 1 fill / 0
|
|
287
|
+
* / 0`) where the entire source is used as a stretched full-element
|
|
288
|
+
* mask. Real 9-slice tiling (non-zero `width` / `outset`, `round` /
|
|
289
|
+
* `space` repeat) needs its own implementation.
|
|
290
|
+
*/
|
|
291
|
+
maskBorderSource?: string;
|
|
292
|
+
/** Resolved `-webkit-mask-box-image-slice` (e.g. `"1 fill"`, `"30 fill"`). */
|
|
293
|
+
maskBorderSlice?: string;
|
|
294
|
+
/** Resolved `-webkit-mask-box-image-width` (e.g. `"0"`, `"20px"`). */
|
|
295
|
+
maskBorderWidth?: string;
|
|
296
|
+
/** Resolved `-webkit-mask-box-image-outset` (e.g. `"0"`, `"15px"`). */
|
|
297
|
+
maskBorderOutset?: string;
|
|
298
|
+
/** Resolved `-webkit-mask-box-image-repeat` (`stretch` / `repeat` /
|
|
299
|
+
* `round` / `space`, optionally one per axis). DM-793. */
|
|
300
|
+
maskBorderRepeat?: string;
|
|
301
|
+
/** Intrinsic dimensions of the `mask-border-source` asset (px). Same
|
|
302
|
+
* probe pattern as `borderImageIntrinsicWidth`. DM-793. */
|
|
303
|
+
maskBorderIntrinsicWidth?: number;
|
|
304
|
+
maskBorderIntrinsicHeight?: number;
|
|
229
305
|
listStyleType: string;
|
|
230
306
|
listStyleImage: string;
|
|
231
307
|
listStylePosition: string;
|
|
@@ -239,6 +315,14 @@ export interface CapturedElement {
|
|
|
239
315
|
backgroundClip: string;
|
|
240
316
|
backgroundOrigin: string;
|
|
241
317
|
backgroundAttachment: string;
|
|
318
|
+
/**
|
|
319
|
+
* CSS `background-blend-mode` — per-layer blend mode (comma-separated to
|
|
320
|
+
* match the layer count). Captured verbatim from `getComputedStyle`. The
|
|
321
|
+
* renderer applies each layer's mode as `style="mix-blend-mode:<mode>"`
|
|
322
|
+
* on the layer's `<rect>`, wrapped in a `<g style="isolation:isolate">`
|
|
323
|
+
* so the blend doesn't escape the element's bg-layer stack.
|
|
324
|
+
*/
|
|
325
|
+
backgroundBlendMode?: string;
|
|
242
326
|
/**
|
|
243
327
|
* CSS `-webkit-text-fill-color`. When `backgroundClip` is `text` the
|
|
244
328
|
* common pattern is `webkit-text-fill-color: transparent` so the
|
|
@@ -248,6 +332,25 @@ export interface CapturedElement {
|
|
|
248
332
|
* when the rendered text is actually transparent. DM-462.
|
|
249
333
|
*/
|
|
250
334
|
webkitTextFillColor?: string;
|
|
335
|
+
/**
|
|
336
|
+
* DM-749: Stripe / Resend pattern — when an element has
|
|
337
|
+
* `webkit-text-fill-color: transparent` but its own background-image is
|
|
338
|
+
* `none`, the gradient lives on an ANCESTOR with `background-clip:
|
|
339
|
+
* text`. Chrome's paint propagates that gradient through descendant
|
|
340
|
+
* glyphs. Captured as the resolved `background-image` string of the
|
|
341
|
+
* nearest ancestor with `background-clip: text` (walked up to 8 levels).
|
|
342
|
+
*/
|
|
343
|
+
inheritedTextFillGradient?: string;
|
|
344
|
+
/** `-webkit-text-stroke-width` (e.g. "2px"). DM-719. */
|
|
345
|
+
webkitTextStrokeWidth?: string;
|
|
346
|
+
/** `-webkit-text-stroke-color` (e.g. "rgb(220,38,38)"). DM-719. */
|
|
347
|
+
webkitTextStrokeColor?: string;
|
|
348
|
+
/** `paint-order` (e.g. "stroke fill"). Controls whether the text stroke
|
|
349
|
+
* paints before or after the fill — `stroke fill` puts the stroke
|
|
350
|
+
* UNDER the fill so the fill rests on top of half the stroke width,
|
|
351
|
+
* eliminating the chunky "fill-on-top-of-stroke" artifact at large
|
|
352
|
+
* stroke widths. DM-719. */
|
|
353
|
+
paintOrder?: string;
|
|
251
354
|
paddingTop: string;
|
|
252
355
|
paddingRight: string;
|
|
253
356
|
paddingBottom: string;
|
|
@@ -457,6 +560,14 @@ export interface CapturedElement {
|
|
|
457
560
|
transformCreatesSc?: boolean;
|
|
458
561
|
/** CSS transform-style. `preserve-3d` (or anything != `flat`) creates a stacking context per CSS Transforms 2 §4 (DM-589). */
|
|
459
562
|
transformStyle?: string;
|
|
563
|
+
/**
|
|
564
|
+
* DM-751: extracted Z translation from `matrix3d(...)` when the
|
|
565
|
+
* element's transform has a non-zero translateZ component. Used by the
|
|
566
|
+
* paint-order sort when the parent has `transform-style: preserve-3d`
|
|
567
|
+
* (CSS Transforms 2 §6 sorts children by Z in 3D space, not z-index).
|
|
568
|
+
* SVG can't render perspective, so this is paint-order only.
|
|
569
|
+
*/
|
|
570
|
+
translateZ?: number;
|
|
460
571
|
/** CSS writing-mode (`horizontal-tb` | `vertical-rl` | `vertical-lr` | `sideways-rl` | `sideways-lr`). */
|
|
461
572
|
writingMode?: string;
|
|
462
573
|
/** CSS text-orientation (`mixed` | `upright` | `sideways`). Used in vertical writing-modes. */
|
|
@@ -518,7 +629,45 @@ export interface CapturedElement {
|
|
|
518
629
|
* `decoration_line_painter.cc`, only solid + double underlines honor
|
|
519
630
|
* skip-ink; dashed / dotted / wavy ignore it. DM-446. */
|
|
520
631
|
textDecorationSkipInk?: string;
|
|
632
|
+
/**
|
|
633
|
+
* `box-decoration-break` — `slice` (default) or `clone`. Controls how
|
|
634
|
+
* inline elements that wrap across multiple line boxes paint their
|
|
635
|
+
* background / border / padding / shadow: `slice` paints the box once
|
|
636
|
+
* across all fragments (first fragment gets the left side, last gets the
|
|
637
|
+
* right side); `clone` paints a complete box on every fragment. Captured
|
|
638
|
+
* so the renderer can split the per-fragment paint at line-box boundaries
|
|
639
|
+
* when `inlineFragments` is present.
|
|
640
|
+
*/
|
|
641
|
+
boxDecorationBreak?: string;
|
|
521
642
|
};
|
|
643
|
+
/**
|
|
644
|
+
* Per-line-fragment rects (viewport-relative px) for inline elements that
|
|
645
|
+
* wrap across multiple line boxes. Populated by capture when the element
|
|
646
|
+
* is `display: inline` AND has a non-transparent background or non-zero
|
|
647
|
+
* border AND `el.getClientRects().length > 1`. When present, the renderer
|
|
648
|
+
* paints the background + border per-fragment instead of once across the
|
|
649
|
+
* element's bbox — without this, an inline span like `<span class="hl">…
|
|
650
|
+
* wrapping text …</span>` paints a single rectangle covering the whole
|
|
651
|
+
* logical inline (typically the full container width) and the text floats
|
|
652
|
+
* outside / behind the painted background. Slice vs clone semantics are
|
|
653
|
+
* driven by `styles.boxDecorationBreak`. See `docs/01-fidelity.md`.
|
|
654
|
+
*/
|
|
655
|
+
inlineFragments?: Array<{
|
|
656
|
+
x: number;
|
|
657
|
+
y: number;
|
|
658
|
+
width: number;
|
|
659
|
+
height: number;
|
|
660
|
+
}>;
|
|
661
|
+
/** DM-754: the fragmentation axis that produced the `inlineFragments`
|
|
662
|
+
* entries. `"inline"` — the element is `display: inline` and wrapped onto
|
|
663
|
+
* multiple line boxes (the original DM-721 case); slice mode suppresses
|
|
664
|
+
* the LEFT side on non-first fragments and the RIGHT side on non-last.
|
|
665
|
+
* `"block"` — the element is block-level inside a multi-column container
|
|
666
|
+
* ancestor (DM-754); slice mode suppresses TOP on non-first and BOTTOM on
|
|
667
|
+
* non-last. Both axes produce vertically-stacked frag rects in practice,
|
|
668
|
+
* so we can't distinguish them geometrically at render time. Defaults to
|
|
669
|
+
* `"inline"` when undefined (backwards-compatible with pre-DM-754 captures). */
|
|
670
|
+
fragmentAxis?: "inline" | "block";
|
|
522
671
|
children: CapturedElement[];
|
|
523
672
|
imageSrc?: string;
|
|
524
673
|
/** Intrinsic pixel dimensions of <img>, used for object-fit: none. */
|
|
@@ -618,6 +767,18 @@ export interface CapturedElement {
|
|
|
618
767
|
height: number;
|
|
619
768
|
dataUri?: string;
|
|
620
769
|
};
|
|
770
|
+
/**
|
|
771
|
+
* DM-680: per-axis cumulative ancestor scale, present ONLY when the element
|
|
772
|
+
* sits inside an anisotropically scaled subtree (e.g. `transform: scale(1.3,
|
|
773
|
+
* 0.8)`). The geometric mean is already folded into fontSize / fontAscent /
|
|
774
|
+
* fontDescent at capture time — these fields drive a per-axis correction
|
|
775
|
+
* `<g transform="scale(cx, cy)">` around the text emission so glyphs render
|
|
776
|
+
* with the same width / height stretch Chrome paints. Absent when the
|
|
777
|
+
* cumulative scale is isotropic (uniform scale, or no scale) — the
|
|
778
|
+
* geometric-mean handling already produces a faithful result there.
|
|
779
|
+
*/
|
|
780
|
+
cumScaleX?: number;
|
|
781
|
+
cumScaleY?: number;
|
|
621
782
|
/**
|
|
622
783
|
* For <canvas> / <video> / <iframe> / <object> / <embed>: a viewport-relative
|
|
623
784
|
* content-box rect (border-box minus border + padding) that
|
|
@@ -679,6 +840,18 @@ export interface CapturedElement {
|
|
|
679
840
|
* See `docs/21-mask-fragment-references.md`.
|
|
680
841
|
*/
|
|
681
842
|
maskDefs?: MaskFragmentDef[];
|
|
843
|
+
/**
|
|
844
|
+
* DM-826: Top-level (root only) collection of `<clipPath>` definitions
|
|
845
|
+
* referenced by fragment URLs (`clip-path: url("#id")`) anywhere in the
|
|
846
|
+
* captured tree. CAPTURE_SCRIPT resolves each fragment id via
|
|
847
|
+
* `document.getElementById` and serialises the `<clipPath>` element's
|
|
848
|
+
* `outerHTML` here. The renderer copies these into the output `<defs>`
|
|
849
|
+
* with id rewriting so a captured `<clipPath id="hex">` becomes a
|
|
850
|
+
* domotion-prefixed clip-path def referenced by elements that point at
|
|
851
|
+
* `#hex`. Same-document only — external `.svg#fragment` refs are
|
|
852
|
+
* deferred. See `docs/39-clip-path-fragment-references.md`.
|
|
853
|
+
*/
|
|
854
|
+
clipPathDefs?: ClipPathFragmentDef[];
|
|
682
855
|
/**
|
|
683
856
|
* DM-494: Raster snapshots of elements referenced by `mask-image:
|
|
684
857
|
* element(#id)`. Top-level (root only) — same-document only (cross-document
|
|
@@ -692,6 +865,38 @@ export interface CapturedElement {
|
|
|
692
865
|
* `docs/22-mask-element-paint-references.md`.
|
|
693
866
|
*/
|
|
694
867
|
maskRasters?: MaskRasterRef[];
|
|
868
|
+
/**
|
|
869
|
+
* DM-579 box-only pseudo-elements: empty-content `::before` / `::after`
|
|
870
|
+
* whose effective rect + per-side borders + background are captured for
|
|
871
|
+
* decorative-separator emission. The renderer in `element-tree-to-svg.ts`
|
|
872
|
+
* emits one `<rect>` per pseudoBox plus up to four `<line>`s for visible
|
|
873
|
+
* borders. Captured per element (not just root). Optional — only emitted
|
|
874
|
+
* when at least one such pseudo exists on this element.
|
|
875
|
+
*/
|
|
876
|
+
pseudoBoxes?: PseudoBox[];
|
|
877
|
+
}
|
|
878
|
+
export interface PseudoBox {
|
|
879
|
+
x: number;
|
|
880
|
+
y: number;
|
|
881
|
+
width: number;
|
|
882
|
+
height: number;
|
|
883
|
+
backgroundColor?: string;
|
|
884
|
+
backgroundImage?: string;
|
|
885
|
+
borderTopWidth?: number;
|
|
886
|
+
borderTopColor?: string;
|
|
887
|
+
borderTopStyle?: string;
|
|
888
|
+
borderRightWidth?: number;
|
|
889
|
+
borderRightColor?: string;
|
|
890
|
+
borderRightStyle?: string;
|
|
891
|
+
borderBottomWidth?: number;
|
|
892
|
+
borderBottomColor?: string;
|
|
893
|
+
borderBottomStyle?: string;
|
|
894
|
+
borderLeftWidth?: number;
|
|
895
|
+
borderLeftColor?: string;
|
|
896
|
+
borderLeftStyle?: string;
|
|
897
|
+
borderRadius?: number;
|
|
898
|
+
transform?: string;
|
|
899
|
+
transformOrigin?: string;
|
|
695
900
|
}
|
|
696
901
|
export interface MaskFragmentDef {
|
|
697
902
|
/** Original DOM id of the captured `<mask>` element. */
|
|
@@ -699,6 +904,12 @@ export interface MaskFragmentDef {
|
|
|
699
904
|
/** Verbatim `outerHTML` of the captured `<mask>` element. */
|
|
700
905
|
outerHTML: string;
|
|
701
906
|
}
|
|
907
|
+
export interface ClipPathFragmentDef {
|
|
908
|
+
/** Original DOM id of the captured `<clipPath>` element. */
|
|
909
|
+
id: string;
|
|
910
|
+
/** Verbatim `outerHTML` of the captured `<clipPath>` element. */
|
|
911
|
+
outerHTML: string;
|
|
912
|
+
}
|
|
702
913
|
export interface MaskRasterRef {
|
|
703
914
|
/** DOM id referenced by `mask-image: element(#id)` — used by the renderer
|
|
704
915
|
* to look up the raster from the layer reference. */
|
package/dist/cli/animate.js
CHANGED
|
@@ -38,8 +38,8 @@ export async function runAnimate(args, help) {
|
|
|
38
38
|
const configPath = resolve(positionals[0]);
|
|
39
39
|
if (!existsSync(configPath))
|
|
40
40
|
throw new Error(`animate: config not found: ${configPath}`);
|
|
41
|
-
const
|
|
42
|
-
validateAnimateConfig(
|
|
41
|
+
const cfgRaw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
42
|
+
const cfg = validateAnimateConfig(cfgRaw);
|
|
43
43
|
const configDir = dirname(configPath);
|
|
44
44
|
const log = makeLogger(values.quiet === true);
|
|
45
45
|
log(`Launching Chromium…`);
|
|
@@ -208,36 +208,171 @@ async function runActions(page, actions) {
|
|
|
208
208
|
throw new Error(`animate: unknown action type "${a.type}"`);
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
const COLOR_SCHEMES = new Set(["light", "dark", "no-preference"]);
|
|
212
|
+
const TRANSITION_TYPES = new Set(["crossfade", "push-left", "scroll", "cut"]);
|
|
213
|
+
const OVERLAY_SLIDE_FROMS = new Set(["top", "bottom", "left", "right"]);
|
|
214
|
+
function isObject(v) {
|
|
215
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
216
|
+
}
|
|
217
|
+
function validateAnimateConfig(raw) {
|
|
218
|
+
if (!isObject(raw))
|
|
219
|
+
throw new Error("animate: config must be an object");
|
|
220
|
+
if (typeof raw.width !== "number" || typeof raw.height !== "number") {
|
|
213
221
|
throw new Error("animate: config requires numeric width and height");
|
|
214
222
|
}
|
|
215
|
-
if (
|
|
223
|
+
if (raw.output != null && typeof raw.output !== "string") {
|
|
224
|
+
throw new Error("animate: config.output must be a string when present");
|
|
225
|
+
}
|
|
226
|
+
if (raw.optimize != null && typeof raw.optimize !== "boolean") {
|
|
227
|
+
throw new Error("animate: config.optimize must be a boolean when present");
|
|
228
|
+
}
|
|
229
|
+
if (raw.mobile != null && typeof raw.mobile !== "boolean") {
|
|
230
|
+
throw new Error("animate: config.mobile must be a boolean when present");
|
|
231
|
+
}
|
|
232
|
+
if (raw.colorScheme != null && (typeof raw.colorScheme !== "string" || !COLOR_SCHEMES.has(raw.colorScheme))) {
|
|
233
|
+
throw new Error(`animate: config.colorScheme must be one of ${[...COLOR_SCHEMES].join(", ")}`);
|
|
234
|
+
}
|
|
235
|
+
if (!Array.isArray(raw.frames) || raw.frames.length === 0) {
|
|
216
236
|
throw new Error("animate: config.frames must be a non-empty array");
|
|
217
237
|
}
|
|
218
|
-
for (let i = 0; i <
|
|
219
|
-
const f =
|
|
238
|
+
for (let i = 0; i < raw.frames.length; i++) {
|
|
239
|
+
const f = raw.frames[i];
|
|
240
|
+
if (!isObject(f))
|
|
241
|
+
throw new Error(`animate: frames[${i}] must be an object`);
|
|
220
242
|
if (typeof f.input !== "string")
|
|
221
243
|
throw new Error(`animate: frames[${i}].input must be a string`);
|
|
222
244
|
if (typeof f.duration !== "number")
|
|
223
245
|
throw new Error(`animate: frames[${i}].duration must be a number`);
|
|
246
|
+
if (f.transition != null) {
|
|
247
|
+
if (!isObject(f.transition))
|
|
248
|
+
throw new Error(`animate: frames[${i}].transition must be an object`);
|
|
249
|
+
if (typeof f.transition.type !== "string" || !TRANSITION_TYPES.has(f.transition.type)) {
|
|
250
|
+
throw new Error(`animate: frames[${i}].transition.type must be one of ${[...TRANSITION_TYPES].join(", ")}`);
|
|
251
|
+
}
|
|
252
|
+
if (typeof f.transition.duration !== "number") {
|
|
253
|
+
throw new Error(`animate: frames[${i}].transition.duration must be a number`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (f.scrollTo != null) {
|
|
257
|
+
if (!Array.isArray(f.scrollTo) || f.scrollTo.length !== 2 || typeof f.scrollTo[0] !== "number" || typeof f.scrollTo[1] !== "number") {
|
|
258
|
+
throw new Error(`animate: frames[${i}].scrollTo must be a [number, number] tuple`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
224
261
|
if (f.scroll != null) {
|
|
262
|
+
if (!isObject(f.scroll))
|
|
263
|
+
throw new Error(`animate: frames[${i}].scroll must be an object`);
|
|
225
264
|
if (typeof f.scroll.pattern !== "string" || f.scroll.pattern.trim() === "") {
|
|
226
265
|
throw new Error(`animate: frames[${i}].scroll.pattern must be a non-empty string`);
|
|
227
266
|
}
|
|
228
|
-
// Parse the pattern eagerly so config errors surface before the run
|
|
229
|
-
// starts (instead of mid-Playwright session). Throws on invalid
|
|
230
|
-
// grammar with the original error including source position.
|
|
231
267
|
try {
|
|
232
268
|
parseScrollPattern(f.scroll.pattern);
|
|
233
269
|
}
|
|
234
270
|
catch (e) {
|
|
235
271
|
throw new Error(`animate: frames[${i}].scroll.pattern is invalid: ${e instanceof Error ? e.message : String(e)}`);
|
|
236
272
|
}
|
|
237
|
-
if (f.scroll.speed != null && (!Number.isFinite(f.scroll.speed) || f.scroll.speed <= 0)) {
|
|
273
|
+
if (f.scroll.speed != null && (typeof f.scroll.speed !== "number" || !Number.isFinite(f.scroll.speed) || f.scroll.speed <= 0)) {
|
|
238
274
|
throw new Error(`animate: frames[${i}].scroll.speed must be a positive number (px/s)`);
|
|
239
275
|
}
|
|
240
276
|
}
|
|
277
|
+
if (f.actions != null) {
|
|
278
|
+
if (!Array.isArray(f.actions))
|
|
279
|
+
throw new Error(`animate: frames[${i}].actions must be an array`);
|
|
280
|
+
f.actions.forEach((a, ai) => validateAction(a, i, ai));
|
|
281
|
+
}
|
|
282
|
+
if (f.animations != null) {
|
|
283
|
+
if (!Array.isArray(f.animations))
|
|
284
|
+
throw new Error(`animate: frames[${i}].animations must be an array`);
|
|
285
|
+
f.animations.forEach((a, ai) => validateFrameAnimation(a, i, ai));
|
|
286
|
+
}
|
|
287
|
+
if (f.overlays != null && !Array.isArray(f.overlays)) {
|
|
288
|
+
throw new Error(`animate: frames[${i}].overlays must be an array`);
|
|
289
|
+
}
|
|
290
|
+
// Overlay shape is validated lazily in `resolveSvgOverlays` via
|
|
291
|
+
// `validateOverlay` — each entry checked at use-site.
|
|
292
|
+
}
|
|
293
|
+
return raw;
|
|
294
|
+
}
|
|
295
|
+
function validateAction(a, frameIdx, ai) {
|
|
296
|
+
if (!isObject(a))
|
|
297
|
+
throw new Error(`animate: frames[${frameIdx}].actions[${ai}] must be an object`);
|
|
298
|
+
switch (a.type) {
|
|
299
|
+
case "click":
|
|
300
|
+
case "hover":
|
|
301
|
+
if (typeof a.selector !== "string")
|
|
302
|
+
throw new Error(`animate: frames[${frameIdx}].actions[${ai}] (${a.type}) requires string selector`);
|
|
303
|
+
return;
|
|
304
|
+
case "fill":
|
|
305
|
+
if (typeof a.selector !== "string" || typeof a.value !== "string") {
|
|
306
|
+
throw new Error(`animate: frames[${frameIdx}].actions[${ai}] (fill) requires string selector and value`);
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
case "press":
|
|
310
|
+
if (typeof a.key !== "string")
|
|
311
|
+
throw new Error(`animate: frames[${frameIdx}].actions[${ai}] (press) requires string key`);
|
|
312
|
+
return;
|
|
313
|
+
case "scroll":
|
|
314
|
+
if (a.x != null && typeof a.x !== "number")
|
|
315
|
+
throw new Error(`animate: frames[${frameIdx}].actions[${ai}] (scroll) x must be a number`);
|
|
316
|
+
if (a.y != null && typeof a.y !== "number")
|
|
317
|
+
throw new Error(`animate: frames[${frameIdx}].actions[${ai}] (scroll) y must be a number`);
|
|
318
|
+
return;
|
|
319
|
+
case "wait":
|
|
320
|
+
if (typeof a.ms !== "number")
|
|
321
|
+
throw new Error(`animate: frames[${frameIdx}].actions[${ai}] (wait) requires numeric ms`);
|
|
322
|
+
return;
|
|
323
|
+
default:
|
|
324
|
+
throw new Error(`animate: frames[${frameIdx}].actions[${ai}].type "${String(a.type)}" is not a recognised action`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function validateFrameAnimation(a, frameIdx, ai) {
|
|
328
|
+
if (!isObject(a))
|
|
329
|
+
throw new Error(`animate: frames[${frameIdx}].animations[${ai}] must be an object`);
|
|
330
|
+
if (typeof a.selector !== "string")
|
|
331
|
+
throw new Error(`animate: frames[${frameIdx}].animations[${ai}].selector must be a string`);
|
|
332
|
+
if (typeof a.property !== "string")
|
|
333
|
+
throw new Error(`animate: frames[${frameIdx}].animations[${ai}].property must be a string`);
|
|
334
|
+
if (typeof a.from !== "string")
|
|
335
|
+
throw new Error(`animate: frames[${frameIdx}].animations[${ai}].from must be a string`);
|
|
336
|
+
if (typeof a.to !== "string")
|
|
337
|
+
throw new Error(`animate: frames[${frameIdx}].animations[${ai}].to must be a string`);
|
|
338
|
+
if (typeof a.duration !== "number")
|
|
339
|
+
throw new Error(`animate: frames[${frameIdx}].animations[${ai}].duration must be a number`);
|
|
340
|
+
}
|
|
341
|
+
function validateOverlay(ov, frameIdx, oi) {
|
|
342
|
+
if (!isObject(ov))
|
|
343
|
+
throw new Error(`animate: frames[${frameIdx}].overlays[${oi}] must be an object`);
|
|
344
|
+
switch (ov.kind) {
|
|
345
|
+
case "typing":
|
|
346
|
+
if (typeof ov.text !== "string" || typeof ov.x !== "number" || typeof ov.y !== "number") {
|
|
347
|
+
throw new Error(`animate: frames[${frameIdx}].overlays[${oi}] (typing) requires text/x/y`);
|
|
348
|
+
}
|
|
349
|
+
return ov;
|
|
350
|
+
case "tap":
|
|
351
|
+
if (typeof ov.x !== "number" || typeof ov.y !== "number") {
|
|
352
|
+
throw new Error(`animate: frames[${frameIdx}].overlays[${oi}] (tap) requires numeric x and y`);
|
|
353
|
+
}
|
|
354
|
+
return ov;
|
|
355
|
+
case "svg":
|
|
356
|
+
if (typeof ov.src !== "string" || typeof ov.x !== "number" || typeof ov.y !== "number" || typeof ov.width !== "number" || typeof ov.height !== "number") {
|
|
357
|
+
throw new Error(`animate: frames[${frameIdx}].overlays[${oi}] (svg) requires src/x/y/width/height`);
|
|
358
|
+
}
|
|
359
|
+
if (ov.enter != null)
|
|
360
|
+
validateOverlaySlide(ov.enter, frameIdx, oi, "enter");
|
|
361
|
+
if (ov.exit != null)
|
|
362
|
+
validateOverlaySlide(ov.exit, frameIdx, oi, "exit");
|
|
363
|
+
return ov;
|
|
364
|
+
default:
|
|
365
|
+
throw new Error(`animate: frames[${frameIdx}].overlays[${oi}].kind "${String(ov.kind)}" is not a recognised overlay`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function validateOverlaySlide(s, frameIdx, oi, which) {
|
|
369
|
+
if (!isObject(s))
|
|
370
|
+
throw new Error(`animate: frames[${frameIdx}].overlays[${oi}].${which} must be an object`);
|
|
371
|
+
if (typeof s.from !== "string" || !OVERLAY_SLIDE_FROMS.has(s.from)) {
|
|
372
|
+
throw new Error(`animate: frames[${frameIdx}].overlays[${oi}].${which}.from must be one of ${[...OVERLAY_SLIDE_FROMS].join(", ")}`);
|
|
373
|
+
}
|
|
374
|
+
if (typeof s.duration !== "number") {
|
|
375
|
+
throw new Error(`animate: frames[${frameIdx}].overlays[${oi}].${which}.duration must be a number`);
|
|
241
376
|
}
|
|
242
377
|
}
|
|
243
378
|
/**
|
|
@@ -250,9 +385,10 @@ function resolveSvgOverlays(rawOverlays, configDir, frameIdx) {
|
|
|
250
385
|
return undefined;
|
|
251
386
|
const out = [];
|
|
252
387
|
let svgIdx = 0;
|
|
253
|
-
for (
|
|
254
|
-
|
|
255
|
-
|
|
388
|
+
for (let oi = 0; oi < rawOverlays.length; oi++) {
|
|
389
|
+
const validated = validateOverlay(rawOverlays[oi], frameIdx, oi);
|
|
390
|
+
if (validated.kind === "svg" && "src" in rawOverlays[oi]) {
|
|
391
|
+
const raw = rawOverlays[oi];
|
|
256
392
|
const srcPath = resolve(configDir, raw.src);
|
|
257
393
|
if (!existsSync(srcPath))
|
|
258
394
|
throw new Error(`animate: svg overlay file not found: ${srcPath}`);
|
|
@@ -268,7 +404,7 @@ function resolveSvgOverlays(rawOverlays, configDir, frameIdx) {
|
|
|
268
404
|
});
|
|
269
405
|
}
|
|
270
406
|
else {
|
|
271
|
-
out.push(
|
|
407
|
+
out.push(validated);
|
|
272
408
|
}
|
|
273
409
|
}
|
|
274
410
|
return out;
|
package/dist/mask.test.js
CHANGED
|
@@ -27,17 +27,22 @@ describe("buildMaskDef — single-layer gradient masks (DM-395)", () => {
|
|
|
27
27
|
it("radial-gradient mask centers correctly when sized + positioned", () => {
|
|
28
28
|
// mask-image: radial-gradient(circle, black 40%, transparent 40%);
|
|
29
29
|
// mask-size: 80px; mask-position: 25% 25%
|
|
30
|
-
// Element at (680, 240)
|
|
30
|
+
// Element at (680, 240, 180x120). DM-679: single-length mask-size is
|
|
31
|
+
// `width=80, height=auto`. For gradient layers (no intrinsic size)
|
|
32
|
+
// `auto` resolves to the container's corresponding axis per CSS
|
|
33
|
+
// Backgrounds 3 §3.7 + CSS Images 3 §6.2 — so the gradient box is
|
|
34
|
+
// 80×120 (height = container 120), not 80×80. Position 25% 25% then
|
|
35
|
+
// gives gx=680 + 0.25*(180-80)=705 and gy=240 + 0.25*(120-120)=240.
|
|
31
36
|
const r = buildMaskDef("m", "radial-gradient(circle, black 40%, transparent 40%)", 680, 240, 180, 120, "match-source", "80px", "25% 25%", "no-repeat", "add");
|
|
32
37
|
expect(r.def).toContain('x="705"');
|
|
33
|
-
expect(r.def).toContain('y="
|
|
38
|
+
expect(r.def).toContain('y="240"');
|
|
34
39
|
expect(r.def).toContain('width="80"');
|
|
35
|
-
expect(r.def).toContain('height="
|
|
36
|
-
// Center of the
|
|
40
|
+
expect(r.def).toContain('height="120"');
|
|
41
|
+
// Center of the 80x120 mask box: cx=745, cy=300.
|
|
37
42
|
expect(r.def).toMatch(/cx="745"/);
|
|
38
|
-
expect(r.def).toMatch(/cy="
|
|
39
|
-
// farthest-corner radius = sqrt(40^2 +
|
|
40
|
-
expect(r.def).toMatch(/r="
|
|
43
|
+
expect(r.def).toMatch(/cy="300"/);
|
|
44
|
+
// farthest-corner radius = sqrt(40^2 + 60^2) ≈ 72.111.
|
|
45
|
+
expect(r.def).toMatch(/r="72\.111"/);
|
|
41
46
|
});
|
|
42
47
|
it("mask-mode: alpha emits mask-type='alpha'", () => {
|
|
43
48
|
const r = buildMaskDef("m", "linear-gradient(45deg, black, transparent)", 0, 0, 180, 120, "alpha", "auto", "0% 0%", "repeat", "add");
|
package/dist/render/borders.d.ts
CHANGED
|
@@ -43,6 +43,15 @@ export declare function parseCornerRadii(styles: {
|
|
|
43
43
|
* that the inner corner is the outer corner pulled in by the adjacent border
|
|
44
44
|
* widths (top + left for TL, top + right for TR, etc.). */
|
|
45
45
|
export declare function insetCornerRadii(c: CornerRadii, top: number, right: number, bottom: number, left: number): CornerRadii;
|
|
46
|
+
/** Grow each corner radius outward by `spread` for an OUTSET box-shadow shape.
|
|
47
|
+
* Per CSS Backgrounds 3 §6.4 and Chromium's `FloatRoundedRect::Outset`, a
|
|
48
|
+
* corner whose source radius is zero STAYS sharp through any spread — only
|
|
49
|
+
* pre-curved corners grow. A naive `corner + spread` produces visibly
|
|
50
|
+
* rounded shadow corners on a sharp-cornered box (e.g. concentric outlines
|
|
51
|
+
* built from `box-shadow: 0 0 0 Npx`). Use this for outset shadow shapes;
|
|
52
|
+
* the dual inset case is already covered by `insetCornerRadii` shrinking to
|
|
53
|
+
* zero when the border eats past the radius. */
|
|
54
|
+
export declare function outsetCornerRadiiForShadow(c: CornerRadii, spread: number): CornerRadii;
|
|
46
55
|
/** Emit an SVG path `d` attribute for a rounded rectangle with per-corner radii.
|
|
47
56
|
* Path goes clockwise from the top-left, using elliptical arc commands at each
|
|
48
57
|
* corner. Zero-radius corners collapse to a sharp 90° join. */
|
|
@@ -82,19 +91,6 @@ export declare function dashArrayForStyle(style: string, width: number): string;
|
|
|
82
91
|
* them onto the nested `<svg>` is correct in both cases.
|
|
83
92
|
*/
|
|
84
93
|
export declare function injectSvgSize(svgHtml: string, w: number, h: number): string;
|
|
85
|
-
/**
|
|
86
|
-
* Render a CSS border-image 9-slice around the element's border box.
|
|
87
|
-
*
|
|
88
|
-
* Supports:
|
|
89
|
-
* - border-image-source: url(...) (gradient sources out of scope)
|
|
90
|
-
* - border-image-slice: t r b l (percent + length, optional 'fill')
|
|
91
|
-
* - border-image-width: per-side (falls back to element border-width)
|
|
92
|
-
* - border-image-outset: per-side
|
|
93
|
-
* - border-image-repeat: stretch / repeat / round / space
|
|
94
|
-
*
|
|
95
|
-
* Returns { svg, usedIds }. usedIds indicates how many clipIdx values were
|
|
96
|
-
* consumed so the caller can keep its own counter in sync.
|
|
97
|
-
*/
|
|
98
94
|
export declare function renderBorderImage(el: CapturedElement, indent: string, idPrefix: string, defsParts: string[], clipIdx: number): {
|
|
99
95
|
svg: string;
|
|
100
96
|
usedIds: number;
|