domotion-svg 0.13.3 → 0.15.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/README.md +2 -1
- package/dist/animation/animator.js +57 -9
- package/dist/animation/embed-namespace.d.ts +52 -0
- package/dist/animation/embed-namespace.js +89 -0
- package/dist/animation/overlay-schema.d.ts +2 -0
- package/dist/animation/overlay-schema.js +13 -2
- package/dist/capture/emoji.d.ts +37 -0
- package/dist/capture/emoji.js +41 -21
- package/dist/capture/script/dotted-circle-detect.js +9 -2
- package/dist/capture/script/emoji-detect.js +51 -5
- package/dist/capture/script/index.js +321 -2
- package/dist/capture/script/utils.js +12 -6
- package/dist/capture/script/walker/form-controls.d.ts +9 -0
- package/dist/capture/script/walker/form-controls.js +28 -0
- package/dist/capture/script/walker/pseudo-inject.js +89 -3
- package/dist/capture/script/walker/text-segments.d.ts +1 -0
- package/dist/capture/script/walker/text-segments.js +76 -19
- package/dist/capture/script.generated.js +1 -1
- package/dist/capture/types.d.ts +43 -1
- package/dist/cli/animate.d.ts +72 -1
- package/dist/cli/animate.js +289 -3
- package/dist/cli/capture.js +18 -1
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +38 -1
- package/dist/cli/template.d.ts +15 -0
- package/dist/cli/template.js +136 -0
- package/dist/cli/term.d.ts +16 -0
- package/dist/cli/term.js +192 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +18 -0
- package/dist/render/borders.d.ts +46 -1
- package/dist/render/borders.js +78 -5
- package/dist/render/clip-path.d.ts +9 -0
- package/dist/render/clip-path.js +173 -0
- package/dist/render/colors.js +154 -16
- package/dist/render/conic-raster.js +6 -0
- package/dist/render/device-chrome.d.ts +61 -0
- package/dist/render/device-chrome.js +187 -0
- package/dist/render/element-tree-to-svg.d.ts +3 -84
- package/dist/render/element-tree-to-svg.js +314 -2396
- package/dist/render/embedded-font-builder.js +13 -0
- package/dist/render/font-resolution.d.ts +483 -0
- package/dist/render/font-resolution.js +3046 -0
- package/dist/render/form-controls.d.ts +28 -0
- package/dist/render/form-controls.js +321 -116
- package/dist/render/gradient-defs.d.ts +37 -0
- package/dist/render/gradient-defs.js +571 -0
- package/dist/render/gradients.d.ts +6 -3
- package/dist/render/gradients.js +25 -20
- package/dist/render/harfbuzz-shaper.d.ts +67 -0
- package/dist/render/harfbuzz-shaper.js +151 -0
- package/dist/render/image-pattern.d.ts +24 -0
- package/dist/render/image-pattern.js +256 -0
- package/dist/render/index.d.ts +1 -0
- package/dist/render/index.js +3 -0
- package/dist/render/mask.d.ts +69 -0
- package/dist/render/mask.js +941 -0
- package/dist/render/stacking.d.ts +99 -0
- package/dist/render/stacking.js +411 -0
- package/dist/render/text-to-path.d.ts +32 -350
- package/dist/render/text-to-path.js +343 -3148
- package/dist/render/unicode-classification.d.ts +51 -0
- package/dist/render/unicode-classification.js +376 -0
- package/dist/render/vertical-text.js +7 -4
- package/dist/scroll/composer.js +2 -0
- package/dist/scroll/executor.js +17 -2
- package/dist/scroll/pattern.d.ts +8 -1
- package/dist/scroll/pattern.js +30 -2
- package/dist/templates/builtin/background-loop.d.ts +171 -0
- package/dist/templates/builtin/background-loop.js +500 -0
- package/dist/templates/builtin/chart.d.ts +140 -0
- package/dist/templates/builtin/chart.js +422 -0
- package/dist/templates/builtin/chat.d.ts +67 -0
- package/dist/templates/builtin/chat.js +197 -0
- package/dist/templates/builtin/device-mockup.d.ts +37 -0
- package/dist/templates/builtin/device-mockup.js +56 -0
- package/dist/templates/builtin/kinetic-text.d.ts +109 -0
- package/dist/templates/builtin/kinetic-text.js +333 -0
- package/dist/templates/builtin/lower-third.d.ts +45 -0
- package/dist/templates/builtin/lower-third.js +126 -0
- package/dist/templates/builtin/subscribe.d.ts +46 -0
- package/dist/templates/builtin/subscribe.js +170 -0
- package/dist/templates/index.d.ts +19 -0
- package/dist/templates/index.js +19 -0
- package/dist/templates/json-schema.d.ts +29 -0
- package/dist/templates/json-schema.js +70 -0
- package/dist/templates/registry.d.ts +24 -0
- package/dist/templates/registry.js +62 -0
- package/dist/templates/render.d.ts +24 -0
- package/dist/templates/render.js +86 -0
- package/dist/templates/types.d.ts +111 -0
- package/dist/templates/types.js +30 -0
- package/dist/terminal/cast.d.ts +53 -0
- package/dist/terminal/cast.js +83 -0
- package/dist/terminal/emulator.d.ts +64 -0
- package/dist/terminal/emulator.js +122 -0
- package/dist/terminal/incremental.d.ts +77 -0
- package/dist/terminal/incremental.js +382 -0
- package/dist/terminal/index.d.ts +93 -0
- package/dist/terminal/index.js +142 -0
- package/dist/terminal/pty.d.ts +66 -0
- package/dist/terminal/pty.js +158 -0
- package/dist/terminal/render.d.ts +65 -0
- package/dist/terminal/render.js +156 -0
- package/dist/terminal/theme.d.ts +43 -0
- package/dist/terminal/theme.js +90 -0
- package/llms.txt +107 -0
- package/package.json +11 -3
- package/schemas/animate-config.schema.json +114 -4
package/README.md
CHANGED
|
@@ -126,7 +126,8 @@ npm run demos:examples # run the bundled example demo scripts
|
|
|
126
126
|
|
|
127
127
|
- `FEATURES.md` — per-feature support checklist with links to test fixtures.
|
|
128
128
|
- `docs/` — requirements docs covering rendering fidelity, supported CSS features, and known caveats.
|
|
129
|
-
- `
|
|
129
|
+
- [`llms.txt`](llms.txt) — a concise, self-contained guide for **AI agents using Domotion as a tool** (Claude, Cursor, etc.): the CLIs, the config schema, the template library, the API, and the gotchas. Point your agent at it.
|
|
130
|
+
- `CLAUDE.md` — guidance for AI assistants working *on this repo's* source (different audience from `llms.txt`).
|
|
130
131
|
|
|
131
132
|
## License
|
|
132
133
|
|
|
@@ -273,6 +273,11 @@ export function generateAnimatedSvg(config) {
|
|
|
273
273
|
const enterStartPct = entersViaOverlap
|
|
274
274
|
? pct(timeOffset - prevTransDur, totalDuration)
|
|
275
275
|
: startPct;
|
|
276
|
+
// DM-1207: the last frame holds solid to 100% (no loop cross-dissolve)
|
|
277
|
+
// unless `loopFade` is set — same rule the crossfade/cut path applies via
|
|
278
|
+
// DM-1148 (see emitCrossfadeOrCutFrame). For the slide paths (push-left /
|
|
279
|
+
// scroll) this means: slide in, then hold (no slide-out / fade-out).
|
|
280
|
+
const holdLastFrame = i === frames.length - 1 && config.loopFade !== true;
|
|
276
281
|
if (transType === "push-left") {
|
|
277
282
|
// Push: slide in from right, slide out to left
|
|
278
283
|
frameGroups.push(` <g class="f f-${i}"><clipPath id="fc-${i}"><rect width="${width}" height="${height}" /></clipPath><g clip-path="url(#fc-${i})" class="fp fp-${i}">\n${frame.svgContent}\n </g></g>`);
|
|
@@ -282,7 +287,7 @@ export function generateAnimatedSvg(config) {
|
|
|
282
287
|
// Window is [enterStartPct .. transEndPct] (when the slide has fully
|
|
283
288
|
// exited the viewBox); 0.01% pad on each side keeps the snap inside the
|
|
284
289
|
// existing opacity:0 bookend.
|
|
285
|
-
keyframes.push(slideKeyframes(i, "X", width, entersViaPush, enterStartPct, startPct, holdEndPct, transEndPct, enterStartPct, transEndPct, totalSec));
|
|
290
|
+
keyframes.push(slideKeyframes(i, "X", width, entersViaPush, enterStartPct, startPct, holdEndPct, transEndPct, enterStartPct, transEndPct, totalSec, holdLastFrame));
|
|
286
291
|
}
|
|
287
292
|
else if (transType === "scroll") {
|
|
288
293
|
// DM-609: `scroll` now means real geometric scroll between two frames
|
|
@@ -294,7 +299,7 @@ export function generateAnimatedSvg(config) {
|
|
|
294
299
|
// `fd-${i}` display animation). (`entersViaScroll` is already computed in
|
|
295
300
|
// the outer scope above — same value, no need to redeclare/shadow it.)
|
|
296
301
|
frameGroups.push(` <g class="f f-${i}"><clipPath id="fc-${i}"><rect width="${width}" height="${height}" /></clipPath><g clip-path="url(#fc-${i})" class="fp fp-${i}">\n${frame.svgContent}\n </g></g>`);
|
|
297
|
-
keyframes.push(slideKeyframes(i, "Y", height, entersViaScroll, enterStartPct, startPct, holdEndPct, transEndPct, enterStartPct, transEndPct, totalSec));
|
|
302
|
+
keyframes.push(slideKeyframes(i, "Y", height, entersViaScroll, enterStartPct, startPct, holdEndPct, transEndPct, enterStartPct, transEndPct, totalSec, holdLastFrame));
|
|
298
303
|
}
|
|
299
304
|
else if (transType === "magic-move" && frame.magicMove != null) {
|
|
300
305
|
// DM-898: magic-move. Frame i holds [start..holdEnd] then HARD-CUTS out;
|
|
@@ -686,17 +691,39 @@ function buildDisplayKeyframes(name, visibleStartPct, visibleEndPct) {
|
|
|
686
691
|
* exits to `-size`. 0.1% pads on each bookend keep the snap inside the
|
|
687
692
|
* opacity:0 frame. Emits the fp/fv/fd keyframes + the `.f-`/`.fp-` rules.
|
|
688
693
|
*/
|
|
689
|
-
function slideKeyframes(i, axis, size, entersSliding, enterStartPct, startPct, holdEndPct, transEndPct, visStart, visEnd, totalSec
|
|
694
|
+
function slideKeyframes(i, axis, size, entersSliding, enterStartPct, startPct, holdEndPct, transEndPct, visStart, visEnd, totalSec,
|
|
695
|
+
/** DM-1207: the last frame, when the loop must NOT cross-dissolve, slides in
|
|
696
|
+
* and then HOLDS solid (transform 0, opacity 1, visible) to 100% — no
|
|
697
|
+
* slide-out / fade-out — and the loop hard-cuts back to frame 0. Mirrors the
|
|
698
|
+
* crossfade/cut path's DM-1148 holdToEnd. Without it the slide-out keyframes
|
|
699
|
+
* ramp the last frame to opacity 0 across its whole hold, washing it out. */
|
|
700
|
+
holdToEnd = false) {
|
|
701
|
+
const enterBound = padBefore(parseFloat(enterStartPct), KEYFRAME_EPSILON.slide, 2);
|
|
702
|
+
if (holdToEnd) {
|
|
703
|
+
return `
|
|
704
|
+
@keyframes fp-${i} {
|
|
705
|
+
0%, ${enterBound}% { transform: translate${axis}(${entersSliding ? size : 0}px); }
|
|
706
|
+
${startPct} { transform: translate${axis}(0); }
|
|
707
|
+
100% { transform: translate${axis}(0); }
|
|
708
|
+
}
|
|
709
|
+
@keyframes fv-${i} {
|
|
710
|
+
0%, ${enterBound}% { opacity: 0; }
|
|
711
|
+
${enterStartPct} { opacity: 1; }
|
|
712
|
+
100% { opacity: 1; }
|
|
713
|
+
}${buildDisplayKeyframes(`fd-${i}`, visStart, "100")}
|
|
714
|
+
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite, fd-${i} ${totalSec.toFixed(2)}s infinite step-end; }
|
|
715
|
+
.fp-${i} { animation: fp-${i} ${totalSec.toFixed(2)}s infinite; }`;
|
|
716
|
+
}
|
|
690
717
|
return `
|
|
691
718
|
@keyframes fp-${i} {
|
|
692
|
-
0%, ${
|
|
719
|
+
0%, ${enterBound}% { transform: translate${axis}(${entersSliding ? size : 0}px); }
|
|
693
720
|
${startPct} { transform: translate${axis}(0); }
|
|
694
721
|
${holdEndPct} { transform: translate${axis}(0); }
|
|
695
722
|
${transEndPct} { transform: translate${axis}(-${size}px); }
|
|
696
723
|
${padAfter(parseFloat(transEndPct), KEYFRAME_EPSILON.slide, 2)}%, 100% { transform: translate${axis}(-${size}px); }
|
|
697
724
|
}
|
|
698
725
|
@keyframes fv-${i} {
|
|
699
|
-
0%, ${
|
|
726
|
+
0%, ${enterBound}% { opacity: 0; }
|
|
700
727
|
${enterStartPct} { opacity: 1; }
|
|
701
728
|
${transEndPct} { opacity: 1; }
|
|
702
729
|
${padAfter(parseFloat(transEndPct), KEYFRAME_EPSILON.slide, 2)}%, 100% { opacity: 0; }
|
|
@@ -802,25 +829,46 @@ function buildIntraFrameAnimationCss(frames, frameTiming, totalSec) {
|
|
|
802
829
|
return `transform: translateX(${val});`;
|
|
803
830
|
if (a.property === "translateY")
|
|
804
831
|
return `transform: translateY(${val});`;
|
|
832
|
+
if (a.property === "scale")
|
|
833
|
+
return `transform: scale(${val});`;
|
|
805
834
|
if (a.property === "clipPath")
|
|
806
835
|
return `clip-path: ${val};`;
|
|
807
836
|
return `${a.property}: ${val};`;
|
|
808
837
|
};
|
|
838
|
+
// DM-1297: SVG transforms are origin-(0,0); a `transformOrigin` makes a
|
|
839
|
+
// scale/rotate/translate resolve about the element's OWN box (e.g. a
|
|
840
|
+
// center-origin scale-pop) instead of the SVG origin. `transform-box:
|
|
841
|
+
// fill-box` switches the reference box to the element's bounding box.
|
|
842
|
+
const originDecl = a.transformOrigin != null && a.transformOrigin !== ""
|
|
843
|
+
? ` transform-box: fill-box; transform-origin: ${a.transformOrigin};`
|
|
844
|
+
: "";
|
|
809
845
|
const animName = `f${i}-${a.animId}-${ai}`;
|
|
810
846
|
if (a.repeat != null) {
|
|
811
847
|
// DM-869: repeating animation (blink / pulse / breathe). The keyframe is
|
|
812
848
|
// a single from→to cycle on the animation's own `duration` clock, looped
|
|
813
849
|
// via animation-iteration-count + (optional) direction:alternate. The
|
|
814
850
|
// loop is only visible while the frame is on screen (the frame group's
|
|
815
|
-
// visibility gating)
|
|
816
|
-
//
|
|
851
|
+
// visibility gating).
|
|
852
|
+
//
|
|
853
|
+
// `animation-delay` positions the first cycle: a POSITIVE delay (after the
|
|
854
|
+
// frame appears) holds `from` until it elapses then plays; a NEGATIVE delay
|
|
855
|
+
// (a phase offset) starts the loop already mid-cycle so it never freezes —
|
|
856
|
+
// the right choice for a seamless ambient loop (DM-1289).
|
|
857
|
+
//
|
|
858
|
+
// DM-1289: emit timing-function / delay / fill-mode INSIDE the `animation`
|
|
859
|
+
// shorthand, not as trailing longhands. The optimizer (csso) merges shared
|
|
860
|
+
// longhands into a separate, earlier grouped rule; a later `animation`
|
|
861
|
+
// shorthand then resets `animation-fill-mode` back to `none`, so during a
|
|
862
|
+
// positive delay the element showed its base value (not `from`) and SNAPPED
|
|
863
|
+
// when the cycle began. Folding everything into the one shorthand leaves
|
|
864
|
+
// nothing for the optimizer to hoist out of order.
|
|
817
865
|
const iterations = a.repeat === "infinite" ? "infinite" : String(a.repeat);
|
|
818
866
|
const direction = a.alternate === true ? " alternate" : "";
|
|
819
867
|
out.push(` @keyframes ${animName} {
|
|
820
868
|
0% { ${propValue(a.from)} }
|
|
821
869
|
100% { ${propValue(a.to)} }
|
|
822
870
|
}
|
|
823
|
-
.anim-${a.animId} { animation: ${animName} ${a.duration}ms ${
|
|
871
|
+
.anim-${a.animId} { animation: ${animName} ${a.duration}ms ${easing} ${startMs.toFixed(0)}ms ${iterations}${direction} both;${originDecl} }`);
|
|
824
872
|
}
|
|
825
873
|
else {
|
|
826
874
|
// One-shot: hold `from` until startPct, animate from→to during
|
|
@@ -832,7 +880,7 @@ function buildIntraFrameAnimationCss(frames, frameTiming, totalSec) {
|
|
|
832
880
|
${endPct.toFixed(3)}% { ${propValue(a.to)} }
|
|
833
881
|
100% { ${propValue(a.to)} }
|
|
834
882
|
}
|
|
835
|
-
.anim-${a.animId} { animation: ${animName} ${totalSec.toFixed(2)}s infinite; animation-timing-function: ${easing}
|
|
883
|
+
.anim-${a.animId} { animation: ${animName} ${totalSec.toFixed(2)}s infinite; animation-timing-function: ${easing};${originDecl} }`);
|
|
836
884
|
}
|
|
837
885
|
}
|
|
838
886
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DM-1287 (doc 73): namespace a self-contained animated SVG so it can be nested
|
|
3
|
+
* INSIDE another animated SVG without its document-global names colliding.
|
|
4
|
+
*
|
|
5
|
+
* A template frame embeds a complete `generateAnimatedSvg` document as one
|
|
6
|
+
* frame's content. SVG/CSS names are document-global — they do NOT scope to a
|
|
7
|
+
* nested `<svg>` subtree — so the inner document's generated names clash with the
|
|
8
|
+
* outer animation's identical names (and with sibling template frames):
|
|
9
|
+
*
|
|
10
|
+
* - **element ids** — `id="viewport-clip"`, `id="f0-bg0"`, glyph defs, gradients,
|
|
11
|
+
* clips, filters (referenced via `url(#…)` / `href="#…"`).
|
|
12
|
+
* - **embedded-font families** — `font-family:"dmf0"` in `@font-face` + the
|
|
13
|
+
* `<text font-family="dmf0">` attrs. A duplicate family name makes a later
|
|
14
|
+
* `@font-face` win, so text in OTHER frames reshapes to the wrong glyphs.
|
|
15
|
+
* - **frame / animation classes** — `class="f f-0"`, `class="anim-…"` and their
|
|
16
|
+
* `.f` / `.f-0` / `.anim-…` style selectors.
|
|
17
|
+
* - **`@keyframes` names** — `fv-0`, the intra-frame `f0-…` names — and the
|
|
18
|
+
* `animation:` references to them. A duplicate `@keyframes` wins globally, so
|
|
19
|
+
* the outer frame-visibility timing gets the inner timeline.
|
|
20
|
+
* - **the `--scene-dur` custom property** on `:root`.
|
|
21
|
+
*
|
|
22
|
+
* The fix: rewrite every such name with a per-frame token. The vocabulary is
|
|
23
|
+
* fully controlled (domotion's own renderer/animator emits it), so the rewrite is
|
|
24
|
+
* precise. The token must be a valid CSS-ident prefix (start with a letter).
|
|
25
|
+
*
|
|
26
|
+
* This is the same class of problem the `cast` frame sidesteps by sharing the
|
|
27
|
+
* outer embedded-font builder (`manageFonts: false`); a template runs a fully
|
|
28
|
+
* independent `composeAnimateConfig`, so it can't share that state and is
|
|
29
|
+
* namespaced after the fact instead.
|
|
30
|
+
*/
|
|
31
|
+
/** Options for {@link namespaceEmbeddedAnimatedSvg}. */
|
|
32
|
+
export interface NamespaceEmbedOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Prefix embedded-font family names (`dmfN`) too. Default `true` — correct when
|
|
35
|
+
* the nested document carries its own `@font-face` rules (a template frame).
|
|
36
|
+
*
|
|
37
|
+
* Pass `false` when the nested document's fonts are DEFERRED to the host
|
|
38
|
+
* pipeline's shared embedded-font builder (a `cast` frame composed with
|
|
39
|
+
* `manageFonts: false`): its `font-family="dmfN"` attrs reference an
|
|
40
|
+
* `@font-face` emitted ONCE at the outer top level, where the names are already
|
|
41
|
+
* globally unique — prefixing them here would dangle the reference. The class /
|
|
42
|
+
* keyframe / id / `--scene-dur` collisions still need fixing, so the rest of the
|
|
43
|
+
* pass runs unchanged.
|
|
44
|
+
*/
|
|
45
|
+
namespaceFonts?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Prefix every document-global name in a complete animated-SVG document with
|
|
49
|
+
* `token` (e.g. `"e1_"`). Returns the rewritten SVG string; the input is assumed
|
|
50
|
+
* to be a single `<svg>…</svg>` document as produced by `generateAnimatedSvg`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function namespaceEmbeddedAnimatedSvg(svg: string, token: string, opts?: NamespaceEmbedOptions): string;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DM-1287 (doc 73): namespace a self-contained animated SVG so it can be nested
|
|
3
|
+
* INSIDE another animated SVG without its document-global names colliding.
|
|
4
|
+
*
|
|
5
|
+
* A template frame embeds a complete `generateAnimatedSvg` document as one
|
|
6
|
+
* frame's content. SVG/CSS names are document-global — they do NOT scope to a
|
|
7
|
+
* nested `<svg>` subtree — so the inner document's generated names clash with the
|
|
8
|
+
* outer animation's identical names (and with sibling template frames):
|
|
9
|
+
*
|
|
10
|
+
* - **element ids** — `id="viewport-clip"`, `id="f0-bg0"`, glyph defs, gradients,
|
|
11
|
+
* clips, filters (referenced via `url(#…)` / `href="#…"`).
|
|
12
|
+
* - **embedded-font families** — `font-family:"dmf0"` in `@font-face` + the
|
|
13
|
+
* `<text font-family="dmf0">` attrs. A duplicate family name makes a later
|
|
14
|
+
* `@font-face` win, so text in OTHER frames reshapes to the wrong glyphs.
|
|
15
|
+
* - **frame / animation classes** — `class="f f-0"`, `class="anim-…"` and their
|
|
16
|
+
* `.f` / `.f-0` / `.anim-…` style selectors.
|
|
17
|
+
* - **`@keyframes` names** — `fv-0`, the intra-frame `f0-…` names — and the
|
|
18
|
+
* `animation:` references to them. A duplicate `@keyframes` wins globally, so
|
|
19
|
+
* the outer frame-visibility timing gets the inner timeline.
|
|
20
|
+
* - **the `--scene-dur` custom property** on `:root`.
|
|
21
|
+
*
|
|
22
|
+
* The fix: rewrite every such name with a per-frame token. The vocabulary is
|
|
23
|
+
* fully controlled (domotion's own renderer/animator emits it), so the rewrite is
|
|
24
|
+
* precise. The token must be a valid CSS-ident prefix (start with a letter).
|
|
25
|
+
*
|
|
26
|
+
* This is the same class of problem the `cast` frame sidesteps by sharing the
|
|
27
|
+
* outer embedded-font builder (`manageFonts: false`); a template runs a fully
|
|
28
|
+
* independent `composeAnimateConfig`, so it can't share that state and is
|
|
29
|
+
* namespaced after the fact instead.
|
|
30
|
+
*/
|
|
31
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
32
|
+
const byLengthDesc = (a, b) => b.length - a.length;
|
|
33
|
+
/**
|
|
34
|
+
* Prefix every document-global name in a complete animated-SVG document with
|
|
35
|
+
* `token` (e.g. `"e1_"`). Returns the rewritten SVG string; the input is assumed
|
|
36
|
+
* to be a single `<svg>…</svg>` document as produced by `generateAnimatedSvg`.
|
|
37
|
+
*/
|
|
38
|
+
export function namespaceEmbeddedAnimatedSvg(svg, token, opts = {}) {
|
|
39
|
+
let out = svg;
|
|
40
|
+
// 1. Element ids + every reference (url(#id) / (xlink:)href="#id"). Longest
|
|
41
|
+
// name first so an id that is a textual prefix of another can't partial-match.
|
|
42
|
+
const ids = new Set();
|
|
43
|
+
for (const m of out.matchAll(/\sid="([^"]+)"/g))
|
|
44
|
+
ids.add(m[1]);
|
|
45
|
+
for (const id of [...ids].sort(byLengthDesc)) {
|
|
46
|
+
const e = escapeRe(id);
|
|
47
|
+
out = out
|
|
48
|
+
.replace(new RegExp(`(\\sid=")${e}(")`, "g"), `$1${token}${id}$2`)
|
|
49
|
+
.replace(new RegExp(`url\\(#${e}\\)`, "g"), `url(#${token}${id})`)
|
|
50
|
+
.replace(new RegExp(`((?:xlink:)?href=")#${e}(")`, "g"), `$1#${token}${id}$2`);
|
|
51
|
+
}
|
|
52
|
+
// 2. Embedded-font families (`dmfN`). Only the `@font-face` declaration and the
|
|
53
|
+
// `font-family` attr/decl contexts — never a bare global replace, since the
|
|
54
|
+
// base64 `src` payload could contain the same byte sequence. Skipped when the
|
|
55
|
+
// fonts are deferred to a shared outer builder (see NamespaceEmbedOptions).
|
|
56
|
+
if (opts.namespaceFonts !== false) {
|
|
57
|
+
out = out
|
|
58
|
+
.replace(/(font-family:\s*")(dmf\d+)(")/g, `$1${token}$2$3`)
|
|
59
|
+
.replace(/(font-family=")(dmf\d+)(")/g, `$1${token}$2$3`);
|
|
60
|
+
}
|
|
61
|
+
// 3. `@keyframes` names + their `animation:` references. A keyframe name is a
|
|
62
|
+
// domotion-generated token (`fv-0`, `f0-f0a0-0`, …) that never appears as an
|
|
63
|
+
// id, class, or inside the (hyphen-free) base64 font data, so a word-boundary
|
|
64
|
+
// replace across the whole doc is safe. Longest-first for prefix safety.
|
|
65
|
+
const keyframes = new Set();
|
|
66
|
+
for (const m of out.matchAll(/@keyframes\s+([A-Za-z0-9_-]+)/g))
|
|
67
|
+
keyframes.add(m[1]);
|
|
68
|
+
for (const name of [...keyframes].sort(byLengthDesc)) {
|
|
69
|
+
out = out.replace(new RegExp(`\\b${escapeRe(name)}\\b`, "g"), `${token}${name}`);
|
|
70
|
+
}
|
|
71
|
+
// 4. Classes. Collect the names from `class="…"` attrs (every token is renderer-
|
|
72
|
+
// generated), then (a) prefix each token in every class attr, and (b) prefix
|
|
73
|
+
// each known name where it appears as a `.name` selector. The selector pass is
|
|
74
|
+
// boundary-guarded (`(?![\w-])`) and limited to the collected names so it can
|
|
75
|
+
// never touch a decimal like `0.22` / `22.500%` in the CSS.
|
|
76
|
+
const classes = new Set();
|
|
77
|
+
for (const m of out.matchAll(/\sclass="([^"]+)"/g)) {
|
|
78
|
+
for (const t of m[1].split(/\s+/))
|
|
79
|
+
if (t !== "")
|
|
80
|
+
classes.add(t);
|
|
81
|
+
}
|
|
82
|
+
out = out.replace(/(\sclass=")([^"]+)(")/g, (_full, a, cls, c) => a + cls.split(/\s+/).map((t) => (t !== "" ? token + t : t)).join(" ") + c);
|
|
83
|
+
for (const name of [...classes].sort(byLengthDesc)) {
|
|
84
|
+
out = out.replace(new RegExp(`\\.${escapeRe(name)}(?![\\w-])`, "g"), `.${token}${name}`);
|
|
85
|
+
}
|
|
86
|
+
// 5. The `--scene-dur` custom property (declaration on `:root` + any `var()`).
|
|
87
|
+
out = out.replace(/--scene-dur\b/g, `--scene-dur-${token}`);
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
@@ -249,11 +249,13 @@ export declare const intraFrameAnimationSchema: z.ZodObject<{
|
|
|
249
249
|
opacity: "opacity";
|
|
250
250
|
translateX: "translateX";
|
|
251
251
|
translateY: "translateY";
|
|
252
|
+
scale: "scale";
|
|
252
253
|
}>;
|
|
253
254
|
from: z.ZodString;
|
|
254
255
|
to: z.ZodString;
|
|
255
256
|
duration: z.ZodNumber;
|
|
256
257
|
easing: z.ZodOptional<z.ZodString>;
|
|
258
|
+
transformOrigin: z.ZodOptional<z.ZodString>;
|
|
257
259
|
delay: z.ZodOptional<z.ZodNumber>;
|
|
258
260
|
repeat: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodLiteral<"infinite">]>>;
|
|
259
261
|
alternate: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -189,8 +189,9 @@ export const intraFrameAnimationSchema = z.object({
|
|
|
189
189
|
* captured element is wrapped in a `<g class="anim-<id>">`, the keyframes
|
|
190
190
|
* apply `clip-path` to that wrapper.
|
|
191
191
|
*/
|
|
192
|
-
property: z.enum(["width", "height", "opacity", "transform", "translateX", "translateY", "clipPath"]),
|
|
193
|
-
/** Start value (CSS string, e.g. `"0%"`, `"240px"`, `"0.3"`).
|
|
192
|
+
property: z.enum(["width", "height", "opacity", "transform", "translateX", "translateY", "scale", "clipPath"]),
|
|
193
|
+
/** Start value (CSS string, e.g. `"0%"`, `"240px"`, `"0.3"`). For `scale`,
|
|
194
|
+
* a unitless factor (`"0.6"` -> `"1"`). */
|
|
194
195
|
from: z.string(),
|
|
195
196
|
/** End value (same syntax as `from`). */
|
|
196
197
|
to: z.string(),
|
|
@@ -198,6 +199,16 @@ export const intraFrameAnimationSchema = z.object({
|
|
|
198
199
|
duration: z.number(),
|
|
199
200
|
/** CSS easing string. Default `linear`. */
|
|
200
201
|
easing: z.string().optional(),
|
|
202
|
+
/**
|
|
203
|
+
* DM-1297: transform-origin for a `transform` / `scale` / `translate*`
|
|
204
|
+
* animation (e.g. `"center"`, `"50% 50%"`, `"left top"`). SVG transforms are
|
|
205
|
+
* origin-(0,0) by default, so a `scale`/`rotate` would shrink/orbit toward the
|
|
206
|
+
* SVG origin instead of the element's own box. Setting this emits
|
|
207
|
+
* `transform-box: fill-box; transform-origin: <value>` on the animated group so
|
|
208
|
+
* the transform resolves about the element's OWN bounding box — e.g. a
|
|
209
|
+
* center-origin scale-pop. Ignored for non-transform properties.
|
|
210
|
+
*/
|
|
211
|
+
transformOrigin: z.string().optional(),
|
|
201
212
|
/** Ms after the frame becomes visible before animation starts. Default 0. */
|
|
202
213
|
delay: z.number().optional(),
|
|
203
214
|
/**
|
package/dist/capture/emoji.d.ts
CHANGED
|
@@ -17,6 +17,43 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { Page } from "@playwright/test";
|
|
19
19
|
import type { CapturedElement } from "./types.js";
|
|
20
|
+
/**
|
|
21
|
+
* Snap an Apple Color Emoji sbix bitmap to the SQUARE box Chrome actually
|
|
22
|
+
* paints it in.
|
|
23
|
+
*
|
|
24
|
+
* Chrome paints a color-emoji glyph as a square whose side equals the glyph
|
|
25
|
+
* ADVANCE — the captured `Range.getBoundingClientRect()` width, minus any
|
|
26
|
+
* letter-spacing Chrome appends to the right of the advance. Two facts make
|
|
27
|
+
* the advance (not the font size) the correct side:
|
|
28
|
+
* - At small font sizes Chrome enforces a minimum emoji advance that exceeds
|
|
29
|
+
* the font size — e.g. a 20px advance at font-size 16 (~1.25×). Sizing the
|
|
30
|
+
* overlay to the font size painted the emoji ~20% too small (DM-1198: "our
|
|
31
|
+
* emojis seem a lot smaller").
|
|
32
|
+
* - The sbix PNG is a full square bitmap (the emoji fills a square em), so
|
|
33
|
+
* drawing it into an advance × advance box reproduces both full-bleed emoji
|
|
34
|
+
* and ones with transparent margins (e.g. 📈) without distortion.
|
|
35
|
+
*
|
|
36
|
+
* Geometry:
|
|
37
|
+
* - Horizontal: the bitmap sits flush at the advance's left (`rect.x`); any
|
|
38
|
+
* letter-spacing pads to the RIGHT, so no horizontal shift is applied
|
|
39
|
+
* (DM-919 — the original DM-381 centering pass wrongly shifted it right
|
|
40
|
+
* whenever letter-spacing > 0).
|
|
41
|
+
* - Vertical: the square is centered in the captured rect's line box, which
|
|
42
|
+
* matches Chrome within ~1px across sizes (DM-438 — a 20×17 rect extends
|
|
43
|
+
* upward to a 20×20 square; DM-801 — a 56×63 rect with 8px letter-spacing
|
|
44
|
+
* snaps to 48×48).
|
|
45
|
+
*/
|
|
46
|
+
export declare function emojiSquareRect(rect: {
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
width: number;
|
|
50
|
+
height: number;
|
|
51
|
+
}, letterSpacing: number): {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
};
|
|
20
57
|
/**
|
|
21
58
|
* For every text segment CAPTURE_SCRIPT flagged with a rasterRect (contains a
|
|
22
59
|
* color-bitmap glyph like U+2713 ✓ or an emoji), ask Playwright for a
|
package/dist/capture/emoji.js
CHANGED
|
@@ -97,6 +97,42 @@ function extractEmojiBitmap(codepoint, paintedWidthPx) {
|
|
|
97
97
|
_sbixCache.set(cacheKey, result);
|
|
98
98
|
return result;
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Snap an Apple Color Emoji sbix bitmap to the SQUARE box Chrome actually
|
|
102
|
+
* paints it in.
|
|
103
|
+
*
|
|
104
|
+
* Chrome paints a color-emoji glyph as a square whose side equals the glyph
|
|
105
|
+
* ADVANCE — the captured `Range.getBoundingClientRect()` width, minus any
|
|
106
|
+
* letter-spacing Chrome appends to the right of the advance. Two facts make
|
|
107
|
+
* the advance (not the font size) the correct side:
|
|
108
|
+
* - At small font sizes Chrome enforces a minimum emoji advance that exceeds
|
|
109
|
+
* the font size — e.g. a 20px advance at font-size 16 (~1.25×). Sizing the
|
|
110
|
+
* overlay to the font size painted the emoji ~20% too small (DM-1198: "our
|
|
111
|
+
* emojis seem a lot smaller").
|
|
112
|
+
* - The sbix PNG is a full square bitmap (the emoji fills a square em), so
|
|
113
|
+
* drawing it into an advance × advance box reproduces both full-bleed emoji
|
|
114
|
+
* and ones with transparent margins (e.g. 📈) without distortion.
|
|
115
|
+
*
|
|
116
|
+
* Geometry:
|
|
117
|
+
* - Horizontal: the bitmap sits flush at the advance's left (`rect.x`); any
|
|
118
|
+
* letter-spacing pads to the RIGHT, so no horizontal shift is applied
|
|
119
|
+
* (DM-919 — the original DM-381 centering pass wrongly shifted it right
|
|
120
|
+
* whenever letter-spacing > 0).
|
|
121
|
+
* - Vertical: the square is centered in the captured rect's line box, which
|
|
122
|
+
* matches Chrome within ~1px across sizes (DM-438 — a 20×17 rect extends
|
|
123
|
+
* upward to a 20×20 square; DM-801 — a 56×63 rect with 8px letter-spacing
|
|
124
|
+
* snaps to 48×48).
|
|
125
|
+
*/
|
|
126
|
+
export function emojiSquareRect(rect, letterSpacing) {
|
|
127
|
+
const ls = Math.max(0, letterSpacing) || 0;
|
|
128
|
+
const side = Math.max(1, rect.width - ls);
|
|
129
|
+
return {
|
|
130
|
+
x: rect.x,
|
|
131
|
+
y: rect.y + (rect.height - side) / 2,
|
|
132
|
+
width: side,
|
|
133
|
+
height: side,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
100
136
|
/**
|
|
101
137
|
* For every text segment CAPTURE_SCRIPT flagged with a rasterRect (contains a
|
|
102
138
|
* color-bitmap glyph like U+2713 ✓ or an emoji), ask Playwright for a
|
|
@@ -178,28 +214,12 @@ export async function rasterizeBitmapGlyphs(page, tree, viewport) {
|
|
|
178
214
|
// isn't carried on the segment (only the SVG path needs it,
|
|
179
215
|
// not the existing screenshot path which already round-trips
|
|
180
216
|
// a rectangular PNG).
|
|
181
|
-
const elFs = parseFloat(el.styles.fontSize ?? "") || 0;
|
|
182
|
-
const fs = seg.fontSize ?? (elFs > 0 ? elFs : Math.max(g.rect.width, g.rect.height));
|
|
183
|
-
// DM-919: Chrome's per-emoji paint origin = advance start
|
|
184
|
-
// (rect.x) when there's NO letter-spacing — the bitmap
|
|
185
|
-
// sits flush at the left of the advance. When letter-
|
|
186
|
-
// spacing > 0, Chrome ADDS the letter-spacing to the
|
|
187
|
-
// advance, which captures as a wider rect — the bitmap
|
|
188
|
-
// is still at rect.x (the spacing pads to the right).
|
|
189
|
-
// The original DM-381 centering pass mis-handled this:
|
|
190
|
-
// it shifted the bitmap by `(rect.width - fs) / 2`,
|
|
191
|
-
// moving the emoji right whenever letter-spacing >0.
|
|
192
|
-
// Pull out the captured letter-spacing and subtract it
|
|
193
|
-
// from rect.width FIRST, then center the bitmap inside
|
|
194
|
-
// the REAL advance — handles both the centered emoji-
|
|
195
|
-
// alone case (where rect.w ≈ fs) and the letter-spaced
|
|
196
|
-
// case (where rect.w = fs + letter-spacing).
|
|
197
217
|
const ls = parseFloat(el.styles.letterSpacing ?? "") || 0;
|
|
198
|
-
const
|
|
199
|
-
g.rect.x
|
|
200
|
-
g.rect.y
|
|
201
|
-
g.rect.width =
|
|
202
|
-
g.rect.height =
|
|
218
|
+
const sq = emojiSquareRect(g.rect, ls);
|
|
219
|
+
g.rect.x = sq.x;
|
|
220
|
+
g.rect.y = sq.y;
|
|
221
|
+
g.rect.width = sq.width;
|
|
222
|
+
g.rect.height = sq.height;
|
|
203
223
|
continue;
|
|
204
224
|
}
|
|
205
225
|
}
|
|
@@ -57,13 +57,20 @@ export const createDottedCircleDetect = () => {
|
|
|
57
57
|
}
|
|
58
58
|
return { cnt, w: cnt > 0 ? (maxx - minx + 1) : 0 };
|
|
59
59
|
};
|
|
60
|
-
// Does Chrome auto-insert a U+25CC before this lone mark in
|
|
60
|
+
// Does Chrome auto-insert a U+25CC before this lone mark/cluster-letter in
|
|
61
|
+
// `font`? Probes category M (combining marks), category Lo (some Brahmic
|
|
62
|
+
// cluster-initial LETTERS — e.g. Soyombo U+11A84) AND category Lm (modifier
|
|
63
|
+
// LETTERS the Universal Shaping Engine also circles when orphaned — e.g. Kirat
|
|
64
|
+
// Rai U+16D6B/6C, length / vowel modifiers that paint "◌ □" when stranded).
|
|
65
|
+
// The ink heuristic below is the real gate (a normal letter renders WITHOUT a
|
|
66
|
+
// circle, so bare ≠ comb → false), so including Lo / Lm only widens what's
|
|
67
|
+
// probed, never forces a false positive.
|
|
61
68
|
const markGetsDottedCircle = (cp, ch, font) => {
|
|
62
69
|
if (cp < 0x0900)
|
|
63
70
|
return false;
|
|
64
71
|
if (font == null || font === '')
|
|
65
72
|
return false;
|
|
66
|
-
if (!/\p{M}/u.test(ch))
|
|
73
|
+
if (!/\p{M}|\p{Lo}|\p{Lm}/u.test(ch))
|
|
67
74
|
return false;
|
|
68
75
|
const key = cp + '|' + font;
|
|
69
76
|
const hit = _cache.get(key);
|
|
@@ -30,11 +30,20 @@ export const createEmojiDetect = () => {
|
|
|
30
30
|
// painting as a dropped/empty path-mode glyph: ✅ ✊ ✋. Unconditional like
|
|
31
31
|
// the ✨ ❌ ➡ family above — emoji presentation wins over the cascade.
|
|
32
32
|
0x2705, 0x270A, 0x270B,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
]);
|
|
34
|
+
// DM-1165: the Miscellaneous Symbols and Arrows (U+2B??) code points with
|
|
35
|
+
// default emoji presentation — ⬅⬆⬇ (2B05-07), ⬛⬜ (2B1B/1C), ⭐ (2B50),
|
|
36
|
+
// ⭕ (2B55). DM-728 added these to `rasterCps` unconditionally for the ⭐ in
|
|
37
|
+
// `20-deep-font-palette.html`, but Chrome's choice is actually CASCADE-
|
|
38
|
+
// DEPENDENT: when the element's font stack reaches a monochrome symbol/math
|
|
39
|
+
// font that covers them first, Chrome paints text, not color. Verified via
|
|
40
|
+
// `CSS.getPlatformFontsForNode` on the 2B00 fixture (cells lead with "Apple
|
|
41
|
+
// Symbols"): 2B05→Apple Symbols, 2B1B & 2B50→STIX Two Math — all MONOCHROME,
|
|
42
|
+
// so the unconditional raster was stamping blue emoji / a yellow star over
|
|
43
|
+
// Chrome's black arrows / hollow star. Probe per-element via `isColorGlyph`
|
|
44
|
+
// in `needsRaster` instead (color → raster, monochrome → path). A Set so the
|
|
45
|
+
// membership test in `needsRaster` is O(1).
|
|
46
|
+
const emojiPresentation2B = new Set([
|
|
38
47
|
0x2B05, 0x2B06, 0x2B07, 0x2B1B, 0x2B1C, 0x2B50, 0x2B55,
|
|
39
48
|
]);
|
|
40
49
|
// Codepoints in the U+2600-26FF Misc Symbols block with EmojiPresentation=Yes
|
|
@@ -200,6 +209,43 @@ export const createEmojiDetect = () => {
|
|
|
200
209
|
// cascade DOES reach the color font still rasters correctly.
|
|
201
210
|
if (cp >= 0x1F700 && cp <= 0x1F77F)
|
|
202
211
|
return isColorGlyph(cp, font);
|
|
212
|
+
// DM-1168: the two Emoji_Presentation=Yes code points in the Enclosed CJK
|
|
213
|
+
// Letters and Months block (U+3200-32FF) — ㊗ U+3297 CIRCLED IDEOGRAPH
|
|
214
|
+
// CONGRATULATION and ㊙ U+3299 CIRCLED IDEOGRAPH SECRET. Chrome paints them
|
|
215
|
+
// as Apple Color Emoji by default (the fixture cells show the red circled
|
|
216
|
+
// ideographs). But several macOS text fonts (Hiragino, Arial Unicode) also
|
|
217
|
+
// cover them with a MONOCHROME glyph, so a `lang=ja` cascade that reaches
|
|
218
|
+
// Hiragino first paints text, not color. Probe Chrome's actual per-element
|
|
219
|
+
// choice via the canvas (color → raster, monochrome → path), exactly like
|
|
220
|
+
// the DM-1025 emojiPresentation26 / DM-1125 alchemical branches.
|
|
221
|
+
if (cp === 0x3297 || cp === 0x3299)
|
|
222
|
+
return isColorGlyph(cp, font);
|
|
223
|
+
// DM-1173: 〽 U+303D PART ALTERNATION MARK (CJK Symbols and Punctuation,
|
|
224
|
+
// U+3000-303F). Emoji=Yes but text-default presentation, so Chrome paints
|
|
225
|
+
// the color glyph only when the cascade reaches Apple Color Emoji and no
|
|
226
|
+
// text font covers it first (many do — Hiragino, M+ 1p, Shippori Mincho).
|
|
227
|
+
// The fixture cell paints the orange color mark, so probe per-element font
|
|
228
|
+
// (color → raster, monochrome → path) like the branches above.
|
|
229
|
+
if (cp === 0x303D)
|
|
230
|
+
return isColorGlyph(cp, font);
|
|
231
|
+
// DM-1165: the U+2B?? emoji-presentation symbols (arrows ⬅⬆⬇, squares ⬛⬜,
|
|
232
|
+
// ⭐, ⭕). Cascade-dependent — Chrome paints text when the stack reaches a
|
|
233
|
+
// monochrome symbol/math font first (Apple Symbols / STIX Two Math), color
|
|
234
|
+
// otherwise. See `emojiPresentation2B` above.
|
|
235
|
+
if (emojiPresentation2B.has(cp))
|
|
236
|
+
return isColorGlyph(cp, font);
|
|
237
|
+
// DM-1167: the ONLY two codepoints in the Misc Symbols & Pictographs block
|
|
238
|
+
// (U+1F300-1F5FF) that a macOS text font also covers monochrome are
|
|
239
|
+
// 🌐 U+1F310 (GLOBE WITH MERIDIANS) and 🎤 U+1F3A4 (MICROPHONE) — Apple
|
|
240
|
+
// Symbols carries both. When the element's cascade leads with Apple Symbols
|
|
241
|
+
// (the html-test `.f1` cells do; CSS.getPlatformFontsForNode confirms),
|
|
242
|
+
// Chrome paints the MONOCHROME path glyph, not the color emoji. Probe
|
|
243
|
+
// Chrome's actual per-element choice (color → raster, monochrome → path)
|
|
244
|
+
// exactly like the DM-1125 Alchemical / DM-1168 ㊗㊙ branches, instead of
|
|
245
|
+
// unconditionally stamping the Apple Color Emoji bitmap over Chrome's text
|
|
246
|
+
// glyph. The common Apple-Color-Emoji-first cell still rasters (probe → true).
|
|
247
|
+
if (cp === 0x1F310 || cp === 0x1F3A4)
|
|
248
|
+
return isColorGlyph(cp, font);
|
|
203
249
|
// Main emoji blocks: Misc Symbols & Pictographs, Emoticons, Transport &
|
|
204
250
|
// Map, Alchemical, Supplemental Symbols & Pictographs, Pictographs
|
|
205
251
|
// Extended-A, Symbols & Pictographs Extended-B.
|