tweakit 0.5.2 → 0.7.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/AGENTS.md +32 -30
- package/README.md +55 -29
- package/THIRD-PARTY-NOTICES.md +0 -1
- package/dist/tweaks/auto.js +2 -0
- package/dist/tweaks/auto.js.map +6 -0
- package/dist/tweaks/bezier-S2BOXQBM.js +2 -0
- package/dist/tweaks/bezier-S2BOXQBM.js.map +6 -0
- package/dist/tweaks/chunk-3MZAAMC2.js +2 -0
- package/dist/tweaks/chunk-3MZAAMC2.js.map +6 -0
- package/dist/tweaks/chunk-ETB35KFV.js +2 -0
- package/dist/tweaks/chunk-ETB35KFV.js.map +6 -0
- package/dist/tweaks/chunk-YB5GKWZI.js +2 -0
- package/dist/tweaks/chunk-YB5GKWZI.js.map +6 -0
- package/dist/tweaks/clip-A3VI53BK.js +2 -0
- package/dist/tweaks/clip-A3VI53BK.js.map +6 -0
- package/dist/tweaks/color-UGYMAF3W.js +2 -0
- package/dist/tweaks/color-UGYMAF3W.js.map +6 -0
- package/dist/tweaks/core.js +2 -1
- package/dist/tweaks/core.js.map +6 -0
- package/dist/tweaks/fold-GY47HIQA.js +2 -0
- package/dist/tweaks/fold-GY47HIQA.js.map +6 -0
- package/dist/tweaks/gradient-BTX25WSH.js +2 -0
- package/dist/tweaks/gradient-BTX25WSH.js.map +6 -0
- package/dist/tweaks/heavy-NPFIGHRM.js +2 -0
- package/dist/tweaks/heavy-NPFIGHRM.js.map +6 -0
- package/dist/tweaks/image-HTQQ4WX6.js +4 -0
- package/dist/tweaks/image-HTQQ4WX6.js.map +6 -0
- package/dist/tweaks/image-store-QD5SMZXG.js +2 -0
- package/dist/tweaks/image-store-QD5SMZXG.js.map +6 -0
- package/dist/tweaks/interval-BHPI4LRN.js +2 -0
- package/dist/tweaks/interval-BHPI4LRN.js.map +6 -0
- package/dist/tweaks/markup-QM53TQJZ.js +2 -0
- package/dist/tweaks/markup-QM53TQJZ.js.map +6 -0
- package/dist/tweaks/monitor-722WIYGW.js +3 -0
- package/dist/tweaks/monitor-722WIYGW.js.map +6 -0
- package/dist/tweaks/motion-HMCZK6HJ.js +2 -0
- package/dist/tweaks/motion-HMCZK6HJ.js.map +6 -0
- package/dist/tweaks/plot-D3NQ6XNN.js +3 -0
- package/dist/tweaks/plot-D3NQ6XNN.js.map +6 -0
- package/dist/tweaks/point-XDXKMDXH.js +2 -0
- package/dist/tweaks/point-XDXKMDXH.js.map +6 -0
- package/dist/tweaks/presets-EUWNSCUW.js +2 -0
- package/dist/tweaks/presets-EUWNSCUW.js.map +6 -0
- package/dist/tweaks/shadow-AM7VSRNS.js +2 -0
- package/dist/tweaks/shadow-AM7VSRNS.js.map +6 -0
- package/dist/tweaks/shadow-root-RYUV3AGS.js +3 -0
- package/dist/tweaks/shadow-root-RYUV3AGS.js.map +6 -0
- package/dist/tweaks/sides-JWIURDA4.js +2 -0
- package/dist/tweaks/sides-JWIURDA4.js.map +6 -0
- package/dist/tweaks/spring-BF5GBTPM.js +2 -0
- package/dist/tweaks/spring-BF5GBTPM.js.map +6 -0
- package/dist/tweaks/tabs-PRVUUZXZ.js +2 -0
- package/dist/tweaks/tabs-PRVUUZXZ.js.map +6 -0
- package/dist/tweaks.css +1 -1
- package/dist/tweaks.js +7 -3
- package/dist/tweaks.js.map +6 -0
- package/dist/types/tweaks/easing.d.ts +8 -11
- package/dist/types/tweaks/enhance.d.ts +3 -2
- package/dist/types/tweaks/panel.d.ts +6 -4
- package/dist/types/tweaks/schema.d.ts +4 -8
- package/dist/types/tweaks/standalone.d.ts +3 -3
- package/dist/types/tweaks/types.d.ts +499 -116
- package/package.json +30 -5
- package/src/tweaks/auto.ts +16 -0
- package/src/tweaks/clip.ts +156 -0
- package/src/tweaks/controls/basic.ts +727 -0
- package/src/tweaks/controls/bezier.ts +204 -0
- package/src/tweaks/controls/color.ts +861 -0
- package/src/tweaks/controls/gradient.ts +419 -0
- package/src/tweaks/controls/image.ts +555 -0
- package/src/tweaks/controls/interval.ts +189 -0
- package/src/tweaks/controls/monitor.ts +211 -0
- package/src/tweaks/controls/motion.ts +316 -0
- package/src/tweaks/controls/plot.ts +440 -0
- package/src/tweaks/controls/point.ts +208 -0
- package/src/tweaks/controls/shadow.ts +156 -0
- package/src/tweaks/controls/sides.ts +196 -0
- package/src/tweaks/controls/spring.ts +308 -0
- package/src/tweaks/controls/tabs.ts +68 -0
- package/src/tweaks/core.ts +9 -0
- package/src/tweaks/easing.ts +118 -0
- package/src/tweaks/enhance.ts +35 -0
- package/src/tweaks/feedback.ts +218 -0
- package/src/tweaks/fold.ts +33 -0
- package/src/tweaks/heavy.ts +626 -0
- package/src/tweaks/icons.ts +26 -0
- package/src/tweaks/image-store.ts +189 -0
- package/src/tweaks/lazy.ts +63 -0
- package/src/tweaks/markup.ts +317 -0
- package/src/tweaks/panel.ts +1693 -0
- package/src/tweaks/presets.ts +85 -0
- package/src/tweaks/schema.ts +351 -0
- package/src/tweaks/shadow-root.ts +178 -0
- package/src/tweaks/shared.ts +1137 -0
- package/src/tweaks/single.ts +22 -0
- package/src/tweaks/standalone.ts +179 -0
- package/src/tweaks/types.ts +1027 -0
- package/src/tweaks/undo.ts +104 -0
- package/src/tweaks.css +965 -0
- package/src/wide-gamut.ts +658 -0
- package/dist/tweaks/bezier-2RABC6LD.js +0 -1
- package/dist/tweaks/chunk-4ATI34PB.js +0 -1
- package/dist/tweaks/chunk-7OSCMZ2J.js +0 -1
- package/dist/tweaks/chunk-W3JNIJ2F.js +0 -1
- package/dist/tweaks/clip-ECGCECEC.js +0 -1
- package/dist/tweaks/colour-2RE3FQ5E.js +0 -1
- package/dist/tweaks/fold-UGN7TXGX.js +0 -1
- package/dist/tweaks/gradient-DB6XTDHO.js +0 -1
- package/dist/tweaks/heavy-PKOMHB3C.js +0 -1
- package/dist/tweaks/image-7MCLBOZ3.js +0 -1
- package/dist/tweaks/interval-L2E4L3AZ.js +0 -1
- package/dist/tweaks/markup-ZJJ6CCWM.js +0 -1
- package/dist/tweaks/monitor-IEK742PG.js +0 -2
- package/dist/tweaks/motion-N5UNVR55.js +0 -1
- package/dist/tweaks/plot-VVTOY3RA.js +0 -2
- package/dist/tweaks/point-YMMHSLO7.js +0 -1
- package/dist/tweaks/shadow-IKYOV3VN.js +0 -1
- package/dist/tweaks/sides-FH227Z54.js +0 -1
- package/dist/tweaks/spring-3PKAFIH7.js +0 -1
- package/dist/tweaks/tabs-BKK5NIIY.js +0 -1
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// ── Motion — a transition's timing: an easing curve with a duration, or a spring, in one popover over one value. The
|
|
2
|
+
// value carries the CSS it resolves to — `duration` in ms and an `easing` string, cubic-bezier() or a sampled linear(). Lazy. ──
|
|
3
|
+
import {
|
|
4
|
+
el,
|
|
5
|
+
txt,
|
|
6
|
+
clamp,
|
|
7
|
+
isObj,
|
|
8
|
+
createSegmented,
|
|
9
|
+
getControl,
|
|
10
|
+
popover,
|
|
11
|
+
registerControl,
|
|
12
|
+
REDUCE_MOTION,
|
|
13
|
+
words,
|
|
14
|
+
} from "../shared.js";
|
|
15
|
+
import { numField, triggerRow, svgEl, springCurve, springRun, timeFromSpring, notice, SPRING_DUR } from "../heavy.js";
|
|
16
|
+
import { KEYWORDS } from "../easing.js";
|
|
17
|
+
import type { OnChange } from "../shared.js";
|
|
18
|
+
import type { Meta } from "../schema.js";
|
|
19
|
+
import type { Control, MotionValue, MotionInput } from "../types.js";
|
|
20
|
+
|
|
21
|
+
type Mode = "easing" | "spring";
|
|
22
|
+
type Curve = [number, number, number, number];
|
|
23
|
+
interface SpringOut {
|
|
24
|
+
stiffness: number;
|
|
25
|
+
damping: number;
|
|
26
|
+
mass: number;
|
|
27
|
+
visualDuration?: number;
|
|
28
|
+
bounce?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DUR_DEF = 300,
|
|
32
|
+
DUR_MAX = 10000,
|
|
33
|
+
STAGE_PAD = 16; // ms; the stage's side padding (tweaks.css)
|
|
34
|
+
const BOUNCE_MAX = 1; // Motion's whole range: its damping-ratio floor of 0.05 keeps a bounce of 1 settling
|
|
35
|
+
// a number, or a numeric string — not `true`, `[]` or " "
|
|
36
|
+
const has = (x: unknown) =>
|
|
37
|
+
typeof x === "number" ? Number.isFinite(x) : typeof x === "string" && x.trim() !== "" && Number.isFinite(+x);
|
|
38
|
+
const curveOf = (c: unknown): Curve | null => {
|
|
39
|
+
// the CSS keywords, linear's identity included; null-prototype, so "constructor" is not one
|
|
40
|
+
if (typeof c === "string") {
|
|
41
|
+
const k = c === "linear" ? [0, 0, 1, 1] : KEYWORDS[c];
|
|
42
|
+
return k ? (k.slice() as Curve) : null;
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(c) && c.length === 4) {
|
|
45
|
+
const m = c.map(Number);
|
|
46
|
+
return m.every(Number.isFinite) ? (m as Curve) : null;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
};
|
|
50
|
+
const springKeys = (v: MotionInput) =>
|
|
51
|
+
has(v.stiffness) || has(v.damping) || has(v.mass) || has(v.visualDuration) || has(v.bounce);
|
|
52
|
+
// Physics keys map to the visualDuration + bounce Motion would resolve to them (timeFromSpring). An explicit
|
|
53
|
+
// duration/bounce wins.
|
|
54
|
+
const springTime = (v: MotionInput) => {
|
|
55
|
+
const out: { mode: "time"; visualDuration?: number; bounce?: number } = { mode: "time" };
|
|
56
|
+
if (has(v.stiffness)) {
|
|
57
|
+
const k = +v.stiffness!,
|
|
58
|
+
m = has(v.mass) ? +v.mass! : 1,
|
|
59
|
+
t = timeFromSpring(k, has(v.damping) ? +v.damping! : 2 * Math.sqrt(k * m), m);
|
|
60
|
+
out.visualDuration = t.visualDuration;
|
|
61
|
+
if (has(v.damping)) out.bounce = clamp(t.bounce, -1, BOUNCE_MAX);
|
|
62
|
+
}
|
|
63
|
+
if (has(v.visualDuration)) out.visualDuration = +v.visualDuration!;
|
|
64
|
+
if (has(v.bounce)) out.bounce = clamp(+v.bounce!, -1, BOUNCE_MAX);
|
|
65
|
+
return out;
|
|
66
|
+
};
|
|
67
|
+
// A stiffness and damping the host wrote, kept as written rather than resolved through the editor's time pair, which
|
|
68
|
+
// rounds and clamps: unless a time pair rides along that isn't the one they resolve to (this control's own value back).
|
|
69
|
+
const exactSpring = (v: MotionInput): SpringOut | null => {
|
|
70
|
+
if (!has(v.stiffness) || !has(v.damping)) return null;
|
|
71
|
+
const k = +v.stiffness!,
|
|
72
|
+
d = +v.damping!,
|
|
73
|
+
m = has(v.mass) ? +v.mass! : 1;
|
|
74
|
+
if (!(k > 0 && d >= 0 && m > 0)) return null;
|
|
75
|
+
const t = timeFromSpring(k, d, m),
|
|
76
|
+
off = (a: unknown, b: number) => has(a) && Math.abs(+a! - b) > 1e-6 * Math.max(1, Math.abs(b));
|
|
77
|
+
return off(v.visualDuration, t.visualDuration) || off(v.bounce, t.bounce)
|
|
78
|
+
? null
|
|
79
|
+
: { stiffness: k, damping: d, mass: m, ...t };
|
|
80
|
+
};
|
|
81
|
+
// a curve that doesn't parse is no reason to switch
|
|
82
|
+
const modeOf = (v: MotionInput, keep: Mode): Mode =>
|
|
83
|
+
v.mode === "spring" || v.mode === "easing" ? v.mode : springKeys(v) ? "spring" : curveOf(v.curve) ? "easing" : keep;
|
|
84
|
+
|
|
85
|
+
function createMotion(meta: Meta, onChange: OnChange): Control {
|
|
86
|
+
const tr = words();
|
|
87
|
+
const Bezier = getControl("cubicbezier"),
|
|
88
|
+
Spring = getControl("spring");
|
|
89
|
+
if (!Bezier || !Spring) throw new Error("[tweaks] motion: the bezier and spring controls are not loaded");
|
|
90
|
+
const init: MotionInput = isObj(meta.value) ? meta.value : {};
|
|
91
|
+
let mode = modeOf(init, "easing");
|
|
92
|
+
let curve = curveOf(init.curve) ?? (KEYWORDS.ease.slice() as Curve),
|
|
93
|
+
dur = has(init.duration) ? clamp(+init.duration!, 0, DUR_MAX) : DUR_DEF;
|
|
94
|
+
|
|
95
|
+
// ── Row — label, a mini curve, a summary. ──
|
|
96
|
+
// ??: an explicit "" label renders none
|
|
97
|
+
const { root, trigger, right } = triggerRow("twk-motion", meta.label ?? tr("motion", "Motion"));
|
|
98
|
+
const chip = el("span", "twk-trigger-chip twk-motion-chip");
|
|
99
|
+
const chipSvg = svgEl("svg");
|
|
100
|
+
chipSvg.setAttribute("viewBox", "0 0 100 100");
|
|
101
|
+
chipSvg.setAttribute("preserveAspectRatio", "none");
|
|
102
|
+
const chipPath = svgEl("path");
|
|
103
|
+
chipSvg.append(chipPath);
|
|
104
|
+
chip.append(chipSvg);
|
|
105
|
+
const valueEl = el("span", "twk-trigger-value");
|
|
106
|
+
right.append(chip, valueEl);
|
|
107
|
+
|
|
108
|
+
// ── Popover — the mode switch, the preview, one editor at a time. ──
|
|
109
|
+
const pop = el("div", "twk-color-pop twk-motion-pop"),
|
|
110
|
+
body = el("div", "twk-motion-body");
|
|
111
|
+
pop.append(body);
|
|
112
|
+
const bezier = Bezier({ ...meta, type: "cubicbezier", label: "", value: curve.slice() } as Meta, (v) => {
|
|
113
|
+
curve = v as Curve;
|
|
114
|
+
update();
|
|
115
|
+
emit();
|
|
116
|
+
});
|
|
117
|
+
curve = bezier.get() as Curve; // as the editor fitted it, so no invalid cubic-bezier()
|
|
118
|
+
// Host-written physics the editor can't draw exactly: it shows the nearest, clamped here so it doesn't warn as well.
|
|
119
|
+
let exact: SpringOut | null = null,
|
|
120
|
+
said = false;
|
|
121
|
+
const editorTime = (inp: MotionInput) => {
|
|
122
|
+
exact = exactSpring(inp);
|
|
123
|
+
if (!exact) return springTime(inp);
|
|
124
|
+
const vd = clamp(exact.visualDuration!, SPRING_DUR[0], SPRING_DUR[1]),
|
|
125
|
+
z = exact.damping / (2 * Math.sqrt(exact.stiffness * exact.mass));
|
|
126
|
+
if ((vd !== exact.visualDuration || z < 0.05 || z > 20) && !said) {
|
|
127
|
+
said = true;
|
|
128
|
+
notice("springKept", [meta.key, exact.stiffness, exact.damping, exact.mass]);
|
|
129
|
+
}
|
|
130
|
+
return { mode: "time" as const, visualDuration: vd, bounce: exact.bounce };
|
|
131
|
+
};
|
|
132
|
+
// Time mode only; its Mode row is hidden (tweaks.css). An edit there drops the kept physics.
|
|
133
|
+
const spring = Spring({ ...meta, type: "spring", label: "", value: editorTime(init) } as Meta, () => {
|
|
134
|
+
exact = null;
|
|
135
|
+
update();
|
|
136
|
+
emit();
|
|
137
|
+
});
|
|
138
|
+
const springNow = () => exact ?? (spring.get() as SpringOut);
|
|
139
|
+
const durFld = numField(
|
|
140
|
+
{ label: tr("duration", "Duration"), value: dur, step: 10, min: 0, max: DUR_MAX, unit: "ms" },
|
|
141
|
+
(v) => {
|
|
142
|
+
dur = clamp(v, 0, DUR_MAX);
|
|
143
|
+
update();
|
|
144
|
+
emit();
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
dur = durFld.get(); // as the field snapped it
|
|
148
|
+
const easingPane = el("div", "twk-motion-pane"),
|
|
149
|
+
springPane = el("div", "twk-motion-pane"),
|
|
150
|
+
durRow = el("div", "twk-fields");
|
|
151
|
+
durRow.append(durFld.el);
|
|
152
|
+
easingPane.append(bezier.el, durRow);
|
|
153
|
+
springPane.append(spring.el);
|
|
154
|
+
const stage = el("div", "twk-motion-stage"),
|
|
155
|
+
box = el("div", "twk-motion-box");
|
|
156
|
+
stage.append(box);
|
|
157
|
+
const modeSeg = createSegmented(
|
|
158
|
+
[
|
|
159
|
+
{ value: "easing", label: tr("easing", "Easing") },
|
|
160
|
+
{ value: "spring", label: tr("spring", "Spring") },
|
|
161
|
+
],
|
|
162
|
+
mode,
|
|
163
|
+
(m: unknown) => {
|
|
164
|
+
if ((m === "easing" || m === "spring") && m !== mode) {
|
|
165
|
+
mode = m;
|
|
166
|
+
showMode();
|
|
167
|
+
update();
|
|
168
|
+
emit();
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
tr("motionType", "Motion type"),
|
|
172
|
+
);
|
|
173
|
+
const typeRow = el("div", "twk-row");
|
|
174
|
+
typeRow.append(txt("span", "twk-row-label", tr("type", "Type")), modeSeg.el);
|
|
175
|
+
body.append(typeRow, easingPane, springPane, stage);
|
|
176
|
+
root.append(pop);
|
|
177
|
+
|
|
178
|
+
const value = (): MotionValue => {
|
|
179
|
+
if (mode === "easing")
|
|
180
|
+
return { mode, curve: curve.slice() as Curve, duration: dur, easing: `cubic-bezier(${curve.join(", ")})` };
|
|
181
|
+
// The run lasts until the spring is at rest (the drawn curve stops at 2.2 s): springRun.
|
|
182
|
+
const s = springNow();
|
|
183
|
+
return { mode, ...s, ...springRun(s.stiffness, s.damping, s.mass) };
|
|
184
|
+
};
|
|
185
|
+
const emit = () => onChange(value());
|
|
186
|
+
|
|
187
|
+
// Web Animations, no frame loop to own. A click or an open flips direction and restarts; an edit lets a run in flight
|
|
188
|
+
// finish — a handle drag fires per pointer move, and restarting each strobed the box.
|
|
189
|
+
let far = false,
|
|
190
|
+
anim: Animation | null = null;
|
|
191
|
+
const replay = (flip: boolean) => {
|
|
192
|
+
if (flip) far = !far;
|
|
193
|
+
else if (anim && anim.playState === "running") return;
|
|
194
|
+
const v = value();
|
|
195
|
+
// measured now: the stage is 0 wide until the pop opens
|
|
196
|
+
const travel = Math.max(0, stage.clientWidth - box.offsetWidth - 2 * STAGE_PAD);
|
|
197
|
+
const from = `translateX(${far ? 0 : travel}px)`,
|
|
198
|
+
to = `translateX(${far ? travel : 0}px)`;
|
|
199
|
+
if (REDUCE_MOTION.matches || typeof box.animate !== "function") {
|
|
200
|
+
box.style.transform = to;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
anim?.cancel();
|
|
204
|
+
const frames = [{ transform: from }, { transform: to }];
|
|
205
|
+
try {
|
|
206
|
+
anim = box.animate(frames, { duration: v.duration, easing: v.easing, fill: "forwards" });
|
|
207
|
+
} catch {
|
|
208
|
+
// no linear() support: a plain run rather than nothing
|
|
209
|
+
anim = box.animate(frames, { duration: v.duration, fill: "forwards" });
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
stage.addEventListener("click", () => replay(true));
|
|
213
|
+
stage.tabIndex = 0;
|
|
214
|
+
stage.setAttribute("role", "button");
|
|
215
|
+
stage.setAttribute("aria-label", tr("replay", "Replay the motion"));
|
|
216
|
+
stage.addEventListener("keydown", (e) => {
|
|
217
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
replay(true);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// The chip's mini curve, y up, scaled so an overshoot still fits.
|
|
224
|
+
const curvePath = () => {
|
|
225
|
+
let pts: [number, number][];
|
|
226
|
+
if (mode === "easing") {
|
|
227
|
+
const [x1, y1, x2, y2] = curve;
|
|
228
|
+
pts = [];
|
|
229
|
+
for (let i = 0; i <= 24; i++) {
|
|
230
|
+
const t = i / 24,
|
|
231
|
+
u = 1 - t,
|
|
232
|
+
a = 3 * u * u * t,
|
|
233
|
+
b = 3 * u * t * t,
|
|
234
|
+
c = t * t * t;
|
|
235
|
+
pts.push([a * x1 + b * x2 + c, a * y1 + b * y2 + c]);
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
const s = springNow();
|
|
239
|
+
pts = springCurve(s.stiffness, s.damping, s.mass, 32).map((y, i) => [i / 31, y]);
|
|
240
|
+
}
|
|
241
|
+
const hi = Math.max(1, ...pts.map((p) => p[1])),
|
|
242
|
+
lo = Math.min(0, ...pts.map((p) => p[1]));
|
|
243
|
+
return pts
|
|
244
|
+
.map(
|
|
245
|
+
([x, y], i) => `${i ? "L" : "M"}${(10 + x * 80).toFixed(1)},${(90 - ((y - lo) / (hi - lo)) * 80).toFixed(1)}`,
|
|
246
|
+
)
|
|
247
|
+
.join(" ");
|
|
248
|
+
};
|
|
249
|
+
// The revealed editor built at 0×0 and onLive may have dropped its resize listener while the pop was detached, so
|
|
250
|
+
// re-point it at its own value, which redraws at real size.
|
|
251
|
+
const resync = () => {
|
|
252
|
+
if (mode === "easing") bezier.set(curve.slice());
|
|
253
|
+
else spring.set(spring.get());
|
|
254
|
+
};
|
|
255
|
+
const showMode = () => {
|
|
256
|
+
easingPane.style.display = mode === "easing" ? "" : "none";
|
|
257
|
+
springPane.style.display = mode === "spring" ? "" : "none";
|
|
258
|
+
resync();
|
|
259
|
+
};
|
|
260
|
+
// The summary shows the authored duration; a spring's emitted run (its settle) is longer.
|
|
261
|
+
const summary = () =>
|
|
262
|
+
mode === "easing"
|
|
263
|
+
? tr("easingSummary", "Easing · {ms} ms", { ms: dur })
|
|
264
|
+
: tr("springSummary", "Spring · {ms} ms", { ms: Math.round((springNow().visualDuration ?? 0) * 1000) });
|
|
265
|
+
const update = () => {
|
|
266
|
+
chipPath.setAttribute("d", curvePath());
|
|
267
|
+
valueEl.textContent = summary();
|
|
268
|
+
replay(false);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// hidden until open: measure now; focus the checked option
|
|
272
|
+
popover(root, trigger, pop, {
|
|
273
|
+
fallbackH: 380,
|
|
274
|
+
onOpen: () => {
|
|
275
|
+
modeSeg.set(mode);
|
|
276
|
+
resync();
|
|
277
|
+
replay(true);
|
|
278
|
+
},
|
|
279
|
+
initialFocus: () => modeSeg.el.querySelector<HTMLElement>('[tabindex="0"]'),
|
|
280
|
+
});
|
|
281
|
+
showMode();
|
|
282
|
+
update();
|
|
283
|
+
|
|
284
|
+
// Set/restore: either mode's shape or a bare keyword; derived fields (easing, a spring's duration) are ignored.
|
|
285
|
+
const set = (v: unknown) => {
|
|
286
|
+
// a bare 4-array is the bezier's own shape
|
|
287
|
+
const inp: MotionInput | null =
|
|
288
|
+
typeof v === "string"
|
|
289
|
+
? { curve: v }
|
|
290
|
+
: Array.isArray(v) && v.length === 4
|
|
291
|
+
? { curve: v as Curve }
|
|
292
|
+
: isObj(v)
|
|
293
|
+
? v
|
|
294
|
+
: null;
|
|
295
|
+
if (!inp) return;
|
|
296
|
+
mode = modeOf(inp, mode);
|
|
297
|
+
const c = curveOf(inp.curve);
|
|
298
|
+
// read back fitted
|
|
299
|
+
if (c) {
|
|
300
|
+
bezier.set(c.slice());
|
|
301
|
+
curve = bezier.get() as Curve;
|
|
302
|
+
}
|
|
303
|
+
// read back snapped
|
|
304
|
+
if (mode === "easing" && has(inp.duration)) {
|
|
305
|
+
durFld.set(clamp(+inp.duration!, 0, DUR_MAX));
|
|
306
|
+
dur = durFld.get();
|
|
307
|
+
}
|
|
308
|
+
if (springKeys(inp)) spring.set(editorTime(inp));
|
|
309
|
+
modeSeg.set(mode);
|
|
310
|
+
showMode();
|
|
311
|
+
update();
|
|
312
|
+
};
|
|
313
|
+
return { el: root, set, get: value, destroy: () => spring.destroy && spring.destroy() };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
registerControl("motion", createMotion);
|