@agent-native/core 0.85.5 → 0.85.7
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AssistantChat.tsx +1 -1
- package/corpus/core/src/client/use-pinch-zoom.ts +76 -11
- package/corpus/core/src/collab/presence.ts +63 -3
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +245 -77
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +190 -13
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +16 -4
- package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +46 -3
- package/corpus/templates/design/AGENTS.md +40 -4
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +20 -73
- package/corpus/templates/design/actions/apply-motion-edit.ts +29 -2
- package/corpus/templates/design/actions/apply-visual-edit.ts +4 -2
- package/corpus/templates/design/actions/connect-localhost.ts +25 -18
- package/corpus/templates/design/actions/edit-design.ts +5 -3
- package/corpus/templates/design/actions/export-pdf.ts +8 -1
- package/corpus/templates/design/actions/export-zip.ts +10 -1
- package/corpus/templates/design/actions/get-motion-timeline.ts +26 -20
- package/corpus/templates/design/actions/grant-localhost-write-consent.ts +10 -0
- package/corpus/templates/design/actions/insert-asset.ts +190 -7
- package/corpus/templates/design/actions/open-visual-edit.ts +27 -5
- package/corpus/templates/design/actions/present-design-variants.ts +12 -8
- package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +18 -14
- package/corpus/templates/design/actions/run-design-audit.ts +7 -4
- package/corpus/templates/design/actions/write-local-file.ts +47 -22
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +221 -66
- package/corpus/templates/design/app/components/design/EditPanel.tsx +650 -237
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +526 -127
- package/corpus/templates/design/app/components/design/MotionDock.tsx +234 -46
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1520 -410
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +25 -2
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +640 -72
- package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +605 -0
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +50 -26
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +16 -4
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +90 -103
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +111 -5
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +13 -2
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +42 -2
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +11 -2
- package/corpus/templates/design/app/components/design/types.ts +16 -0
- package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +24 -6
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +15 -1
- package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +77 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/i18n-data.ts +18 -0
- package/corpus/templates/design/app/lib/design-import.ts +95 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +4015 -840
- package/corpus/templates/design/changelog/2026-07-02-creating-two-frames-in-a-row-no-longer-freezes-and-reloads-t.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-canvas-interactions-now-match-figma-rotation-aware-resizing-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-design-retries-stalled-screen-edits-with-a-smaller-safer-fol.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-inspector-fixes-mixed-selections-show-mixed-instead-of-wrong.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-keyframe-timeline-works-reliably-drag-keyframes-smoothly-pic.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-layers-panel-matches-figma-top-layer-on-top-drag-with-auto-s.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-much-faster-editor-smooth-dragging-zooming-and-panel-updates.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-pen-tool-refinements-click-the-first-point-to-close-with-a-d.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-text-editing-enter-adds-a-line-break-international-ime-typin.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-undo-and-redo-are-dependable-across-agent-edits-screen-switc.md +6 -0
- package/corpus/templates/design/server/lib/design-export.ts +43 -1
- package/corpus/templates/design/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/design/shared/board-file.ts +25 -5
- package/corpus/templates/design/shared/canvas-math.ts +754 -9
- package/corpus/templates/design/shared/code-layer.ts +304 -26
- package/corpus/templates/design/shared/color-utils.ts +84 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +16 -7
- package/corpus/templates/design/shared/motion-compiler.ts +75 -31
- package/corpus/templates/design/shared/motion-timeline.ts +335 -0
- package/corpus/templates/design/shared/pen-path.ts +200 -23
- package/dist/client/AssistantChat.js +1 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/use-pinch-zoom.d.ts.map +1 -1
- package/dist/client/use-pinch-zoom.js +76 -11
- package/dist/client/use-pinch-zoom.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/presence.d.ts.map +1 -1
- package/dist/collab/presence.js +54 -3
- package/dist/collab/presence.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/observability/routes.d.ts +2 -2
- package/dist/secrets/routes.d.ts +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion-preview bridge — injected into every canvas iframe.
|
|
3
|
+
*
|
|
4
|
+
* Allows the MotionDock's scrubbing preview to animate elements in ALL editor
|
|
5
|
+
* modes without writing anything to the DB, Yjs state, or source files.
|
|
6
|
+
*
|
|
7
|
+
* Protocol (parent → iframe):
|
|
8
|
+
*
|
|
9
|
+
* { type: 'motion-load-tracks', tracks: Array<{
|
|
10
|
+
* targetNodeId: string,
|
|
11
|
+
* property: string,
|
|
12
|
+
* keyframes: Array<{ t: number, value: string, ease?: string }>
|
|
13
|
+
* }>, defaultEase?: string }
|
|
14
|
+
* Load (or replace) the track list. t ∈ [0, 1]. defaultEase is the
|
|
15
|
+
* timeline-level fallback easing applied to any keyframe that omits its
|
|
16
|
+
* own `ease` (mirrors the compiled stylesheet); defaults to "ease".
|
|
17
|
+
*
|
|
18
|
+
* { type: 'motion-preview', t, durationMs }
|
|
19
|
+
* Seek all loaded tracks to normalised position t ∈ [0, 1] and apply
|
|
20
|
+
* interpolated CSS property values as inline styles on the matching
|
|
21
|
+
* [data-agent-native-node-id="…"] elements. Never writes to storage.
|
|
22
|
+
*
|
|
23
|
+
* { type: 'motion-preview-clear' }
|
|
24
|
+
* Remove all motion-preview inline-style overrides and the in-memory
|
|
25
|
+
* track list. Called when the dock is closed or the timeline is discarded.
|
|
26
|
+
*
|
|
27
|
+
* Rules:
|
|
28
|
+
* • No import/require of any module (DOM globals only).
|
|
29
|
+
* • No references to outer/module scope (the code runs inside an iframe).
|
|
30
|
+
* • Wrap everything in a self-executing IIFE.
|
|
31
|
+
*/
|
|
32
|
+
(function () {
|
|
33
|
+
// Track list loaded by 'motion-load-tracks'.
|
|
34
|
+
var loadedTracks: Array<{
|
|
35
|
+
targetNodeId: string;
|
|
36
|
+
property: string;
|
|
37
|
+
keyframes: Array<{ t: number; value: string; ease?: string }>;
|
|
38
|
+
}> = [];
|
|
39
|
+
// Timeline-level default easing sent alongside the tracks. Applied to any
|
|
40
|
+
// keyframe that omits its own `ease`, matching the compiled stylesheet's
|
|
41
|
+
// defaultEase fallback so scrub/playback preview equals the persisted CSS.
|
|
42
|
+
// Falls back to "ease" when the loader doesn't provide one.
|
|
43
|
+
var loadedDefaultEase = "ease";
|
|
44
|
+
// Map of nodeId -> [property, ...] we have touched, for cleanup.
|
|
45
|
+
var touchedProps: Record<string, string[]> = {};
|
|
46
|
+
// Map of nodeId -> property -> original inline style value.
|
|
47
|
+
var originalInlineValues: Record<string, Record<string, string>> = {};
|
|
48
|
+
|
|
49
|
+
function camelizeProp(prop: string): string {
|
|
50
|
+
return String(prop).replace(/-([a-z])/g, function (_m: string, c: string) {
|
|
51
|
+
return c.toUpperCase();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatNum(n: number): string {
|
|
56
|
+
if (!Number.isFinite(n)) return "0";
|
|
57
|
+
return (Math.round(n * 10000) / 10000).toString();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function clamp255(x: number): number {
|
|
61
|
+
return x < 0 ? 0 : x > 255 ? 255 : x;
|
|
62
|
+
}
|
|
63
|
+
function clamp01(x: number): number {
|
|
64
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseAlpha(s: string): number {
|
|
68
|
+
if (typeof s === "string" && s.indexOf("%") >= 0)
|
|
69
|
+
return parseFloat(s) / 100;
|
|
70
|
+
return parseFloat(s);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hueToRgb(p: number, q: number, h: number): number {
|
|
74
|
+
if (h < 0) h += 1;
|
|
75
|
+
if (h > 1) h -= 1;
|
|
76
|
+
if (h < 1 / 6) return p + (q - p) * 6 * h;
|
|
77
|
+
if (h < 1 / 2) return q;
|
|
78
|
+
if (h < 2 / 3) return p + (q - p) * (2 / 3 - h) * 6;
|
|
79
|
+
return p;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|
83
|
+
h = (((h % 360) + 360) % 360) / 360;
|
|
84
|
+
s = clamp01(s);
|
|
85
|
+
l = clamp01(l);
|
|
86
|
+
if (s === 0) return [l * 255, l * 255, l * 255];
|
|
87
|
+
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
88
|
+
var p = 2 * l - q;
|
|
89
|
+
return [
|
|
90
|
+
hueToRgb(p, q, h + 1 / 3) * 255,
|
|
91
|
+
hueToRgb(p, q, h) * 255,
|
|
92
|
+
hueToRgb(p, q, h - 1 / 3) * 255,
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Parse a CSS color (hex, rgb/rgba, hsl/hsla) into [r, g, b, a] or null.
|
|
97
|
+
function parseColor(str: string): [number, number, number, number] | null {
|
|
98
|
+
if (typeof str !== "string") return null;
|
|
99
|
+
var s = str.trim();
|
|
100
|
+
var hex = /^#([0-9a-fA-F]{3,8})$/.exec(s);
|
|
101
|
+
if (hex) {
|
|
102
|
+
var h = hex[1];
|
|
103
|
+
if (h.length === 3 || h.length === 4) {
|
|
104
|
+
return [
|
|
105
|
+
parseInt(h.charAt(0) + h.charAt(0), 16),
|
|
106
|
+
parseInt(h.charAt(1) + h.charAt(1), 16),
|
|
107
|
+
parseInt(h.charAt(2) + h.charAt(2), 16),
|
|
108
|
+
h.length === 4 ? parseInt(h.charAt(3) + h.charAt(3), 16) / 255 : 1,
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
if (h.length === 6 || h.length === 8) {
|
|
112
|
+
return [
|
|
113
|
+
parseInt(h.slice(0, 2), 16),
|
|
114
|
+
parseInt(h.slice(2, 4), 16),
|
|
115
|
+
parseInt(h.slice(4, 6), 16),
|
|
116
|
+
h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1,
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
var rgb = /^rgba?\(([^)]+)\)$/i.exec(s);
|
|
122
|
+
if (rgb) {
|
|
123
|
+
var rp = rgb[1].split(/[\s,\/]+/).filter(function (x: string) {
|
|
124
|
+
return x.length;
|
|
125
|
+
});
|
|
126
|
+
if (rp.length >= 3) {
|
|
127
|
+
return [
|
|
128
|
+
parseFloat(rp[0]),
|
|
129
|
+
parseFloat(rp[1]),
|
|
130
|
+
parseFloat(rp[2]),
|
|
131
|
+
rp.length >= 4 ? parseAlpha(rp[3]) : 1,
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
var hsl = /^hsla?\(([^)]+)\)$/i.exec(s);
|
|
137
|
+
if (hsl) {
|
|
138
|
+
var hp = hsl[1].split(/[\s,\/]+/).filter(function (x: string) {
|
|
139
|
+
return x.length;
|
|
140
|
+
});
|
|
141
|
+
if (hp.length >= 3) {
|
|
142
|
+
var c = hslToRgb(
|
|
143
|
+
parseFloat(hp[0]),
|
|
144
|
+
parseFloat(hp[1]) / 100,
|
|
145
|
+
parseFloat(hp[2]) / 100,
|
|
146
|
+
);
|
|
147
|
+
return [c[0], c[1], c[2], hp.length >= 4 ? parseAlpha(hp[3]) : 1];
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function lerpColorArr(
|
|
155
|
+
a: [number, number, number, number],
|
|
156
|
+
b: [number, number, number, number],
|
|
157
|
+
ratio: number,
|
|
158
|
+
): [number, number, number, number] {
|
|
159
|
+
return [
|
|
160
|
+
a[0] + (b[0] - a[0]) * ratio,
|
|
161
|
+
a[1] + (b[1] - a[1]) * ratio,
|
|
162
|
+
a[2] + (b[2] - a[2]) * ratio,
|
|
163
|
+
a[3] + (b[3] - a[3]) * ratio,
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function formatColor(c: [number, number, number, number]): string {
|
|
168
|
+
var r = Math.round(clamp255(c[0]));
|
|
169
|
+
var g = Math.round(clamp255(c[1]));
|
|
170
|
+
var b = Math.round(clamp255(c[2]));
|
|
171
|
+
var a = clamp01(c[3]);
|
|
172
|
+
if (a >= 1) return "rgb(" + r + ", " + g + ", " + b + ")";
|
|
173
|
+
return (
|
|
174
|
+
"rgba(" +
|
|
175
|
+
r +
|
|
176
|
+
", " +
|
|
177
|
+
g +
|
|
178
|
+
", " +
|
|
179
|
+
b +
|
|
180
|
+
", " +
|
|
181
|
+
Math.round(a * 1000) / 1000 +
|
|
182
|
+
")"
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
type Segment =
|
|
187
|
+
| { lit: string }
|
|
188
|
+
| { type: "color"; value: [number, number, number, number] }
|
|
189
|
+
| { type: "num"; value: number; unit: string };
|
|
190
|
+
|
|
191
|
+
// Split a value into literal + typed (number / color) segments so two values
|
|
192
|
+
// that share a skeleton (e.g. 'translateY(16px)' / 'translateY(0px)') can be
|
|
193
|
+
// interpolated component-wise instead of snapping at the midpoint.
|
|
194
|
+
function tokenizeSegments(str: string): Segment[] | null {
|
|
195
|
+
if (typeof str !== "string") return null;
|
|
196
|
+
var segs: Segment[] = [];
|
|
197
|
+
var i = 0;
|
|
198
|
+
var n = str.length;
|
|
199
|
+
var litStart = 0;
|
|
200
|
+
var colorRe = /^(#[0-9a-fA-F]{3,8}|rgba?\([^)]*\)|hsla?\([^)]*\))/i;
|
|
201
|
+
var numRe = /^[+-]?(?:\d+\.?\d*|\.\d+)/;
|
|
202
|
+
while (i < n) {
|
|
203
|
+
var rest = str.slice(i);
|
|
204
|
+
var cm = colorRe.exec(rest);
|
|
205
|
+
if (cm) {
|
|
206
|
+
var parsed = parseColor(cm[0]);
|
|
207
|
+
if (parsed) {
|
|
208
|
+
if (i > litStart) segs.push({ lit: str.slice(litStart, i) });
|
|
209
|
+
segs.push({ type: "color", value: parsed });
|
|
210
|
+
i += cm[0].length;
|
|
211
|
+
litStart = i;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Skip a number when the previous char is a letter — it belongs to an
|
|
216
|
+
// identifier such as translate3d / matrix3d, not a numeric argument.
|
|
217
|
+
var prevCh = i > 0 ? str.charAt(i - 1) : "";
|
|
218
|
+
if (!/[a-zA-Z]/.test(prevCh)) {
|
|
219
|
+
var nm = numRe.exec(rest);
|
|
220
|
+
if (nm) {
|
|
221
|
+
var numText = nm[0];
|
|
222
|
+
var unit = "";
|
|
223
|
+
var um = /^[a-z%]+/i.exec(str.slice(i + numText.length));
|
|
224
|
+
if (um) unit = um[0];
|
|
225
|
+
if (i > litStart) segs.push({ lit: str.slice(litStart, i) });
|
|
226
|
+
segs.push({ type: "num", value: parseFloat(numText), unit: unit });
|
|
227
|
+
i += numText.length + unit.length;
|
|
228
|
+
litStart = i;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
i++;
|
|
233
|
+
}
|
|
234
|
+
if (n > litStart) segs.push({ lit: str.slice(litStart, n) });
|
|
235
|
+
return segs;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function segShape(segs: Segment[]): string {
|
|
239
|
+
var parts: string[] = [];
|
|
240
|
+
for (var i = 0; i < segs.length; i++) {
|
|
241
|
+
var s = segs[i];
|
|
242
|
+
parts.push("lit" in s ? "L" + s.lit : "T" + s.type);
|
|
243
|
+
}
|
|
244
|
+
return parts.join("\x1f");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── Easing evaluation ─────────────────────────────────────────────────────
|
|
248
|
+
// Dependency-free copy of the shared evaluateMotionEase algorithm in
|
|
249
|
+
// shared/motion-timeline.ts (this bridge cannot import modules) — keep the
|
|
250
|
+
// two in sync. Evaluates CSS timing functions so scrubbing matches the real
|
|
251
|
+
// compiled animation instead of always lerping linearly.
|
|
252
|
+
|
|
253
|
+
// Evaluate y for a CSS cubic-bezier at progress x (Newton + bisection).
|
|
254
|
+
function cubicBezierY(
|
|
255
|
+
x1: number,
|
|
256
|
+
y1: number,
|
|
257
|
+
x2: number,
|
|
258
|
+
y2: number,
|
|
259
|
+
x: number,
|
|
260
|
+
): number {
|
|
261
|
+
if (x <= 0) return 0;
|
|
262
|
+
if (x >= 1) return 1;
|
|
263
|
+
var cx = 3 * x1;
|
|
264
|
+
var bx = 3 * (x2 - x1) - cx;
|
|
265
|
+
var ax = 1 - cx - bx;
|
|
266
|
+
var cy = 3 * y1;
|
|
267
|
+
var by = 3 * (y2 - y1) - cy;
|
|
268
|
+
var ay = 1 - cy - by;
|
|
269
|
+
function sampleX(u: number): number {
|
|
270
|
+
return ((ax * u + bx) * u + cx) * u;
|
|
271
|
+
}
|
|
272
|
+
function sampleY(u: number): number {
|
|
273
|
+
return ((ay * u + by) * u + cy) * u;
|
|
274
|
+
}
|
|
275
|
+
function sampleDX(u: number): number {
|
|
276
|
+
return (3 * ax * u + 2 * bx) * u + cx;
|
|
277
|
+
}
|
|
278
|
+
var u = x;
|
|
279
|
+
for (var i = 0; i < 8; i++) {
|
|
280
|
+
var err = sampleX(u) - x;
|
|
281
|
+
if (Math.abs(err) < 1e-6) return sampleY(u);
|
|
282
|
+
var d = sampleDX(u);
|
|
283
|
+
if (Math.abs(d) < 1e-6) break;
|
|
284
|
+
u = Math.min(1, Math.max(0, u - err / d));
|
|
285
|
+
}
|
|
286
|
+
var lo = 0;
|
|
287
|
+
var hi = 1;
|
|
288
|
+
u = x;
|
|
289
|
+
while (hi - lo > 1e-6) {
|
|
290
|
+
u = (lo + hi) / 2;
|
|
291
|
+
if (sampleX(u) < x) lo = u;
|
|
292
|
+
else hi = u;
|
|
293
|
+
}
|
|
294
|
+
return sampleY(u);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Evaluate a CSS timing function (keywords, cubic-bezier, steps, spring
|
|
298
|
+
// approximation) at linear progress x ∈ [0, 1]. Result may overshoot [0, 1]
|
|
299
|
+
// for overshoot beziers (the dock's "Spring" preset).
|
|
300
|
+
function evalEase(ease: string | undefined, x: number): number {
|
|
301
|
+
var clamped = x <= 0 ? 0 : x >= 1 ? 1 : x;
|
|
302
|
+
var raw = String(ease == null ? "ease" : ease)
|
|
303
|
+
.trim()
|
|
304
|
+
.toLowerCase();
|
|
305
|
+
if (raw === "linear") return clamped;
|
|
306
|
+
if (raw === "step-start") return clamped > 0 ? 1 : 0;
|
|
307
|
+
if (raw === "step-end") return clamped >= 1 ? 1 : 0;
|
|
308
|
+
if (raw === "ease") return cubicBezierY(0.25, 0.1, 0.25, 1, clamped);
|
|
309
|
+
if (raw === "ease-in") return cubicBezierY(0.42, 0, 1, 1, clamped);
|
|
310
|
+
if (raw === "ease-out") return cubicBezierY(0, 0, 0.58, 1, clamped);
|
|
311
|
+
if (raw === "ease-in-out") return cubicBezierY(0.42, 0, 0.58, 1, clamped);
|
|
312
|
+
var bezier = /^cubic-bezier\(([^)]+)\)$/.exec(raw);
|
|
313
|
+
if (bezier) {
|
|
314
|
+
var parts = bezier[1].split(",");
|
|
315
|
+
if (parts.length === 4) {
|
|
316
|
+
var p0 = parseFloat(parts[0]);
|
|
317
|
+
var p1 = parseFloat(parts[1]);
|
|
318
|
+
var p2 = parseFloat(parts[2]);
|
|
319
|
+
var p3 = parseFloat(parts[3]);
|
|
320
|
+
if (
|
|
321
|
+
Number.isFinite(p0) &&
|
|
322
|
+
Number.isFinite(p1) &&
|
|
323
|
+
Number.isFinite(p2) &&
|
|
324
|
+
Number.isFinite(p3)
|
|
325
|
+
) {
|
|
326
|
+
// x control points must stay in [0, 1]; y may overshoot.
|
|
327
|
+
return cubicBezierY(
|
|
328
|
+
Math.min(1, Math.max(0, p0)),
|
|
329
|
+
p1,
|
|
330
|
+
Math.min(1, Math.max(0, p2)),
|
|
331
|
+
p3,
|
|
332
|
+
clamped,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
var steps = /^steps\(([^)]+)\)$/.exec(raw);
|
|
338
|
+
if (steps) {
|
|
339
|
+
var stepArgs = steps[1].split(",");
|
|
340
|
+
var count = parseInt(stepArgs[0], 10);
|
|
341
|
+
if (Number.isFinite(count) && count > 0) {
|
|
342
|
+
if (clamped >= 1) return 1;
|
|
343
|
+
var pos = (stepArgs[1] || "").trim();
|
|
344
|
+
var jumpStart = pos === "start" || pos === "jump-start";
|
|
345
|
+
return Math.min(
|
|
346
|
+
1,
|
|
347
|
+
(Math.floor(clamped * count) + (jumpStart ? 1 : 0)) / count,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Spring approximation: gentle overshoot bezier.
|
|
352
|
+
if (raw.indexOf("spring") === 0) {
|
|
353
|
+
return cubicBezierY(0.34, 1.56, 0.64, 1, clamped);
|
|
354
|
+
}
|
|
355
|
+
return clamped;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Build the identity counterpart of a transform/filter list so a
|
|
359
|
+
// `none` endpoint interpolates smoothly instead of midpoint-snapping
|
|
360
|
+
// (e.g. none -> translateY(16px) lerps from translateY(0px)).
|
|
361
|
+
function identityForFunctions(value: string): string | null {
|
|
362
|
+
var fnRe = /([a-zA-Z][a-zA-Z0-9]*)\(([^)]*)\)/g;
|
|
363
|
+
var out = "";
|
|
364
|
+
var lastIndex = 0;
|
|
365
|
+
var matched = false;
|
|
366
|
+
var m: RegExpExecArray | null;
|
|
367
|
+
while ((m = fnRe.exec(value)) !== null) {
|
|
368
|
+
// Only whitespace may sit between functions in a transform/filter list.
|
|
369
|
+
if (value.slice(lastIndex, m.index).trim() !== "") return null;
|
|
370
|
+
var fn = m[1].toLowerCase();
|
|
371
|
+
var args = m[2];
|
|
372
|
+
var identity: string | null = null;
|
|
373
|
+
if (/^translate/.test(fn) || /^skew/.test(fn)) {
|
|
374
|
+
identity = args.replace(/[+-]?(?:\d+\.?\d*|\.\d+)([a-z%]*)/gi, "0$1");
|
|
375
|
+
} else if (/^scale/.test(fn)) {
|
|
376
|
+
identity = args.replace(/[+-]?(?:\d+\.?\d*|\.\d+)/g, "1");
|
|
377
|
+
} else if (/^rotate/.test(fn) || fn === "hue-rotate") {
|
|
378
|
+
identity = args.replace(/[+-]?(?:\d+\.?\d*|\.\d+)([a-z%]*)/gi, "0$1");
|
|
379
|
+
} else if (fn === "matrix") {
|
|
380
|
+
identity = "1, 0, 0, 1, 0, 0";
|
|
381
|
+
} else if (
|
|
382
|
+
fn === "blur" ||
|
|
383
|
+
fn === "grayscale" ||
|
|
384
|
+
fn === "sepia" ||
|
|
385
|
+
fn === "invert"
|
|
386
|
+
) {
|
|
387
|
+
identity = args.replace(/[+-]?(?:\d+\.?\d*|\.\d+)([a-z%]*)/gi, "0$1");
|
|
388
|
+
} else if (
|
|
389
|
+
fn === "brightness" ||
|
|
390
|
+
fn === "contrast" ||
|
|
391
|
+
fn === "saturate" ||
|
|
392
|
+
fn === "opacity"
|
|
393
|
+
) {
|
|
394
|
+
identity = args.indexOf("%") >= 0 ? "100%" : "1";
|
|
395
|
+
} else {
|
|
396
|
+
// matrix3d, perspective, drop-shadow, unknown functions: bail out.
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
out += (out ? " " : "") + m[1] + "(" + identity + ")";
|
|
400
|
+
lastIndex = m.index + m[0].length;
|
|
401
|
+
matched = true;
|
|
402
|
+
}
|
|
403
|
+
if (!matched) return null;
|
|
404
|
+
if (value.slice(lastIndex).trim() !== "") return null;
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Interpolate two keyframe values. Handles plain numbers (with units), CSS
|
|
409
|
+
// function values (translate/scale/blur/…), numbers embedded in gradients,
|
|
410
|
+
// and colors (hex / rgb(a) / hsl(a)). Falls back to a midpoint snap only for
|
|
411
|
+
// genuinely non-interpolable keywords (e.g. none -> block).
|
|
412
|
+
function lerp(a: string, b: string, ratio: number): string {
|
|
413
|
+
a = a == null ? "" : String(a);
|
|
414
|
+
b = b == null ? "" : String(b);
|
|
415
|
+
if (a === b) return a;
|
|
416
|
+
// Map a `none` endpoint to the identity form of the other endpoint so
|
|
417
|
+
// transform/filter tracks seeded from a `none` base animate smoothly.
|
|
418
|
+
if (a.trim() === "none") {
|
|
419
|
+
var identA = identityForFunctions(b);
|
|
420
|
+
if (identA) a = identA;
|
|
421
|
+
} else if (b.trim() === "none") {
|
|
422
|
+
var identB = identityForFunctions(a);
|
|
423
|
+
if (identB) b = identB;
|
|
424
|
+
}
|
|
425
|
+
if (a === b) return a;
|
|
426
|
+
var ca = parseColor(a);
|
|
427
|
+
var cb = parseColor(b);
|
|
428
|
+
if (ca && cb) return formatColor(lerpColorArr(ca, cb, ratio));
|
|
429
|
+
var aSegs = tokenizeSegments(a);
|
|
430
|
+
var bSegs = tokenizeSegments(b);
|
|
431
|
+
if (
|
|
432
|
+
aSegs &&
|
|
433
|
+
bSegs &&
|
|
434
|
+
aSegs.length === bSegs.length &&
|
|
435
|
+
segShape(aSegs) === segShape(bSegs)
|
|
436
|
+
) {
|
|
437
|
+
var out = "";
|
|
438
|
+
var ok = true;
|
|
439
|
+
var touched = false;
|
|
440
|
+
for (var k = 0; k < aSegs.length; k++) {
|
|
441
|
+
var seg = aSegs[k];
|
|
442
|
+
if ("lit" in seg) {
|
|
443
|
+
out += seg.lit;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
var other = bSegs[k];
|
|
447
|
+
if (!other || !("type" in other) || other.type !== seg.type) {
|
|
448
|
+
ok = false;
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
touched = true;
|
|
452
|
+
if (seg.type === "color" && "value" in seg && "value" in other) {
|
|
453
|
+
out += formatColor(
|
|
454
|
+
lerpColorArr(
|
|
455
|
+
seg.value as [number, number, number, number],
|
|
456
|
+
other.value as [number, number, number, number],
|
|
457
|
+
ratio,
|
|
458
|
+
),
|
|
459
|
+
);
|
|
460
|
+
} else if ("value" in seg && "value" in other) {
|
|
461
|
+
var unit =
|
|
462
|
+
("unit" in seg ? seg.unit : "") ||
|
|
463
|
+
("unit" in other ? other.unit : "") ||
|
|
464
|
+
"";
|
|
465
|
+
out +=
|
|
466
|
+
formatNum(
|
|
467
|
+
(seg.value as number) +
|
|
468
|
+
((other.value as number) - (seg.value as number)) * ratio,
|
|
469
|
+
) + unit;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (ok && touched) return out;
|
|
473
|
+
}
|
|
474
|
+
// Non-interpolable (keywords, mismatched shapes): snap at the midpoint.
|
|
475
|
+
return ratio < 0.5 ? a : b;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function interpolate(
|
|
479
|
+
keyframes: Array<{ t: number; value: string; ease?: string }>,
|
|
480
|
+
t: number,
|
|
481
|
+
): string {
|
|
482
|
+
if (!keyframes || keyframes.length === 0) return "";
|
|
483
|
+
if (keyframes.length === 1) return keyframes[0].value;
|
|
484
|
+
// Find surrounding keyframes.
|
|
485
|
+
var prev = keyframes[0];
|
|
486
|
+
var next = keyframes[keyframes.length - 1];
|
|
487
|
+
for (var i = 0; i < keyframes.length - 1; i++) {
|
|
488
|
+
if (t >= keyframes[i].t && t <= keyframes[i + 1].t) {
|
|
489
|
+
prev = keyframes[i];
|
|
490
|
+
next = keyframes[i + 1];
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
var span = next.t - prev.t;
|
|
495
|
+
if (span <= 0) return prev.value;
|
|
496
|
+
var ratio = Math.max(0, Math.min(1, (t - prev.t) / span));
|
|
497
|
+
// Per-keyframe ease shapes the interval to the NEXT keyframe (standard
|
|
498
|
+
// CSS keyframe semantics). A keyframe that omits its own ease falls back to
|
|
499
|
+
// the timeline's defaultEase (loadedDefaultEase) so scrub/playback preview
|
|
500
|
+
// matches the compiled stylesheet, which uses that same fallback when it
|
|
501
|
+
// emits animation-timing-function. Overshoot beziers may push the eased
|
|
502
|
+
// ratio outside [0, 1] on purpose — lerp extrapolates.
|
|
503
|
+
var kfEase = prev.ease == null ? loadedDefaultEase : prev.ease;
|
|
504
|
+
var eased = evalEase(kfEase, ratio);
|
|
505
|
+
return lerp(prev.value, next.value, eased);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function applyPreview(t: number): void {
|
|
509
|
+
for (var i = 0; i < loadedTracks.length; i++) {
|
|
510
|
+
var track = loadedTracks[i];
|
|
511
|
+
var el = document.querySelector(
|
|
512
|
+
'[data-agent-native-node-id="' + track.targetNodeId + '"]',
|
|
513
|
+
) as HTMLElement | null;
|
|
514
|
+
if (!el) continue;
|
|
515
|
+
var value = interpolate(track.keyframes, t);
|
|
516
|
+
if (value === "") continue;
|
|
517
|
+
// Normalize kebab properties (e.g. background-color) to the camelCase
|
|
518
|
+
// CSSOM accessor; el.style['background-color'] = … is unreliable.
|
|
519
|
+
var prop = camelizeProp(track.property);
|
|
520
|
+
if (!originalInlineValues[track.targetNodeId])
|
|
521
|
+
originalInlineValues[track.targetNodeId] = {};
|
|
522
|
+
if (!(prop in originalInlineValues[track.targetNodeId])) {
|
|
523
|
+
originalInlineValues[track.targetNodeId][prop] =
|
|
524
|
+
(el.style as unknown as Record<string, string>)[prop] || "";
|
|
525
|
+
}
|
|
526
|
+
(el.style as unknown as Record<string, string>)[prop] = value;
|
|
527
|
+
if (!touchedProps[track.targetNodeId])
|
|
528
|
+
touchedProps[track.targetNodeId] = [];
|
|
529
|
+
if (touchedProps[track.targetNodeId].indexOf(prop) === -1) {
|
|
530
|
+
touchedProps[track.targetNodeId].push(prop);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function clearPreview(): void {
|
|
536
|
+
var nodeIds = Object.keys(touchedProps);
|
|
537
|
+
for (var i = 0; i < nodeIds.length; i++) {
|
|
538
|
+
var el = document.querySelector(
|
|
539
|
+
'[data-agent-native-node-id="' + nodeIds[i] + '"]',
|
|
540
|
+
) as HTMLElement | null;
|
|
541
|
+
if (!el) continue;
|
|
542
|
+
var props = touchedProps[nodeIds[i]];
|
|
543
|
+
for (var j = 0; j < props.length; j++) {
|
|
544
|
+
var originals = originalInlineValues[nodeIds[i]] || {};
|
|
545
|
+
(el.style as unknown as Record<string, string>)[props[j]] =
|
|
546
|
+
Object.prototype.hasOwnProperty.call(originals, props[j])
|
|
547
|
+
? originals[props[j]]
|
|
548
|
+
: "";
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
touchedProps = {};
|
|
552
|
+
originalInlineValues = {};
|
|
553
|
+
loadedTracks = [];
|
|
554
|
+
loadedDefaultEase = "ease";
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Last previewed playhead position, so reloading the track list (which
|
|
558
|
+
// resets touched styles to base) can immediately re-apply the current
|
|
559
|
+
// scrub position instead of leaving the canvas at t=0 while the dock's
|
|
560
|
+
// playhead still shows the old time.
|
|
561
|
+
var lastPreviewT: number | null = null;
|
|
562
|
+
|
|
563
|
+
window.addEventListener("message", function (e: MessageEvent) {
|
|
564
|
+
if (e.source !== window.parent) return;
|
|
565
|
+
if (!e.data || typeof e.data.type !== "string") return;
|
|
566
|
+
if (e.data.type === "motion-load-tracks") {
|
|
567
|
+
clearPreview();
|
|
568
|
+
loadedTracks = Array.isArray(e.data.tracks) ? e.data.tracks : [];
|
|
569
|
+
// Timeline-level default easing for keyframes that omit their own ease.
|
|
570
|
+
// Read AFTER clearPreview() (which resets it to "ease") so a non-default
|
|
571
|
+
// defaultEase from API/agent-created timelines drives the preview.
|
|
572
|
+
loadedDefaultEase =
|
|
573
|
+
typeof e.data.defaultEase === "string" && e.data.defaultEase
|
|
574
|
+
? e.data.defaultEase
|
|
575
|
+
: "ease";
|
|
576
|
+
// Defensive: interpolate() scans keyframe pairs in array order, so an
|
|
577
|
+
// unsorted track (e.g. mid-drag ordering) would fail the window test.
|
|
578
|
+
for (var i = 0; i < loadedTracks.length; i++) {
|
|
579
|
+
var kfs = loadedTracks[i] && loadedTracks[i].keyframes;
|
|
580
|
+
if (Array.isArray(kfs)) {
|
|
581
|
+
kfs.sort(function (a, b) {
|
|
582
|
+
return (a && a.t) - (b && b.t);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (lastPreviewT !== null && loadedTracks.length > 0) {
|
|
587
|
+
applyPreview(lastPreviewT);
|
|
588
|
+
}
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (e.data.type === "motion-preview") {
|
|
592
|
+
var t = Number(e.data.t);
|
|
593
|
+
if (!Number.isFinite(t)) return;
|
|
594
|
+
t = Math.max(0, Math.min(1, t));
|
|
595
|
+
lastPreviewT = t;
|
|
596
|
+
applyPreview(t);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (e.data.type === "motion-preview-clear") {
|
|
600
|
+
clearPreview();
|
|
601
|
+
lastPreviewT = null;
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
})();
|