domotion-svg 0.19.1 → 0.20.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/dist/animation/animator.js +140 -51
- package/dist/animation/easing.d.ts +31 -0
- package/dist/animation/easing.js +0 -0
- package/dist/animation/overlay-schema.d.ts +17 -0
- package/dist/animation/overlay-schema.js +31 -0
- package/dist/cli/animate.d.ts +17 -0
- package/dist/cli/animate.js +1 -0
- package/dist/templates/builtin/kinetic-text.d.ts +8 -3
- package/dist/templates/builtin/kinetic-text.js +26 -19
- package/dist/templates/builtin/lower-third.d.ts +4 -3
- package/dist/templates/builtin/lower-third.js +10 -5
- package/dist/utils/keyframe-pad.d.ts +24 -0
- package/dist/utils/keyframe-pad.js +26 -0
- package/package.json +1 -1
- package/schemas/animate-config.schema.json +41 -0
|
@@ -10,7 +10,8 @@ import { isTransparentBackground } from "../utils/transparent-background.js";
|
|
|
10
10
|
import { rootSvgA11y } from "../render/format.js";
|
|
11
11
|
import { DEFAULT_TRANSITION_MS, frameAdvanceMs, transitionDurationMs } from "./frame-timeline.js";
|
|
12
12
|
import { offsetEmbeddedAnimatedSvgTimeline } from "./embed-timeline.js";
|
|
13
|
-
import { KEYFRAME_EPSILON, padAfter, padBefore } from "../utils/keyframe-pad.js";
|
|
13
|
+
import { KEYFRAME_EPSILON, cullOverlapPct, padAfter, padBefore } from "../utils/keyframe-pad.js";
|
|
14
|
+
import { interpolateCssValue, resolveEasing } from "./easing.js";
|
|
14
15
|
/**
|
|
15
16
|
* Emit one magic-move frame (DM-898): the frame-i blob (held [start..holdEnd],
|
|
16
17
|
* hard-cut out), the bridge composite (visible only across [holdEnd..transEnd]),
|
|
@@ -30,29 +31,32 @@ function emitMagicMoveFrame(i, frame, mm, startPct, holdEndPct, transEndPct, tot
|
|
|
30
31
|
const beforeH = padBefore(hNum, KEYFRAME_EPSILON.cull, 3);
|
|
31
32
|
const afterT = padAfter(tNum, KEYFRAME_EPSILON.cull, 3);
|
|
32
33
|
// Frame i blob: visible only during its hold, hard-cut out at hold end.
|
|
34
|
+
// DM-1511: opacity does the cut; a SEPARATE wide-overlap `fd-` `visibility`
|
|
35
|
+
// track handles paint-culling so Firefox can't flash a transparent gap at the
|
|
36
|
+
// hand-off (it composites `visibility` off the opacity clock).
|
|
33
37
|
groups.push(` <g class="f f-${i}">\n${frame.svgContent}\n </g>`);
|
|
34
38
|
keyframes.push(`
|
|
35
39
|
@keyframes fv-${i} {
|
|
36
|
-
0% { opacity: 0;
|
|
37
|
-
${beforeS}% { opacity: 0;
|
|
38
|
-
${sNum.toFixed(3)}% { opacity: 1;
|
|
39
|
-
${hNum.toFixed(3)}% { opacity: 1;
|
|
40
|
-
${afterH}% { opacity: 0;
|
|
41
|
-
100% { opacity: 0;
|
|
42
|
-
}
|
|
43
|
-
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s step-end infinite; }`);
|
|
40
|
+
0% { opacity: 0; }
|
|
41
|
+
${beforeS}% { opacity: 0; }
|
|
42
|
+
${sNum.toFixed(3)}% { opacity: 1; }
|
|
43
|
+
${hNum.toFixed(3)}% { opacity: 1; }
|
|
44
|
+
${afterH}% { opacity: 0; }
|
|
45
|
+
100% { opacity: 0; }
|
|
46
|
+
}${buildDisplayKeyframes(`fd-${i}`, startPct, holdEndPct, totalSec)}
|
|
47
|
+
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s step-end infinite, fd-${i} ${totalSec.toFixed(2)}s step-end infinite; }`);
|
|
44
48
|
// Bridge composite: visible during the transition window only.
|
|
45
49
|
groups.push(` <g class="f mm-${i}">\n${mm.compositeSvg}\n </g>`);
|
|
46
50
|
keyframes.push(`
|
|
47
51
|
@keyframes mmv-${i} {
|
|
48
|
-
0% { opacity: 0;
|
|
49
|
-
${beforeH}% { opacity: 0;
|
|
50
|
-
${hNum.toFixed(3)}% { opacity: 1;
|
|
51
|
-
${tNum.toFixed(3)}% { opacity: 1;
|
|
52
|
-
${afterT}% { opacity: 0;
|
|
53
|
-
100% { opacity: 0;
|
|
54
|
-
}
|
|
55
|
-
.mm-${i} { animation: mmv-${i} ${totalSec.toFixed(2)}s step-end infinite; }`);
|
|
52
|
+
0% { opacity: 0; }
|
|
53
|
+
${beforeH}% { opacity: 0; }
|
|
54
|
+
${hNum.toFixed(3)}% { opacity: 1; }
|
|
55
|
+
${tNum.toFixed(3)}% { opacity: 1; }
|
|
56
|
+
${afterT}% { opacity: 0; }
|
|
57
|
+
100% { opacity: 0; }
|
|
58
|
+
}${buildDisplayKeyframes(`mmd-${i}`, holdEndPct, transEndPct, totalSec)}
|
|
59
|
+
.mm-${i} { animation: mmv-${i} ${totalSec.toFixed(2)}s step-end infinite, mmd-${i} ${totalSec.toFixed(2)}s step-end infinite; }`);
|
|
56
60
|
// Per-element slide / fade keyframes within the window (linear interp).
|
|
57
61
|
// The composite is only visible [holdEnd..transEnd], so the held values
|
|
58
62
|
// outside that window are never painted — they just pin the endpoints.
|
|
@@ -127,16 +131,19 @@ holdToEnd) {
|
|
|
127
131
|
const endNum = parseFloat(transEndPct);
|
|
128
132
|
const beforeStart = padBefore(startNum, KEYFRAME_EPSILON.cull, 3);
|
|
129
133
|
const afterEnd = padAfter(endNum, KEYFRAME_EPSILON.cull, 3);
|
|
134
|
+
// DM-1511: opacity does the cut (tight overlap, Firefox-safe); `visibility`
|
|
135
|
+
// is a SEPARATE wide-overlap paint-cull track so Firefox can't flash a
|
|
136
|
+
// transparent gap at the hand-off. (Previously one combined keyframe.)
|
|
130
137
|
keyframes.push(`
|
|
131
138
|
@keyframes fv-${i} {
|
|
132
|
-
0% { opacity: 0;
|
|
133
|
-
${beforeStart}% { opacity: 0;
|
|
134
|
-
${startNum.toFixed(3)}% { opacity: 1;
|
|
135
|
-
${endNum.toFixed(3)}% { opacity: 1;
|
|
136
|
-
${afterEnd}% { opacity: 0;
|
|
137
|
-
100% { opacity: 0;
|
|
138
|
-
}
|
|
139
|
-
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s step-end infinite; }`);
|
|
139
|
+
0% { opacity: 0; }
|
|
140
|
+
${beforeStart}% { opacity: 0; }
|
|
141
|
+
${startNum.toFixed(3)}% { opacity: 1; }
|
|
142
|
+
${endNum.toFixed(3)}% { opacity: 1; }
|
|
143
|
+
${afterEnd}% { opacity: 0; }
|
|
144
|
+
100% { opacity: 0; }
|
|
145
|
+
}${buildDisplayKeyframes(`fd-${i}`, startPct, transEndPct, totalSec)}
|
|
146
|
+
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s step-end infinite, fd-${i} ${totalSec.toFixed(2)}s step-end infinite; }`);
|
|
140
147
|
}
|
|
141
148
|
else {
|
|
142
149
|
const prevEnd = i > 0
|
|
@@ -151,7 +158,7 @@ holdToEnd) {
|
|
|
151
158
|
keyframes.push(`
|
|
152
159
|
@keyframes fv-${i} {
|
|
153
160
|
${offSeg}${startPct}, 100% { opacity: 1; }
|
|
154
|
-
}${buildDisplayKeyframes(`fd-${i}`, fadeInStartPct, "100")}
|
|
161
|
+
}${buildDisplayKeyframes(`fd-${i}`, fadeInStartPct, "100", totalSec)}
|
|
155
162
|
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite, fd-${i} ${totalSec.toFixed(2)}s infinite step-end; }`);
|
|
156
163
|
}
|
|
157
164
|
else {
|
|
@@ -159,7 +166,7 @@ holdToEnd) {
|
|
|
159
166
|
@keyframes fv-${i} {
|
|
160
167
|
0%, ${prevEnd} ${transEndPct}, 100% { opacity: 0; }
|
|
161
168
|
${startPct}, ${holdEndPct} { opacity: 1; }
|
|
162
|
-
}${buildDisplayKeyframes(`fd-${i}`, fadeInStartPct, transEndPct)}
|
|
169
|
+
}${buildDisplayKeyframes(`fd-${i}`, fadeInStartPct, transEndPct, totalSec)}
|
|
163
170
|
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite, fd-${i} ${totalSec.toFixed(2)}s infinite step-end; }`);
|
|
164
171
|
}
|
|
165
172
|
}
|
|
@@ -735,16 +742,26 @@ function pctNum(ms, total) {
|
|
|
735
742
|
* `visibleStartPct` / `visibleEndPct` accept either a numeric-style string
|
|
736
743
|
* (`"12.34"`) or one with a trailing `%` (`"12.34%"`) — `pct()` returns the
|
|
737
744
|
* latter and the unmerged-path keyframes feed either form.
|
|
745
|
+
*
|
|
746
|
+
* DM-1511: the visible window is padded OUTWARD by a wide wall-clock margin
|
|
747
|
+
* (`cullOverlapPct`) so adjacent frames' paint windows OVERLAP. The visual cut
|
|
748
|
+
* is driven by the sibling `opacity` animation (`fv-*`), which Firefox
|
|
749
|
+
* composites in lock-step; `visibility` is only the paint-cull gate, and a
|
|
750
|
+
* sub-millisecond visibility hand-off flashed a transparent gap in Firefox at
|
|
751
|
+
* cut points. Over-wide visibility is harmless — the frame is `opacity:0`
|
|
752
|
+
* outside its true window — but removes any instant where both neighbors are
|
|
753
|
+
* `visibility:hidden`. `totalSec` is the scene length used to size the margin.
|
|
738
754
|
*/
|
|
739
|
-
function buildDisplayKeyframes(name, visibleStartPct, visibleEndPct) {
|
|
755
|
+
function buildDisplayKeyframes(name, visibleStartPct, visibleEndPct, totalSec) {
|
|
740
756
|
// DM-641: kept the function name for callers but the toggle is now on
|
|
741
757
|
// `visibility`, not `display`, for the same reason as `fv-${i}` above —
|
|
742
758
|
// animating `display` away from an element starting `display: none` never
|
|
743
759
|
// ticks in Chromium.
|
|
744
|
-
const
|
|
745
|
-
const
|
|
746
|
-
const
|
|
747
|
-
const
|
|
760
|
+
const margin = cullOverlapPct(totalSec * 1000);
|
|
761
|
+
const start = Math.max(0, parseFloat(String(visibleStartPct)) - margin);
|
|
762
|
+
const end = Math.min(100, parseFloat(String(visibleEndPct)) + margin);
|
|
763
|
+
const startMinus = padBefore(start, KEYFRAME_EPSILON.cull, 3);
|
|
764
|
+
const endPlus = padAfter(end, KEYFRAME_EPSILON.cull, 3);
|
|
748
765
|
return `
|
|
749
766
|
@keyframes ${name} {
|
|
750
767
|
0% { visibility: hidden; }
|
|
@@ -816,7 +833,7 @@ holdToEnd = false) {
|
|
|
816
833
|
@keyframes fv-${i} {
|
|
817
834
|
${fvEnter}
|
|
818
835
|
100% { opacity: 1; }
|
|
819
|
-
}${buildDisplayKeyframes(`fd-${i}`, visStart, "100")}
|
|
836
|
+
}${buildDisplayKeyframes(`fd-${i}`, visStart, "100", totalSec)}
|
|
820
837
|
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite, fd-${i} ${totalSec.toFixed(2)}s infinite step-end; }
|
|
821
838
|
.fp-${i} { animation: fp-${i} ${totalSec.toFixed(2)}s infinite; }`;
|
|
822
839
|
}
|
|
@@ -832,7 +849,7 @@ holdToEnd = false) {
|
|
|
832
849
|
${fvEnter}
|
|
833
850
|
${transEndPct} { opacity: 1; }
|
|
834
851
|
${padAfter(parseFloat(transEndPct), KEYFRAME_EPSILON.slide, 2)}%, 100% { opacity: 0; }
|
|
835
|
-
}${buildDisplayKeyframes(`fd-${i}`, visStart, visEnd)}
|
|
852
|
+
}${buildDisplayKeyframes(`fd-${i}`, visStart, visEnd, totalSec)}
|
|
836
853
|
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite, fd-${i} ${totalSec.toFixed(2)}s infinite step-end; }
|
|
837
854
|
.fp-${i} { animation: fp-${i} ${totalSec.toFixed(2)}s infinite; }`;
|
|
838
855
|
}
|
|
@@ -914,6 +931,34 @@ function offsetForDirection(dir, w, h) {
|
|
|
914
931
|
* visible (+ `delay`), animates to `to` over `duration`, then holds at `to`
|
|
915
932
|
* until the loop restarts.
|
|
916
933
|
*/
|
|
934
|
+
/**
|
|
935
|
+
* DM-1512/1513: compose one keyframe stop's declaration from several property
|
|
936
|
+
* "parts" (a value per fused track). Transform-family tracks
|
|
937
|
+
* (`translateX`/`translateY`/`scale`/raw `transform`) collapse into a SINGLE
|
|
938
|
+
* `transform:` — two `transform:` declarations would clobber each other — while
|
|
939
|
+
* other properties (`opacity`, `clip-path`, …) emit alongside.
|
|
940
|
+
*/
|
|
941
|
+
function composeAnimStop(parts) {
|
|
942
|
+
const transforms = [];
|
|
943
|
+
const others = [];
|
|
944
|
+
for (const p of parts) {
|
|
945
|
+
if (p.property === "translateX")
|
|
946
|
+
transforms.push(`translateX(${p.val})`);
|
|
947
|
+
else if (p.property === "translateY")
|
|
948
|
+
transforms.push(`translateY(${p.val})`);
|
|
949
|
+
else if (p.property === "scale")
|
|
950
|
+
transforms.push(`scale(${p.val})`);
|
|
951
|
+
else if (p.property === "transform")
|
|
952
|
+
transforms.push(p.val);
|
|
953
|
+
else if (p.property === "clipPath")
|
|
954
|
+
others.push(`clip-path: ${p.val};`);
|
|
955
|
+
else
|
|
956
|
+
others.push(`${p.property}: ${p.val};`);
|
|
957
|
+
}
|
|
958
|
+
if (transforms.length > 0)
|
|
959
|
+
others.push(`transform: ${transforms.join(" ")};`);
|
|
960
|
+
return others.join(" ");
|
|
961
|
+
}
|
|
917
962
|
function buildIntraFrameAnimationCss(frames, frameTiming, totalSec) {
|
|
918
963
|
const totalMs = totalSec * 1000;
|
|
919
964
|
const out = [];
|
|
@@ -930,17 +975,9 @@ function buildIntraFrameAnimationCss(frames, frameTiming, totalSec) {
|
|
|
930
975
|
const endMs = startMs + a.duration;
|
|
931
976
|
const startPct = (startMs / totalMs) * 100;
|
|
932
977
|
const endPct = (endMs / totalMs) * 100;
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
if (a.property === "translateY")
|
|
937
|
-
return `transform: translateY(${val});`;
|
|
938
|
-
if (a.property === "scale")
|
|
939
|
-
return `transform: scale(${val});`;
|
|
940
|
-
if (a.property === "clipPath")
|
|
941
|
-
return `clip-path: ${val};`;
|
|
942
|
-
return `${a.property}: ${val};`;
|
|
943
|
-
};
|
|
978
|
+
const tracks = [{ property: a.property, from: a.from, to: a.to }, ...(a.fuse ?? [])];
|
|
979
|
+
const declFrom = composeAnimStop(tracks.map((t) => ({ property: t.property, val: t.from })));
|
|
980
|
+
const declTo = composeAnimStop(tracks.map((t) => ({ property: t.property, val: t.to })));
|
|
944
981
|
// DM-1297: SVG transforms are origin-(0,0); a `transformOrigin` makes a
|
|
945
982
|
// scale/rotate/translate resolve about the element's OWN box (e.g. a
|
|
946
983
|
// center-origin scale-pop) instead of the SVG origin. `transform-box:
|
|
@@ -949,6 +986,58 @@ function buildIntraFrameAnimationCss(frames, frameTiming, totalSec) {
|
|
|
949
986
|
? ` transform-box: fill-box; transform-origin: ${a.transformOrigin};`
|
|
950
987
|
: "";
|
|
951
988
|
const animName = `f${i}-${a.animId}-${ai}`;
|
|
989
|
+
// DM-1517: when a fused track carries its OWN duration/delay/easing, the
|
|
990
|
+
// tracks no longer share one `animation-timing-function`, so we can't emit
|
|
991
|
+
// a from/to pair. Instead SAMPLE each track's eased value over its own
|
|
992
|
+
// window at many stops and emit them with `linear` timing (easing baked
|
|
993
|
+
// in) — still ONE animation / one timeline. Only for one-shot reveals
|
|
994
|
+
// (`repeat` loops keep the shared-timing cycle form).
|
|
995
|
+
const sampledTracks = a.fuse ?? [];
|
|
996
|
+
const needsSampling = a.repeat == null
|
|
997
|
+
&& sampledTracks.some((t) => t.duration != null || t.delay != null || t.easing != null);
|
|
998
|
+
if (needsSampling) {
|
|
999
|
+
const win = tracks.map((t) => {
|
|
1000
|
+
const tStart = frameStartMs + (t.delay ?? delay);
|
|
1001
|
+
const tEnd = tStart + (t.duration ?? a.duration);
|
|
1002
|
+
return {
|
|
1003
|
+
property: t.property, from: t.from, to: t.to,
|
|
1004
|
+
startPct: (tStart / totalMs) * 100,
|
|
1005
|
+
endPct: (tEnd / totalMs) * 100,
|
|
1006
|
+
ease: resolveEasing(t.easing ?? a.easing),
|
|
1007
|
+
};
|
|
1008
|
+
});
|
|
1009
|
+
const minStart = Math.min(...win.map((w) => w.startPct));
|
|
1010
|
+
const maxEnd = Math.max(...win.map((w) => w.endPct));
|
|
1011
|
+
// Stops: 0/100 + every track boundary + a fine grid across the active
|
|
1012
|
+
// span so each track's eased curve is well approximated.
|
|
1013
|
+
const stopSet = new Set([0, 100]);
|
|
1014
|
+
for (const w of win) {
|
|
1015
|
+
stopSet.add(w.startPct);
|
|
1016
|
+
stopSet.add(w.endPct);
|
|
1017
|
+
}
|
|
1018
|
+
const STEP = 2;
|
|
1019
|
+
for (let p = minStart; p < maxEnd; p += STEP)
|
|
1020
|
+
stopSet.add(p);
|
|
1021
|
+
const stops = [...stopSet].filter((p) => p >= 0 && p <= 100).sort((x, y) => x - y);
|
|
1022
|
+
const seen = new Set();
|
|
1023
|
+
const body = stops.map((p) => {
|
|
1024
|
+
const pctStr = p.toFixed(3);
|
|
1025
|
+
if (seen.has(pctStr))
|
|
1026
|
+
return "";
|
|
1027
|
+
seen.add(pctStr);
|
|
1028
|
+
const parts = win.map((w) => {
|
|
1029
|
+
const span = w.endPct - w.startPct;
|
|
1030
|
+
const localT = span > 0 ? Math.min(1, Math.max(0, (p - w.startPct) / span)) : (p >= w.endPct ? 1 : 0);
|
|
1031
|
+
return { property: w.property, val: interpolateCssValue(w.from, w.to, w.ease(localT)) };
|
|
1032
|
+
});
|
|
1033
|
+
return ` ${pctStr}% { ${composeAnimStop(parts)} }`;
|
|
1034
|
+
}).filter((s) => s !== "").join("\n");
|
|
1035
|
+
out.push(` @keyframes ${animName} {
|
|
1036
|
+
${body}
|
|
1037
|
+
}
|
|
1038
|
+
.anim-${a.animId} { animation: ${animName} ${totalSec.toFixed(2)}s linear infinite;${originDecl} }`);
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
952
1041
|
if (a.repeat != null) {
|
|
953
1042
|
// DM-869: repeating animation (blink / pulse / breathe). The keyframe is
|
|
954
1043
|
// a single from→to cycle on the animation's own `duration` clock, looped
|
|
@@ -971,8 +1060,8 @@ function buildIntraFrameAnimationCss(frames, frameTiming, totalSec) {
|
|
|
971
1060
|
const iterations = a.repeat === "infinite" ? "infinite" : String(a.repeat);
|
|
972
1061
|
const direction = a.alternate === true ? " alternate" : "";
|
|
973
1062
|
out.push(` @keyframes ${animName} {
|
|
974
|
-
0% { ${
|
|
975
|
-
100% { ${
|
|
1063
|
+
0% { ${declFrom} }
|
|
1064
|
+
100% { ${declTo} }
|
|
976
1065
|
}
|
|
977
1066
|
.anim-${a.animId} { animation: ${animName} ${a.duration}ms ${easing} ${startMs.toFixed(0)}ms ${iterations}${direction} both;${originDecl} }`);
|
|
978
1067
|
}
|
|
@@ -981,10 +1070,10 @@ function buildIntraFrameAnimationCss(frames, frameTiming, totalSec) {
|
|
|
981
1070
|
// [startPct, endPct], hold `to` afterwards, mapped onto the global scene
|
|
982
1071
|
// clock so it replays in sync each scene loop.
|
|
983
1072
|
out.push(` @keyframes ${animName} {
|
|
984
|
-
0% { ${
|
|
985
|
-
${startPct.toFixed(3)}% { ${
|
|
986
|
-
${endPct.toFixed(3)}% { ${
|
|
987
|
-
100% { ${
|
|
1073
|
+
0% { ${declFrom} }
|
|
1074
|
+
${startPct.toFixed(3)}% { ${declFrom} }
|
|
1075
|
+
${endPct.toFixed(3)}% { ${declTo} }
|
|
1076
|
+
100% { ${declTo} }
|
|
988
1077
|
}
|
|
989
1078
|
.anim-${a.animId} { animation: ${animName} ${totalSec.toFixed(2)}s ${easing} infinite;${originDecl} }`);
|
|
990
1079
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Easing evaluation + CSS-value interpolation for BAKED keyframe sampling
|
|
3
|
+
* (DM-1517). Used by the animator when a fused animation's tracks have different
|
|
4
|
+
* windows/easings and so can't share one `animation-timing-function`: instead of
|
|
5
|
+
* a from→to pair, the animator samples each track's eased value at many stops
|
|
6
|
+
* and emits them with `linear` timing (the easing is baked into the values), so
|
|
7
|
+
* several property tracks with independent timing still animate as ONE CSS
|
|
8
|
+
* animation — one timeline, immune to Firefox's off-main-thread desync
|
|
9
|
+
* (docs/84). Pure functions, no I/O.
|
|
10
|
+
*/
|
|
11
|
+
/** A normalized easing: progress `t` in [0,1] → eased output in [0,1]. */
|
|
12
|
+
export type EasingFn = (t: number) => number;
|
|
13
|
+
/** Solve a CSS `cubic-bezier(x1,y1,x2,y2)` for y at x=t (endpoints (0,0),(1,1)). */
|
|
14
|
+
export declare function cubicBezier(x1: number, y1: number, x2: number, y2: number): EasingFn;
|
|
15
|
+
/**
|
|
16
|
+
* Parse a CSS easing string into an `EasingFn`. Handles the named keywords and
|
|
17
|
+
* `cubic-bezier(a,b,c,d)`. `step`/`steps()` and anything unrecognized fall back
|
|
18
|
+
* to `linear` — the sampled path is for smooth curves; discrete steps don't need
|
|
19
|
+
* baking (they'd be authored as explicit stops).
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveEasing(css: string | undefined): EasingFn;
|
|
22
|
+
/**
|
|
23
|
+
* Interpolate between two CSS value strings at `t` in [0,1] by matching their
|
|
24
|
+
* numeric tokens pairwise and lerping each, keeping the surrounding literal
|
|
25
|
+
* skeleton. Handles the value shapes the intra-frame animator produces —
|
|
26
|
+
* `"0.3"`→`"1"` (scale), `"-0.6em"`→`"0em"` (translate), `"240px"`→`"0px"`,
|
|
27
|
+
* `"inset(-10% 100% -10% 0)"`→`"inset(-10% 0% -10% 0)"` (clip-path). If the two
|
|
28
|
+
* strings don't share the same non-numeric skeleton (e.g. different units), it
|
|
29
|
+
* can't be smoothly interpolated, so it steps at the midpoint.
|
|
30
|
+
*/
|
|
31
|
+
export declare function interpolateCssValue(from: string, to: string, t: number): string;
|
|
Binary file
|
|
@@ -259,5 +259,22 @@ export declare const intraFrameAnimationSchema: z.ZodObject<{
|
|
|
259
259
|
delay: z.ZodOptional<z.ZodNumber>;
|
|
260
260
|
repeat: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodLiteral<"infinite">]>>;
|
|
261
261
|
alternate: z.ZodOptional<z.ZodBoolean>;
|
|
262
|
+
fuse: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
263
|
+
property: z.ZodEnum<{
|
|
264
|
+
width: "width";
|
|
265
|
+
clipPath: "clipPath";
|
|
266
|
+
height: "height";
|
|
267
|
+
transform: "transform";
|
|
268
|
+
opacity: "opacity";
|
|
269
|
+
scale: "scale";
|
|
270
|
+
translateX: "translateX";
|
|
271
|
+
translateY: "translateY";
|
|
272
|
+
}>;
|
|
273
|
+
from: z.ZodString;
|
|
274
|
+
to: z.ZodString;
|
|
275
|
+
duration: z.ZodOptional<z.ZodNumber>;
|
|
276
|
+
delay: z.ZodOptional<z.ZodNumber>;
|
|
277
|
+
easing: z.ZodOptional<z.ZodString>;
|
|
278
|
+
}, z.core.$strip>>>;
|
|
262
279
|
}, z.core.$strip>;
|
|
263
280
|
export type IntraFrameAnimation = z.infer<typeof intraFrameAnimationSchema>;
|
|
@@ -222,4 +222,35 @@ export const intraFrameAnimationSchema = z.object({
|
|
|
222
222
|
repeat: z.union([z.number(), z.literal("infinite")]).optional(),
|
|
223
223
|
/** DM-869: when true, the loop ping-pongs `from`→`to`→`from` (CSS `animation-direction: alternate`). */
|
|
224
224
|
alternate: z.boolean().optional(),
|
|
225
|
+
/**
|
|
226
|
+
* DM-1512/1513: additional property tracks fused into THIS animation so they
|
|
227
|
+
* animate as ONE CSS animation on one element — a single timeline that can't
|
|
228
|
+
* desync. Firefox composites `opacity`/`transform` off the main thread and,
|
|
229
|
+
* under load, can demote one of two SEPARATE animations to the main thread
|
|
230
|
+
* while the other stays on the compositor, drifting them apart (e.g. a fade
|
|
231
|
+
* running ahead of its slide/scale). A single animation is always sampled at
|
|
232
|
+
* one instant regardless of thread, so fusing removes that failure mode.
|
|
233
|
+
*
|
|
234
|
+
* By default each fused track rides the primary entry's window (`delay` +
|
|
235
|
+
* `duration`) and `easing`, emitted as from/to stops. A track MAY override
|
|
236
|
+
* `duration` / `delay` / `easing` for independent timing (e.g. a fast fade over
|
|
237
|
+
* a slower slide, or a different curve) — when any track does, the whole
|
|
238
|
+
* animation is emitted by SAMPLING each track's eased value over its own window
|
|
239
|
+
* at many stops with `linear` timing (DM-1517), so it stays one animation / one
|
|
240
|
+
* timeline. The primary `property` is track 0; these are the rest. Multiple
|
|
241
|
+
* transform-family tracks (`translateX`/`translateY`/`scale`/`transform`) are
|
|
242
|
+
* composed into a single `transform:` declaration; other properties (`opacity`,
|
|
243
|
+
* `clip-path`, …) emit alongside. See docs/84-viewer-browser-support.md.
|
|
244
|
+
*/
|
|
245
|
+
fuse: z.array(z.object({
|
|
246
|
+
property: z.enum(["width", "height", "opacity", "transform", "translateX", "translateY", "scale", "clipPath"]),
|
|
247
|
+
from: z.string(),
|
|
248
|
+
to: z.string(),
|
|
249
|
+
/** Override the primary's duration for this track (ms). Triggers sampling. */
|
|
250
|
+
duration: z.number().optional(),
|
|
251
|
+
/** Override the primary's delay for this track (ms). Triggers sampling. */
|
|
252
|
+
delay: z.number().optional(),
|
|
253
|
+
/** Override the primary's easing for this track. Triggers sampling. */
|
|
254
|
+
easing: z.string().optional(),
|
|
255
|
+
})).optional(),
|
|
225
256
|
});
|
package/dist/cli/animate.d.ts
CHANGED
|
@@ -678,6 +678,23 @@ export declare const animateConfigSchema: z.ZodObject<{
|
|
|
678
678
|
translateY: "translateY";
|
|
679
679
|
}>;
|
|
680
680
|
transformOrigin: z.ZodOptional<z.ZodString>;
|
|
681
|
+
fuse: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
682
|
+
property: z.ZodEnum<{
|
|
683
|
+
width: "width";
|
|
684
|
+
clipPath: "clipPath";
|
|
685
|
+
height: "height";
|
|
686
|
+
transform: "transform";
|
|
687
|
+
opacity: "opacity";
|
|
688
|
+
scale: "scale";
|
|
689
|
+
translateX: "translateX";
|
|
690
|
+
translateY: "translateY";
|
|
691
|
+
}>;
|
|
692
|
+
from: z.ZodString;
|
|
693
|
+
to: z.ZodString;
|
|
694
|
+
duration: z.ZodOptional<z.ZodNumber>;
|
|
695
|
+
delay: z.ZodOptional<z.ZodNumber>;
|
|
696
|
+
easing: z.ZodOptional<z.ZodString>;
|
|
697
|
+
}, z.core.$strip>>>;
|
|
681
698
|
selector: z.ZodString;
|
|
682
699
|
repeat: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodLiteral<"infinite">]>>;
|
|
683
700
|
}, z.core.$strip>>>;
|
package/dist/cli/animate.js
CHANGED
|
@@ -10,9 +10,14 @@
|
|
|
10
10
|
* Same two animation constraints as `background-loop` (doc 71): only one
|
|
11
11
|
* intra-frame animation applies per captured element, and SVG transforms are
|
|
12
12
|
* origin-(0,0). So each animated unit is a `.kt-w-N` transform-wrapper (rise /
|
|
13
|
-
* slide via origin-safe translateX/translateY) around a `.kt-wi-N`
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* slide via origin-safe translateX/translateY) around a `.kt-wi-N` inner span.
|
|
14
|
+
* DM-1512/1513: the move and the fade are FUSED into that one wrapper animation
|
|
15
|
+
* (via the animation entry's `fuse` list) rather than a separate opacity
|
|
16
|
+
* animation on the inner span — one CSS timeline can't desync under Firefox's
|
|
17
|
+
* off-main-thread compositing (see docs/84). The `.kt-wi-N` inner span is now
|
|
18
|
+
* inert (it just carries the glyph). The reveal is one-shot (no repeat): units
|
|
19
|
+
* hold `from` until their staggered turn, animate in, then hold `to` so the
|
|
20
|
+
* headline stays assembled.
|
|
16
21
|
*/
|
|
17
22
|
import { z } from "zod";
|
|
18
23
|
import type { AnimateConfig } from "../../cli/animate.js";
|
|
@@ -10,9 +10,14 @@
|
|
|
10
10
|
* Same two animation constraints as `background-loop` (doc 71): only one
|
|
11
11
|
* intra-frame animation applies per captured element, and SVG transforms are
|
|
12
12
|
* origin-(0,0). So each animated unit is a `.kt-w-N` transform-wrapper (rise /
|
|
13
|
-
* slide via origin-safe translateX/translateY) around a `.kt-wi-N`
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* slide via origin-safe translateX/translateY) around a `.kt-wi-N` inner span.
|
|
14
|
+
* DM-1512/1513: the move and the fade are FUSED into that one wrapper animation
|
|
15
|
+
* (via the animation entry's `fuse` list) rather than a separate opacity
|
|
16
|
+
* animation on the inner span — one CSS timeline can't desync under Firefox's
|
|
17
|
+
* off-main-thread compositing (see docs/84). The `.kt-wi-N` inner span is now
|
|
18
|
+
* inert (it just carries the glyph). The reveal is one-shot (no repeat): units
|
|
19
|
+
* hold `from` until their staggered turn, animate in, then hold `to` so the
|
|
20
|
+
* headline stays assembled.
|
|
16
21
|
*/
|
|
17
22
|
import { runSingleFrameGenerator } from "../run-single-frame.js";
|
|
18
23
|
import { z } from "zod";
|
|
@@ -256,39 +261,41 @@ export function buildKineticAnimations(p, plan) {
|
|
|
256
261
|
// `boomerang`: each unit assembles then disassembles forever, phase-offset by
|
|
257
262
|
// its stagger. `loop`: one-shot; the infinitely-looping scene replays it.
|
|
258
263
|
const loopFields = p.loop === "boomerang" ? { repeat: "infinite", alternate: true } : {};
|
|
264
|
+
// DM-1512/1513: each unit's move + fade is emitted as ONE animation on the
|
|
265
|
+
// wrapper (`.kt-w-*`), with the fade FUSED into the move via `fuse` rather than
|
|
266
|
+
// a separate opacity animation on the inner span. One CSS animation is one
|
|
267
|
+
// timeline, so the fade and the move stay in perfect sync — immune to
|
|
268
|
+
// Firefox's off-main-thread compositing, which under load demotes one of two
|
|
269
|
+
// SEPARATE animations to the main thread and drifts them apart (a fade running
|
|
270
|
+
// ahead of its slide/scale). The fused fade rides the move's window + easing.
|
|
271
|
+
// See docs/84-viewer-browser-support.md.
|
|
272
|
+
const fade = [{ property: "opacity", from: "0", to: "1" }];
|
|
259
273
|
for (const line of plan.lines) {
|
|
260
274
|
for (const units of line) {
|
|
261
275
|
for (const u of units) {
|
|
262
276
|
const delay = u.index * p.staggerMs;
|
|
263
|
-
|
|
264
|
-
anims.push({
|
|
265
|
-
selector: `.kt-wi-${u.index}`,
|
|
266
|
-
property: "opacity",
|
|
267
|
-
from: "0",
|
|
268
|
-
to: "1",
|
|
269
|
-
duration: p.revealMs,
|
|
270
|
-
delay,
|
|
271
|
-
easing: "ease-out",
|
|
272
|
-
...loopFields,
|
|
273
|
-
});
|
|
274
|
-
// Move/reveal in, on the wrapper. `fade` has no wrapper animation.
|
|
277
|
+
const sel = `.kt-w-${u.index}`;
|
|
275
278
|
if (p.variant === "rise") {
|
|
276
|
-
anims.push({ selector:
|
|
279
|
+
anims.push({ selector: sel, property: "translateY", from: "0.55em", to: "0em", duration: p.revealMs, delay, easing: "cubic-bezier(0.22,1,0.36,1)", fuse: fade, ...loopFields });
|
|
277
280
|
}
|
|
278
281
|
else if (p.variant === "slide") {
|
|
279
|
-
anims.push({ selector:
|
|
282
|
+
anims.push({ selector: sel, property: "translateX", from: "-0.6em", to: "0em", duration: p.revealMs, delay, easing: "cubic-bezier(0.22,1,0.36,1)", fuse: fade, ...loopFields });
|
|
280
283
|
}
|
|
281
284
|
else if (p.variant === "clip") {
|
|
282
285
|
// Left-to-right wipe via the `clipPath` intra-frame property (doc 08):
|
|
283
286
|
// `inset(0 100% 0 0)` clips everything but the left edge; animating the
|
|
284
287
|
// right inset to 0 reveals the unit left→right.
|
|
285
|
-
anims.push({ selector:
|
|
288
|
+
anims.push({ selector: sel, property: "clipPath", from: "inset(-10% 100% -10% 0)", to: "inset(-10% 0% -10% 0)", duration: p.revealMs, delay, easing: "cubic-bezier(0.22,1,0.36,1)", fuse: fade, ...loopFields });
|
|
286
289
|
}
|
|
287
290
|
else if (p.variant === "pop") {
|
|
288
291
|
// Scale-pop: grow from small to full about the unit's OWN CENTER
|
|
289
292
|
// (`transformOrigin`, DM-1297), with a back-eased overshoot. Without the
|
|
290
293
|
// center origin an SVG scale would shrink toward the canvas corner.
|
|
291
|
-
anims.push({ selector:
|
|
294
|
+
anims.push({ selector: sel, property: "scale", from: "0.3", to: "1", duration: p.revealMs, delay, easing: "cubic-bezier(0.34,1.56,0.64,1)", transformOrigin: "center", fuse: fade, ...loopFields });
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
// `fade` variant: no move — just the fade, on the wrapper.
|
|
298
|
+
anims.push({ selector: sel, property: "opacity", from: "0", to: "1", duration: p.revealMs, delay, easing: "ease-out", ...loopFields });
|
|
292
299
|
}
|
|
293
300
|
}
|
|
294
301
|
}
|
|
@@ -37,9 +37,10 @@ export declare const lowerThirdParamsSchema: z.ZodObject<{
|
|
|
37
37
|
export type LowerThirdParams = z.infer<typeof lowerThirdParamsSchema>;
|
|
38
38
|
/**
|
|
39
39
|
* Build the standalone HTML for the banner. Pure function (no I/O) so it's unit-
|
|
40
|
-
* testable without a browser. The `.lt`
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* testable without a browser. The `.lt-panel` slides (translateY) with the fade
|
|
41
|
+
* (opacity) FUSED into the same animation (DM-1512/1513) — one CSS timeline so
|
|
42
|
+
* the two can't desync under Firefox's off-main-thread compositing. `.lt` is a
|
|
43
|
+
* bare flex wrapper around the single `.lt-panel`.
|
|
43
44
|
*/
|
|
44
45
|
export declare function buildLowerThirdHtml(p: LowerThirdParams): string;
|
|
45
46
|
export declare const lowerThirdTemplate: Template<LowerThirdParams>;
|
|
@@ -39,9 +39,10 @@ function alignmentFor(position) {
|
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* Build the standalone HTML for the banner. Pure function (no I/O) so it's unit-
|
|
42
|
-
* testable without a browser. The `.lt`
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* testable without a browser. The `.lt-panel` slides (translateY) with the fade
|
|
43
|
+
* (opacity) FUSED into the same animation (DM-1512/1513) — one CSS timeline so
|
|
44
|
+
* the two can't desync under Firefox's off-main-thread compositing. `.lt` is a
|
|
45
|
+
* bare flex wrapper around the single `.lt-panel`.
|
|
45
46
|
*/
|
|
46
47
|
export function buildLowerThirdHtml(p) {
|
|
47
48
|
const { justify, align } = alignmentFor(p.position);
|
|
@@ -109,9 +110,13 @@ export const lowerThirdTemplate = {
|
|
|
109
110
|
width: params.width,
|
|
110
111
|
height: params.height,
|
|
111
112
|
durationMs: params.holdMs,
|
|
113
|
+
// DM-1512/1513: slide + fade FUSED into one animation on `.lt-panel` (the
|
|
114
|
+
// sole child of `.lt`, so fading it is equivalent to fading `.lt`). One
|
|
115
|
+
// CSS animation = one timeline, so the fade and slide can't desync under
|
|
116
|
+
// Firefox's off-main-thread compositing under load. The fade rides the
|
|
117
|
+
// slide's 600ms/cubic window (was a separate 450ms/ease-out track).
|
|
112
118
|
animations: [
|
|
113
|
-
{ selector: ".lt", property: "
|
|
114
|
-
{ selector: ".lt-panel", property: "translateY", from: rise, to: "0px", duration: 600, easing: "cubic-bezier(0.22,1,0.36,1)" },
|
|
119
|
+
{ selector: ".lt-panel", property: "translateY", from: rise, to: "0px", duration: 600, easing: "cubic-bezier(0.22,1,0.36,1)", fuse: [{ property: "opacity", from: "0", to: "1" }] },
|
|
115
120
|
],
|
|
116
121
|
});
|
|
117
122
|
},
|
|
@@ -22,3 +22,27 @@ export declare const KEYFRAME_EPSILON: {
|
|
|
22
22
|
export declare function padBefore(pct: number, epsilon: number, precision: number): string;
|
|
23
23
|
/** Percentage nudged UP by `epsilon` (capped at 100), printed at `precision`. */
|
|
24
24
|
export declare function padAfter(pct: number, epsilon: number, precision: number): string;
|
|
25
|
+
/**
|
|
26
|
+
* DM-1511: Firefox composites a `visibility` animation off the main-thread /
|
|
27
|
+
* opacity clock. At a step-end frame hand-off it can drop the OUTGOING frame's
|
|
28
|
+
* paint a few compositor frames BEFORE the incoming frame starts painting,
|
|
29
|
+
* flashing the transparent page-through gap for ~30-70ms — the "hard flash to
|
|
30
|
+
* white/transparent at cut points" bug. Chromium and Safari keep the two in
|
|
31
|
+
* lock-step, so a sub-millisecond overlap is enough there; Firefox is not.
|
|
32
|
+
*
|
|
33
|
+
* The fix is to drive the visual cut with `opacity` alone (Firefox composites
|
|
34
|
+
* that correctly, and the tight `cull` overlap already guarantees a seamless
|
|
35
|
+
* hand-off) and use `visibility` ONLY as the paint-cull gate (DM-599), with its
|
|
36
|
+
* visible window padded WIDE — a fixed wall-clock margin on each side — so
|
|
37
|
+
* adjacent frames' paint windows overlap well past any compositor slop. The
|
|
38
|
+
* frame is `opacity:0` outside its true window, so the wide visibility window is
|
|
39
|
+
* never itself visible; it only means a frame is *paintable* a little early and
|
|
40
|
+
* a little late, which is harmless.
|
|
41
|
+
*
|
|
42
|
+
* Expressed as wall-clock (not a fixed %) so the overlap always beats the
|
|
43
|
+
* compositor slop, which is a handful of vsync frames regardless of how long the
|
|
44
|
+
* scene is. 150 ms is ~2x the largest gap observed (~70 ms).
|
|
45
|
+
*/
|
|
46
|
+
export declare const VISIBILITY_CULL_OVERLAP_MS = 150;
|
|
47
|
+
/** `VISIBILITY_CULL_OVERLAP_MS` as a percentage of a scene `totalMs` long. */
|
|
48
|
+
export declare function cullOverlapPct(totalMs: number): number;
|
|
@@ -26,3 +26,29 @@ export function padBefore(pct, epsilon, precision) {
|
|
|
26
26
|
export function padAfter(pct, epsilon, precision) {
|
|
27
27
|
return Math.min(100, pct + epsilon).toFixed(precision);
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* DM-1511: Firefox composites a `visibility` animation off the main-thread /
|
|
31
|
+
* opacity clock. At a step-end frame hand-off it can drop the OUTGOING frame's
|
|
32
|
+
* paint a few compositor frames BEFORE the incoming frame starts painting,
|
|
33
|
+
* flashing the transparent page-through gap for ~30-70ms — the "hard flash to
|
|
34
|
+
* white/transparent at cut points" bug. Chromium and Safari keep the two in
|
|
35
|
+
* lock-step, so a sub-millisecond overlap is enough there; Firefox is not.
|
|
36
|
+
*
|
|
37
|
+
* The fix is to drive the visual cut with `opacity` alone (Firefox composites
|
|
38
|
+
* that correctly, and the tight `cull` overlap already guarantees a seamless
|
|
39
|
+
* hand-off) and use `visibility` ONLY as the paint-cull gate (DM-599), with its
|
|
40
|
+
* visible window padded WIDE — a fixed wall-clock margin on each side — so
|
|
41
|
+
* adjacent frames' paint windows overlap well past any compositor slop. The
|
|
42
|
+
* frame is `opacity:0` outside its true window, so the wide visibility window is
|
|
43
|
+
* never itself visible; it only means a frame is *paintable* a little early and
|
|
44
|
+
* a little late, which is harmless.
|
|
45
|
+
*
|
|
46
|
+
* Expressed as wall-clock (not a fixed %) so the overlap always beats the
|
|
47
|
+
* compositor slop, which is a handful of vsync frames regardless of how long the
|
|
48
|
+
* scene is. 150 ms is ~2x the largest gap observed (~70 ms).
|
|
49
|
+
*/
|
|
50
|
+
export const VISIBILITY_CULL_OVERLAP_MS = 150;
|
|
51
|
+
/** `VISIBILITY_CULL_OVERLAP_MS` as a percentage of a scene `totalMs` long. */
|
|
52
|
+
export function cullOverlapPct(totalMs) {
|
|
53
|
+
return totalMs > 0 ? (VISIBILITY_CULL_OVERLAP_MS / totalMs) * 100 : 0;
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "domotion-svg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "DOM-to-animated-SVG renderer. Captures HTML/CSS via Playwright Chromium and converts it to self-contained SVG with CSS animations — pixel-faithful demos that scale crisply and load lazily.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Brian Westphal",
|
|
@@ -1355,6 +1355,47 @@
|
|
|
1355
1355
|
"alternate": {
|
|
1356
1356
|
"type": "boolean"
|
|
1357
1357
|
},
|
|
1358
|
+
"fuse": {
|
|
1359
|
+
"type": "array",
|
|
1360
|
+
"items": {
|
|
1361
|
+
"type": "object",
|
|
1362
|
+
"properties": {
|
|
1363
|
+
"property": {
|
|
1364
|
+
"type": "string",
|
|
1365
|
+
"enum": [
|
|
1366
|
+
"width",
|
|
1367
|
+
"height",
|
|
1368
|
+
"opacity",
|
|
1369
|
+
"transform",
|
|
1370
|
+
"translateX",
|
|
1371
|
+
"translateY",
|
|
1372
|
+
"scale",
|
|
1373
|
+
"clipPath"
|
|
1374
|
+
]
|
|
1375
|
+
},
|
|
1376
|
+
"from": {
|
|
1377
|
+
"type": "string"
|
|
1378
|
+
},
|
|
1379
|
+
"to": {
|
|
1380
|
+
"type": "string"
|
|
1381
|
+
},
|
|
1382
|
+
"duration": {
|
|
1383
|
+
"type": "number"
|
|
1384
|
+
},
|
|
1385
|
+
"delay": {
|
|
1386
|
+
"type": "number"
|
|
1387
|
+
},
|
|
1388
|
+
"easing": {
|
|
1389
|
+
"type": "string"
|
|
1390
|
+
}
|
|
1391
|
+
},
|
|
1392
|
+
"required": [
|
|
1393
|
+
"property",
|
|
1394
|
+
"from",
|
|
1395
|
+
"to"
|
|
1396
|
+
]
|
|
1397
|
+
}
|
|
1398
|
+
},
|
|
1358
1399
|
"selector": {
|
|
1359
1400
|
"type": "string"
|
|
1360
1401
|
}
|