tweakit 0.6.0 → 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 +5 -5
- package/README.md +3 -3
- package/dist/tweaks/auto.js +1 -1
- package/dist/tweaks/auto.js.map +1 -1
- 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/core.js +1 -1
- package/dist/tweaks/core.js.map +3 -3
- 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-CLD7UAVI.js → heavy-NPFIGHRM.js} +2 -2
- package/dist/tweaks/image-HTQQ4WX6.js +4 -0
- package/dist/tweaks/image-HTQQ4WX6.js.map +6 -0
- package/dist/tweaks/{image-store-WRKY2NPU.js → image-store-QD5SMZXG.js} +1 -1
- 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 +5 -4
- package/dist/tweaks.js.map +3 -3
- package/dist/types/tweaks/enhance.d.ts +3 -2
- package/dist/types/tweaks/types.d.ts +278 -72
- package/package.json +5 -2
- package/src/tweaks/auto.ts +9 -5
- package/src/tweaks/clip.ts +84 -22
- package/src/tweaks/controls/basic.ts +463 -139
- package/src/tweaks/controls/bezier.ts +152 -47
- package/src/tweaks/controls/color.ts +610 -161
- package/src/tweaks/controls/gradient.ts +256 -77
- package/src/tweaks/controls/image.ts +388 -132
- package/src/tweaks/controls/interval.ts +126 -41
- package/src/tweaks/controls/monitor.ts +142 -36
- package/src/tweaks/controls/motion.ts +231 -56
- package/src/tweaks/controls/plot.ts +321 -101
- package/src/tweaks/controls/point.ts +134 -45
- package/src/tweaks/controls/shadow.ts +103 -30
- package/src/tweaks/controls/sides.ts +150 -40
- package/src/tweaks/controls/spring.ts +220 -60
- package/src/tweaks/controls/tabs.ts +39 -18
- package/src/tweaks/core.ts +1 -1
- package/src/tweaks/easing.ts +45 -20
- package/src/tweaks/enhance.ts +21 -6
- package/src/tweaks/feedback.ts +133 -34
- package/src/tweaks/fold.ts +22 -10
- package/src/tweaks/heavy.ts +461 -137
- package/src/tweaks/icons.ts +12 -5
- package/src/tweaks/image-store.ts +131 -43
- package/src/tweaks/lazy.ts +36 -19
- package/src/tweaks/markup.ts +255 -92
- package/src/tweaks/panel.ts +1262 -438
- package/src/tweaks/presets.ts +85 -0
- package/src/tweaks/schema.ts +213 -51
- package/src/tweaks/shadow-root.ts +178 -0
- package/src/tweaks/shared.ts +802 -190
- package/src/tweaks/standalone.ts +120 -30
- package/src/tweaks/types.ts +644 -143
- package/src/tweaks/undo.ts +104 -0
- package/src/tweaks.css +556 -550
- package/src/wide-gamut.ts +473 -125
- package/dist/tweaks/bezier-JFEZMCLP.js +0 -2
- package/dist/tweaks/bezier-JFEZMCLP.js.map +0 -6
- package/dist/tweaks/chunk-6UO4DHBN.js +0 -2
- package/dist/tweaks/chunk-6UO4DHBN.js.map +0 -6
- package/dist/tweaks/chunk-DIQMLBAS.js +0 -2
- package/dist/tweaks/chunk-DIQMLBAS.js.map +0 -6
- package/dist/tweaks/chunk-XE6OUJBG.js +0 -2
- package/dist/tweaks/chunk-XE6OUJBG.js.map +0 -6
- package/dist/tweaks/clip-WW7JYO4X.js +0 -2
- package/dist/tweaks/clip-WW7JYO4X.js.map +0 -6
- package/dist/tweaks/color-YMMFURP6.js +0 -2
- package/dist/tweaks/fold-U6YRZLCJ.js +0 -2
- package/dist/tweaks/fold-U6YRZLCJ.js.map +0 -6
- package/dist/tweaks/gradient-JER7PW4D.js +0 -2
- package/dist/tweaks/gradient-JER7PW4D.js.map +0 -6
- package/dist/tweaks/image-H6QSLGO3.js +0 -4
- package/dist/tweaks/image-H6QSLGO3.js.map +0 -6
- package/dist/tweaks/image-store-WRKY2NPU.js.map +0 -6
- package/dist/tweaks/interval-TEUHQGZN.js +0 -2
- package/dist/tweaks/interval-TEUHQGZN.js.map +0 -6
- package/dist/tweaks/markup-NIBJD6E2.js +0 -2
- package/dist/tweaks/markup-NIBJD6E2.js.map +0 -6
- package/dist/tweaks/monitor-3BY7YCGX.js +0 -3
- package/dist/tweaks/monitor-3BY7YCGX.js.map +0 -6
- package/dist/tweaks/motion-FQPUBKKM.js +0 -2
- package/dist/tweaks/motion-FQPUBKKM.js.map +0 -6
- package/dist/tweaks/plot-2HBNBZSG.js +0 -3
- package/dist/tweaks/plot-2HBNBZSG.js.map +0 -6
- package/dist/tweaks/point-VV6XV3I6.js +0 -2
- package/dist/tweaks/point-VV6XV3I6.js.map +0 -6
- package/dist/tweaks/shadow-5E4E55I2.js +0 -2
- package/dist/tweaks/shadow-5E4E55I2.js.map +0 -6
- package/dist/tweaks/sides-P3STEVYY.js +0 -2
- package/dist/tweaks/sides-P3STEVYY.js.map +0 -6
- package/dist/tweaks/spring-HUVSXNVN.js +0 -2
- package/dist/tweaks/spring-HUVSXNVN.js.map +0 -6
- package/dist/tweaks/tabs-76FW7LQ7.js +0 -2
- package/dist/tweaks/tabs-76FW7LQ7.js.map +0 -6
- /package/dist/tweaks/{color-YMMFURP6.js.map → color-UGYMAF3W.js.map} +0 -0
- /package/dist/tweaks/{heavy-CLD7UAVI.js.map → heavy-NPFIGHRM.js.map} +0 -0
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
// ── Motion — a transition's timing: an easing curve with a duration, or a spring, in one popover over one value. The
|
|
2
2
|
// value carries the CSS it resolves to — `duration` in ms and an `easing` string, cubic-bezier() or a sampled linear(). Lazy. ──
|
|
3
|
-
import {
|
|
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";
|
|
4
15
|
import { numField, triggerRow, svgEl, springCurve, springRun, timeFromSpring, notice, SPRING_DUR } from "../heavy.js";
|
|
5
16
|
import { KEYWORDS } from "../easing.js";
|
|
6
17
|
import type { OnChange } from "../shared.js";
|
|
@@ -9,21 +20,46 @@ import type { Control, MotionValue, MotionInput } from "../types.js";
|
|
|
9
20
|
|
|
10
21
|
type Mode = "easing" | "spring";
|
|
11
22
|
type Curve = [number, number, number, number];
|
|
12
|
-
interface SpringOut {
|
|
23
|
+
interface SpringOut {
|
|
24
|
+
stiffness: number;
|
|
25
|
+
damping: number;
|
|
26
|
+
mass: number;
|
|
27
|
+
visualDuration?: number;
|
|
28
|
+
bounce?: number;
|
|
29
|
+
}
|
|
13
30
|
|
|
14
|
-
const DUR_DEF = 300,
|
|
31
|
+
const DUR_DEF = 300,
|
|
32
|
+
DUR_MAX = 10000,
|
|
33
|
+
STAGE_PAD = 16; // ms; the stage's side padding (tweaks.css)
|
|
15
34
|
const BOUNCE_MAX = 1; // Motion's whole range: its damping-ratio floor of 0.05 keeps a bounce of 1 settling
|
|
16
|
-
|
|
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);
|
|
17
38
|
const curveOf = (c: unknown): Curve | null => {
|
|
18
|
-
|
|
19
|
-
if (
|
|
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
|
+
}
|
|
20
48
|
return null;
|
|
21
49
|
};
|
|
22
|
-
const springKeys = (v: MotionInput) =>
|
|
23
|
-
|
|
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.
|
|
24
54
|
const springTime = (v: MotionInput) => {
|
|
25
55
|
const out: { mode: "time"; visualDuration?: number; bounce?: number } = { mode: "time" };
|
|
26
|
-
if (has(v.stiffness)) {
|
|
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
|
+
}
|
|
27
63
|
if (has(v.visualDuration)) out.visualDuration = +v.visualDuration!;
|
|
28
64
|
if (has(v.bounce)) out.bounce = clamp(+v.bounce!, -1, BOUNCE_MAX);
|
|
29
65
|
return out;
|
|
@@ -32,56 +68,116 @@ const springTime = (v: MotionInput) => {
|
|
|
32
68
|
// rounds and clamps: unless a time pair rides along that isn't the one they resolve to (this control's own value back).
|
|
33
69
|
const exactSpring = (v: MotionInput): SpringOut | null => {
|
|
34
70
|
if (!has(v.stiffness) || !has(v.damping)) return null;
|
|
35
|
-
const k = +v.stiffness!,
|
|
71
|
+
const k = +v.stiffness!,
|
|
72
|
+
d = +v.damping!,
|
|
73
|
+
m = has(v.mass) ? +v.mass! : 1;
|
|
36
74
|
if (!(k > 0 && d >= 0 && m > 0)) return null;
|
|
37
|
-
const t = timeFromSpring(k, d, m),
|
|
38
|
-
|
|
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 };
|
|
39
80
|
};
|
|
40
|
-
|
|
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;
|
|
41
84
|
|
|
42
85
|
function createMotion(meta: Meta, onChange: OnChange): Control {
|
|
43
|
-
const
|
|
86
|
+
const tr = words();
|
|
87
|
+
const Bezier = getControl("cubicbezier"),
|
|
88
|
+
Spring = getControl("spring");
|
|
44
89
|
if (!Bezier || !Spring) throw new Error("[tweaks] motion: the bezier and spring controls are not loaded");
|
|
45
90
|
const init: MotionInput = isObj(meta.value) ? meta.value : {};
|
|
46
91
|
let mode = modeOf(init, "easing");
|
|
47
|
-
let curve = curveOf(init.curve) ?? (KEYWORDS.ease.slice() as Curve),
|
|
92
|
+
let curve = curveOf(init.curve) ?? (KEYWORDS.ease.slice() as Curve),
|
|
93
|
+
dur = has(init.duration) ? clamp(+init.duration!, 0, DUR_MAX) : DUR_DEF;
|
|
48
94
|
|
|
49
95
|
// ── Row — label, a mini curve, a summary. ──
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
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");
|
|
55
106
|
right.append(chip, valueEl);
|
|
56
107
|
|
|
57
108
|
// ── Popover — the mode switch, the preview, one editor at a time. ──
|
|
58
|
-
const pop = el("div", "
|
|
59
|
-
|
|
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
|
+
});
|
|
60
117
|
curve = bezier.get() as Curve; // as the editor fitted it, so no invalid cubic-bezier()
|
|
61
118
|
// Host-written physics the editor can't draw exactly: it shows the nearest, clamped here so it doesn't warn as well.
|
|
62
|
-
let exact: SpringOut | null = null,
|
|
119
|
+
let exact: SpringOut | null = null,
|
|
120
|
+
said = false;
|
|
63
121
|
const editorTime = (inp: MotionInput) => {
|
|
64
122
|
exact = exactSpring(inp);
|
|
65
123
|
if (!exact) return springTime(inp);
|
|
66
|
-
const vd = clamp(exact.visualDuration!, SPRING_DUR[0], SPRING_DUR[1]),
|
|
67
|
-
|
|
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
|
+
}
|
|
68
130
|
return { mode: "time" as const, visualDuration: vd, bounce: exact.bounce };
|
|
69
131
|
};
|
|
70
132
|
// Time mode only; its Mode row is hidden (tweaks.css). An edit there drops the kept physics.
|
|
71
|
-
const spring = Spring({ ...meta, type: "spring", label: "", value: editorTime(init) } as Meta, () => {
|
|
133
|
+
const spring = Spring({ ...meta, type: "spring", label: "", value: editorTime(init) } as Meta, () => {
|
|
134
|
+
exact = null;
|
|
135
|
+
update();
|
|
136
|
+
emit();
|
|
137
|
+
});
|
|
72
138
|
const springNow = () => exact ?? (spring.get() as SpringOut);
|
|
73
|
-
const durFld = numField(
|
|
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
|
+
);
|
|
74
147
|
dur = durFld.get(); // as the field snapped it
|
|
75
|
-
const easingPane = el("div", "
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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);
|
|
80
175
|
body.append(typeRow, easingPane, springPane, stage);
|
|
81
176
|
root.append(pop);
|
|
82
177
|
|
|
83
178
|
const value = (): MotionValue => {
|
|
84
|
-
if (mode === "easing")
|
|
179
|
+
if (mode === "easing")
|
|
180
|
+
return { mode, curve: curve.slice() as Curve, duration: dur, easing: `cubic-bezier(${curve.join(", ")})` };
|
|
85
181
|
// The run lasts until the spring is at rest (the drawn curve stops at 2.2 s): springRun.
|
|
86
182
|
const s = springNow();
|
|
87
183
|
return { mode, ...s, ...springRun(s.stiffness, s.damping, s.mass) };
|
|
@@ -90,50 +186,129 @@ function createMotion(meta: Meta, onChange: OnChange): Control {
|
|
|
90
186
|
|
|
91
187
|
// Web Animations, no frame loop to own. A click or an open flips direction and restarts; an edit lets a run in flight
|
|
92
188
|
// finish — a handle drag fires per pointer move, and restarting each strobed the box.
|
|
93
|
-
let far = false,
|
|
189
|
+
let far = false,
|
|
190
|
+
anim: Animation | null = null;
|
|
94
191
|
const replay = (flip: boolean) => {
|
|
95
|
-
if (flip) far = !far;
|
|
192
|
+
if (flip) far = !far;
|
|
193
|
+
else if (anim && anim.playState === "running") return;
|
|
96
194
|
const v = value();
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
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
|
+
}
|
|
100
203
|
anim?.cancel();
|
|
101
204
|
const frames = [{ transform: from }, { transform: to }];
|
|
102
|
-
try {
|
|
103
|
-
|
|
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
|
+
}
|
|
104
211
|
};
|
|
105
212
|
stage.addEventListener("click", () => replay(true));
|
|
106
|
-
stage.tabIndex = 0;
|
|
107
|
-
stage.
|
|
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
|
+
});
|
|
108
222
|
|
|
109
223
|
// The chip's mini curve, y up, scaled so an overshoot still fits.
|
|
110
224
|
const curvePath = () => {
|
|
111
225
|
let pts: [number, number][];
|
|
112
|
-
if (mode === "easing") {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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(" ");
|
|
116
248
|
};
|
|
117
249
|
// The revealed editor built at 0×0 and onLive may have dropped its resize listener while the pop was detached, so
|
|
118
250
|
// re-point it at its own value, which redraws at real size.
|
|
119
|
-
const resync = () => {
|
|
120
|
-
|
|
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
|
+
};
|
|
121
260
|
// The summary shows the authored duration; a spring's emitted run (its settle) is longer.
|
|
122
|
-
const summary = () =>
|
|
123
|
-
|
|
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
|
+
};
|
|
124
270
|
|
|
125
|
-
|
|
126
|
-
|
|
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();
|
|
127
283
|
|
|
128
284
|
// Set/restore: either mode's shape or a bare keyword; derived fields (easing, a spring's duration) are ignored.
|
|
129
285
|
const set = (v: unknown) => {
|
|
130
|
-
|
|
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;
|
|
131
295
|
if (!inp) return;
|
|
132
296
|
mode = modeOf(inp, mode);
|
|
133
|
-
const c = curveOf(inp.curve);
|
|
134
|
-
|
|
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
|
+
}
|
|
135
308
|
if (springKeys(inp)) spring.set(editorTime(inp));
|
|
136
|
-
modeSeg.set(mode);
|
|
309
|
+
modeSeg.set(mode);
|
|
310
|
+
showMode();
|
|
311
|
+
update();
|
|
137
312
|
};
|
|
138
313
|
return { el: root, set, get: value, destroy: () => spring.destroy && spring.destroy() };
|
|
139
314
|
}
|