@hyperframes/studio 0.6.88 → 0.6.89
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/assets/index-2SbRRd33.js +146 -0
- package/dist/assets/index-D2NkPomd.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +33 -193
- package/src/components/StudioLeftSidebar.tsx +6 -0
- package/src/components/StudioRightPanel.tsx +8 -0
- package/src/components/TimelineToolbar.tsx +54 -31
- package/src/components/editor/AnimationCard.tsx +15 -3
- package/src/components/editor/DomEditOverlay.test.ts +34 -1
- package/src/components/editor/FileTree.tsx +5 -1
- package/src/components/editor/FileTreeNodes.tsx +17 -3
- package/src/components/editor/LayersPanel.tsx +19 -4
- package/src/components/editor/PropertyPanel.tsx +82 -170
- package/src/components/editor/domEditOverlayStartGesture.ts +1 -0
- package/src/components/editor/gsapAnimatesProperty.ts +52 -0
- package/src/components/editor/manualEditsDom.ts +11 -57
- package/src/components/editor/manualOffsetDrag.test.ts +18 -1
- package/src/components/editor/manualOffsetDrag.ts +16 -10
- package/src/components/editor/propertyPanel3dTransform.tsx +133 -0
- package/src/components/editor/propertyPanelHelpers.ts +76 -0
- package/src/components/editor/propertyPanelStyleSections.tsx +1 -9
- package/src/components/editor/useDomEditOverlayGestures.ts +3 -0
- package/src/components/editor/useLayerDrag.ts +6 -3
- package/src/components/renders/RenderQueueItem.tsx +47 -46
- package/src/components/sidebar/CompositionsTab.tsx +15 -2
- package/src/components/sidebar/LeftSidebar.tsx +11 -0
- package/src/hooks/gsapDragCommit.ts +294 -0
- package/src/hooks/gsapKeyframeCacheHelpers.ts +88 -0
- package/src/hooks/gsapRuntimeBridge.ts +49 -402
- package/src/hooks/gsapRuntimeReaders.ts +201 -0
- package/src/hooks/timelineEditingHelpers.ts +148 -0
- package/src/hooks/useAnimatedPropertyCommit.ts +54 -12
- package/src/hooks/useBlockHandlers.ts +150 -0
- package/src/hooks/useClipboard.ts +1 -10
- package/src/hooks/useDomEditPreviewSync.ts +126 -0
- package/src/hooks/useDomEditSession.ts +11 -79
- package/src/hooks/useGestureCommit.ts +166 -0
- package/src/hooks/useGestureRecording.ts +271 -169
- package/src/hooks/useGsapScriptCommits.ts +7 -80
- package/src/hooks/useLintModal.ts +97 -25
- package/src/hooks/useTimelineEditing.ts +10 -132
- package/src/player/components/TimelineCanvas.tsx +24 -7
- package/src/player/components/useTimelinePlayhead.ts +2 -1
- package/src/player/store/playerStore.ts +12 -0
- package/src/utils/gsapSoftReload.ts +18 -1
- package/src/utils/studioUrlState.test.ts +9 -0
- package/dist/assets/index-B9_ctmee.js +0 -143
- package/dist/assets/index-CGlIm_-E.css +0 -1
|
@@ -54,6 +54,8 @@ interface LeftSidebarProps {
|
|
|
54
54
|
isRendering?: boolean;
|
|
55
55
|
onLint?: () => void;
|
|
56
56
|
linting?: boolean;
|
|
57
|
+
lintFindingCount?: number;
|
|
58
|
+
lintFindingsByFile?: Map<string, { count: number; messages: string[] }>;
|
|
57
59
|
onToggleCollapse?: () => void;
|
|
58
60
|
onAddBlock?: (blockName: string) => void;
|
|
59
61
|
onPreviewBlock?: (preview: BlockPreviewInfo | null) => void;
|
|
@@ -84,6 +86,8 @@ export const LeftSidebar = memo(
|
|
|
84
86
|
isRendering,
|
|
85
87
|
onLint,
|
|
86
88
|
linting,
|
|
89
|
+
lintFindingCount,
|
|
90
|
+
lintFindingsByFile,
|
|
87
91
|
onToggleCollapse,
|
|
88
92
|
onAddBlock,
|
|
89
93
|
onPreviewBlock,
|
|
@@ -216,6 +220,7 @@ export const LeftSidebar = memo(
|
|
|
216
220
|
onSelect={onSelectComposition}
|
|
217
221
|
onRenderComposition={onRenderComposition}
|
|
218
222
|
isRendering={isRendering}
|
|
223
|
+
lintFindingsByFile={lintFindingsByFile}
|
|
219
224
|
/>
|
|
220
225
|
)}
|
|
221
226
|
{tab === "assets" && (
|
|
@@ -242,6 +247,7 @@ export const LeftSidebar = memo(
|
|
|
242
247
|
onDuplicateFile={onDuplicateFile}
|
|
243
248
|
onMoveFile={onMoveFile}
|
|
244
249
|
onImportFiles={onImportFiles}
|
|
250
|
+
lintFindingsByFile={lintFindingsByFile}
|
|
245
251
|
/>
|
|
246
252
|
</div>
|
|
247
253
|
)}
|
|
@@ -279,6 +285,11 @@ export const LeftSidebar = memo(
|
|
|
279
285
|
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11" />
|
|
280
286
|
</svg>
|
|
281
287
|
{linting ? "Linting…" : "Lint"}
|
|
288
|
+
{!linting && lintFindingCount != null && lintFindingCount > 0 && (
|
|
289
|
+
<span className="ml-1 min-w-[16px] rounded-full bg-amber-500/20 px-1 text-[9px] font-bold text-amber-400">
|
|
290
|
+
{lintFindingCount}
|
|
291
|
+
</span>
|
|
292
|
+
)}
|
|
282
293
|
</button>
|
|
283
294
|
</div>
|
|
284
295
|
)}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level drag commit helpers for GSAP position mutations.
|
|
3
|
+
* Extracted from gsapRuntimeBridge.ts to keep file sizes under the 600-line limit.
|
|
4
|
+
*/
|
|
5
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
6
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
7
|
+
import { usePlayerStore } from "../player/store/playerStore";
|
|
8
|
+
import { readRuntimeKeyframes, scanAllRuntimeKeyframes } from "./gsapRuntimeKeyframes";
|
|
9
|
+
import {
|
|
10
|
+
absoluteToPercentage,
|
|
11
|
+
resolveTweenStart,
|
|
12
|
+
resolveTweenDuration,
|
|
13
|
+
} from "../utils/globalTimeCompiler";
|
|
14
|
+
import { readAllAnimatedProperties } from "./gsapRuntimeReaders";
|
|
15
|
+
|
|
16
|
+
export interface GsapDragCommitCallbacks {
|
|
17
|
+
commitMutation: (
|
|
18
|
+
selection: DomEditSelection,
|
|
19
|
+
mutation: Record<string, unknown>,
|
|
20
|
+
options: {
|
|
21
|
+
label: string;
|
|
22
|
+
coalesceKey?: string;
|
|
23
|
+
softReload?: boolean;
|
|
24
|
+
skipReload?: boolean;
|
|
25
|
+
beforeReload?: () => void;
|
|
26
|
+
},
|
|
27
|
+
) => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Percentage computation ─────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export function computeCurrentPercentage(
|
|
33
|
+
selection: DomEditSelection,
|
|
34
|
+
animation?: GsapAnimation,
|
|
35
|
+
): number {
|
|
36
|
+
const currentTime = usePlayerStore.getState().currentTime;
|
|
37
|
+
if (animation) {
|
|
38
|
+
const start = resolveTweenStart(animation);
|
|
39
|
+
const duration = resolveTweenDuration(animation);
|
|
40
|
+
if (start !== null) {
|
|
41
|
+
return absoluteToPercentage(currentTime, start, duration);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const elStart = Number.parseFloat(selection.dataAttributes?.start ?? "0") || 0;
|
|
45
|
+
const elDuration = Number.parseFloat(selection.dataAttributes?.duration ?? "1") || 1;
|
|
46
|
+
return elDuration > 0
|
|
47
|
+
? Math.max(0, Math.min(100, Math.round(((currentTime - elStart) / elDuration) * 1000) / 10))
|
|
48
|
+
: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Dynamic keyframe materialization ──────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export async function materializeIfDynamic(
|
|
54
|
+
anim: GsapAnimation,
|
|
55
|
+
iframe: HTMLIFrameElement | null,
|
|
56
|
+
commitMutation: GsapDragCommitCallbacks["commitMutation"],
|
|
57
|
+
selection: DomEditSelection,
|
|
58
|
+
): Promise<string | void> {
|
|
59
|
+
if (!anim.hasUnresolvedKeyframes && !anim.hasUnresolvedSelector) return;
|
|
60
|
+
|
|
61
|
+
if (anim.hasUnresolvedSelector) {
|
|
62
|
+
const allScanned = scanAllRuntimeKeyframes(iframe);
|
|
63
|
+
if (allScanned.size === 0) return;
|
|
64
|
+
const allElements = Array.from(allScanned.entries()).map(([id, data]) => ({
|
|
65
|
+
selector: `#${id}`,
|
|
66
|
+
keyframes: data.keyframes,
|
|
67
|
+
easeEach: data.easeEach,
|
|
68
|
+
}));
|
|
69
|
+
await commitMutation(
|
|
70
|
+
selection,
|
|
71
|
+
{
|
|
72
|
+
type: "materialize-keyframes",
|
|
73
|
+
animationId: anim.id,
|
|
74
|
+
keyframes: allScanned.get(selection.id ?? "")?.keyframes ?? [],
|
|
75
|
+
allElements,
|
|
76
|
+
},
|
|
77
|
+
{ label: "Unroll dynamic animations", skipReload: true },
|
|
78
|
+
);
|
|
79
|
+
return `${anim.targetSelector}-to-0`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const runtime = readRuntimeKeyframes(iframe, anim.targetSelector);
|
|
83
|
+
if (!runtime || runtime.keyframes.length === 0) return;
|
|
84
|
+
await commitMutation(
|
|
85
|
+
selection,
|
|
86
|
+
{
|
|
87
|
+
type: "materialize-keyframes",
|
|
88
|
+
animationId: anim.id,
|
|
89
|
+
keyframes: runtime.keyframes,
|
|
90
|
+
easeEach: runtime.easeEach,
|
|
91
|
+
},
|
|
92
|
+
{ label: "Materialize dynamic keyframes", skipReload: true },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Extend tween ──────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extend a tween's time range to cover `targetTime`, remap all existing
|
|
100
|
+
* keyframe percentages to preserve their absolute positions, then add
|
|
101
|
+
* a new keyframe at the target time.
|
|
102
|
+
*/
|
|
103
|
+
async function extendTweenAndAddKeyframe(
|
|
104
|
+
selection: DomEditSelection,
|
|
105
|
+
anim: GsapAnimation,
|
|
106
|
+
properties: Record<string, number>,
|
|
107
|
+
targetTime: number,
|
|
108
|
+
tweenStart: number,
|
|
109
|
+
tweenDuration: number,
|
|
110
|
+
callbacks: GsapDragCommitCallbacks,
|
|
111
|
+
beforeReload?: () => void,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
const tweenEnd = tweenStart + tweenDuration;
|
|
114
|
+
const newStart = Math.min(targetTime, tweenStart);
|
|
115
|
+
const newEnd = Math.max(targetTime, tweenEnd);
|
|
116
|
+
const newDuration = Math.max(0.01, newEnd - newStart);
|
|
117
|
+
|
|
118
|
+
const existingKfs = anim.keyframes?.keyframes ?? [];
|
|
119
|
+
const remappedKfs: Array<{ percentage: number; properties: Record<string, number | string> }> =
|
|
120
|
+
[];
|
|
121
|
+
for (const kf of existingKfs) {
|
|
122
|
+
const absTime = tweenStart + (kf.percentage / 100) * tweenDuration;
|
|
123
|
+
const newPct = Math.round(((absTime - newStart) / newDuration) * 1000) / 10;
|
|
124
|
+
remappedKfs.push({ percentage: newPct, properties: { ...kf.properties } });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const targetPct = Math.round(((targetTime - newStart) / newDuration) * 1000) / 10;
|
|
128
|
+
remappedKfs.push({ percentage: targetPct, properties });
|
|
129
|
+
remappedKfs.sort((a, b) => a.percentage - b.percentage);
|
|
130
|
+
|
|
131
|
+
await callbacks.commitMutation(
|
|
132
|
+
selection,
|
|
133
|
+
{ type: "delete", animationId: anim.id },
|
|
134
|
+
{ label: "Extend tween range", skipReload: true },
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const selector = anim.targetSelector;
|
|
138
|
+
await callbacks.commitMutation(
|
|
139
|
+
selection,
|
|
140
|
+
{
|
|
141
|
+
type: "add-with-keyframes",
|
|
142
|
+
targetSelector: selector,
|
|
143
|
+
position: Math.round(newStart * 1000) / 1000,
|
|
144
|
+
duration: Math.round(newDuration * 1000) / 1000,
|
|
145
|
+
keyframes: remappedKfs,
|
|
146
|
+
},
|
|
147
|
+
{ label: `Move layer (extended keyframe)`, softReload: true, beforeReload },
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// fallow-ignore-next-line complexity
|
|
152
|
+
async function commitKeyframedPosition(
|
|
153
|
+
selection: DomEditSelection,
|
|
154
|
+
anim: GsapAnimation,
|
|
155
|
+
properties: Record<string, number>,
|
|
156
|
+
callbacks: GsapDragCommitCallbacks,
|
|
157
|
+
beforeReload?: () => void,
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
const pct = computeCurrentPercentage(selection, anim);
|
|
160
|
+
|
|
161
|
+
await callbacks.commitMutation(
|
|
162
|
+
selection,
|
|
163
|
+
{
|
|
164
|
+
type: "add-keyframe",
|
|
165
|
+
animationId: anim.id,
|
|
166
|
+
percentage: pct,
|
|
167
|
+
properties,
|
|
168
|
+
},
|
|
169
|
+
{ label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload },
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* For flat to()/set() tweens, convert to keyframes first so we can place the
|
|
175
|
+
* drag position at the current percentage.
|
|
176
|
+
*/
|
|
177
|
+
// fallow-ignore-next-line complexity
|
|
178
|
+
async function commitFlatViaKeyframes(
|
|
179
|
+
selection: DomEditSelection,
|
|
180
|
+
anim: GsapAnimation,
|
|
181
|
+
properties: Record<string, number>,
|
|
182
|
+
callbacks: GsapDragCommitCallbacks,
|
|
183
|
+
beforeReload?: () => void,
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
await callbacks.commitMutation(
|
|
186
|
+
selection,
|
|
187
|
+
{ type: "convert-to-keyframes", animationId: anim.id },
|
|
188
|
+
{ label: "Convert to keyframes for drag", skipReload: true },
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const pct = computeCurrentPercentage(selection, anim);
|
|
192
|
+
|
|
193
|
+
await callbacks.commitMutation(
|
|
194
|
+
selection,
|
|
195
|
+
{
|
|
196
|
+
type: "add-keyframe",
|
|
197
|
+
animationId: anim.id,
|
|
198
|
+
percentage: pct,
|
|
199
|
+
properties,
|
|
200
|
+
},
|
|
201
|
+
{ label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload },
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Main drag commit ──────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Compute the new GSAP position values from runtime-read positions + drag
|
|
209
|
+
* offset, then commit the mutation to the GSAP script.
|
|
210
|
+
*/
|
|
211
|
+
// fallow-ignore-next-line complexity
|
|
212
|
+
export async function commitGsapPositionFromDrag(
|
|
213
|
+
selection: DomEditSelection,
|
|
214
|
+
anim: GsapAnimation,
|
|
215
|
+
studioOffset: { x: number; y: number },
|
|
216
|
+
gsapPos: { x: number; y: number },
|
|
217
|
+
iframe: HTMLIFrameElement | null,
|
|
218
|
+
selector: string,
|
|
219
|
+
callbacks: GsapDragCommitCallbacks,
|
|
220
|
+
): Promise<void> {
|
|
221
|
+
const rotStyle = selection.element.style.getPropertyValue("--hf-studio-rotation");
|
|
222
|
+
const rotDeg = Number.parseFloat(rotStyle) || 0;
|
|
223
|
+
const rad = (-rotDeg * Math.PI) / 180;
|
|
224
|
+
const cos = Math.cos(rad);
|
|
225
|
+
const sin = Math.sin(rad);
|
|
226
|
+
const el = selection.element;
|
|
227
|
+
const origX = Number.parseFloat(el.getAttribute("data-hf-drag-initial-offset-x") ?? "") || 0;
|
|
228
|
+
const origY = Number.parseFloat(el.getAttribute("data-hf-drag-initial-offset-y") ?? "") || 0;
|
|
229
|
+
const deltaX = studioOffset.x - origX;
|
|
230
|
+
const deltaY = studioOffset.y - origY;
|
|
231
|
+
const adjX = deltaX * cos - deltaY * sin;
|
|
232
|
+
const adjY = deltaX * sin + deltaY * cos;
|
|
233
|
+
const parsedBaseX = Number.parseFloat(el.getAttribute("data-hf-drag-gsap-base-x") ?? "");
|
|
234
|
+
const parsedBaseY = Number.parseFloat(el.getAttribute("data-hf-drag-gsap-base-y") ?? "");
|
|
235
|
+
const baseGsapX = Number.isFinite(parsedBaseX) ? parsedBaseX : gsapPos.x;
|
|
236
|
+
const baseGsapY = Number.isFinite(parsedBaseY) ? parsedBaseY : gsapPos.y;
|
|
237
|
+
const newX = Math.round(baseGsapX + adjX);
|
|
238
|
+
const newY = Math.round(baseGsapY + adjY);
|
|
239
|
+
const restoreOffset = () => {
|
|
240
|
+
el.style.setProperty("--hf-studio-offset-x", `${origX}px`);
|
|
241
|
+
el.style.setProperty("--hf-studio-offset-y", `${origY}px`);
|
|
242
|
+
el.removeAttribute("data-hf-drag-initial-offset-x");
|
|
243
|
+
el.removeAttribute("data-hf-drag-initial-offset-y");
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (anim.keyframes) {
|
|
247
|
+
const newId = await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
|
|
248
|
+
const effectiveAnim = newId ? { ...anim, id: newId } : anim;
|
|
249
|
+
const runtimeProps = readAllAnimatedProperties(iframe, selector, anim);
|
|
250
|
+
|
|
251
|
+
const ct = usePlayerStore.getState().currentTime;
|
|
252
|
+
const ts = resolveTweenStart(effectiveAnim);
|
|
253
|
+
const td = resolveTweenDuration(effectiveAnim);
|
|
254
|
+
if (ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01)) {
|
|
255
|
+
await extendTweenAndAddKeyframe(
|
|
256
|
+
selection,
|
|
257
|
+
effectiveAnim,
|
|
258
|
+
{ ...runtimeProps, x: newX, y: newY },
|
|
259
|
+
ct,
|
|
260
|
+
ts,
|
|
261
|
+
td,
|
|
262
|
+
callbacks,
|
|
263
|
+
restoreOffset,
|
|
264
|
+
);
|
|
265
|
+
} else {
|
|
266
|
+
await commitKeyframedPosition(
|
|
267
|
+
selection,
|
|
268
|
+
effectiveAnim,
|
|
269
|
+
{ ...runtimeProps, x: newX, y: newY },
|
|
270
|
+
callbacks,
|
|
271
|
+
restoreOffset,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
} else if (anim.method === "from" || anim.method === "fromTo") {
|
|
275
|
+
await callbacks.commitMutation(
|
|
276
|
+
selection,
|
|
277
|
+
{
|
|
278
|
+
type: "convert-to-keyframes",
|
|
279
|
+
animationId: anim.id,
|
|
280
|
+
resolvedFromValues: { x: newX, y: newY },
|
|
281
|
+
},
|
|
282
|
+
{ label: "Move layer (keyframe rest)", softReload: true, beforeReload: restoreOffset },
|
|
283
|
+
);
|
|
284
|
+
} else {
|
|
285
|
+
const runtimeProps = readAllAnimatedProperties(iframe, selector, anim);
|
|
286
|
+
await commitFlatViaKeyframes(
|
|
287
|
+
selection,
|
|
288
|
+
anim,
|
|
289
|
+
{ ...runtimeProps, x: newX, y: newY },
|
|
290
|
+
callbacks,
|
|
291
|
+
restoreOffset,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for reading/writing the GSAP keyframe cache in the player store.
|
|
3
|
+
* Extracted from useGsapScriptCommits to keep file sizes under the 600-line limit.
|
|
4
|
+
*/
|
|
5
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
6
|
+
import { usePlayerStore, type KeyframeCacheEntry } from "../player/store/playerStore";
|
|
7
|
+
|
|
8
|
+
export function updateKeyframeCacheFromParsed(
|
|
9
|
+
animations: GsapAnimation[],
|
|
10
|
+
targetPath: string,
|
|
11
|
+
selectionId: string | undefined,
|
|
12
|
+
mutation: Record<string, unknown>,
|
|
13
|
+
): void {
|
|
14
|
+
const { setKeyframeCache, elements } = usePlayerStore.getState();
|
|
15
|
+
const idsWithKeyframes = new Set<string>();
|
|
16
|
+
const merged = new Map<string, KeyframeCacheEntry>();
|
|
17
|
+
for (const anim of animations) {
|
|
18
|
+
const id = anim.targetSelector.match(/^#([\w-]+)/)?.[1];
|
|
19
|
+
if (!id || !anim.keyframes) continue;
|
|
20
|
+
idsWithKeyframes.add(id);
|
|
21
|
+
|
|
22
|
+
// Convert tween-relative percentages to clip-relative so diamonds
|
|
23
|
+
// render at the correct position within the timeline clip.
|
|
24
|
+
const tweenPos = typeof anim.position === "number" ? anim.position : 0;
|
|
25
|
+
const tweenDur = anim.duration ?? 1;
|
|
26
|
+
const timelineEl = elements.find(
|
|
27
|
+
(el) => el.domId === id || (el.key ?? el.id) === `${targetPath}#${id}`,
|
|
28
|
+
);
|
|
29
|
+
const elStart = timelineEl?.start ?? 0;
|
|
30
|
+
const elDuration = timelineEl?.duration ?? 4;
|
|
31
|
+
const clipKeyframes = anim.keyframes.keyframes.map((kf) => {
|
|
32
|
+
const absTime = tweenPos + (kf.percentage / 100) * tweenDur;
|
|
33
|
+
const clipPct =
|
|
34
|
+
elDuration > 0 ? Math.round(((absTime - elStart) / elDuration) * 1000) / 10 : kf.percentage;
|
|
35
|
+
return { ...kf, percentage: clipPct };
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const existing = merged.get(id);
|
|
39
|
+
if (existing) {
|
|
40
|
+
const byPct = new Map<number, (typeof existing.keyframes)[0]>();
|
|
41
|
+
for (const kf of [...existing.keyframes, ...clipKeyframes]) {
|
|
42
|
+
const prev = byPct.get(kf.percentage);
|
|
43
|
+
if (prev) {
|
|
44
|
+
prev.properties = { ...prev.properties, ...kf.properties };
|
|
45
|
+
if (kf.ease) prev.ease = kf.ease;
|
|
46
|
+
} else {
|
|
47
|
+
byPct.set(kf.percentage, { ...kf, properties: { ...kf.properties } });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
existing.keyframes = Array.from(byPct.values()).sort((a, b) => a.percentage - b.percentage);
|
|
51
|
+
} else {
|
|
52
|
+
merged.set(id, { ...anim.keyframes, keyframes: clipKeyframes });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const [id, entry] of merged) {
|
|
56
|
+
setKeyframeCache(`${targetPath}#${id}`, entry);
|
|
57
|
+
setKeyframeCache(id, entry);
|
|
58
|
+
if (targetPath !== "index.html") setKeyframeCache(`index.html#${id}`, entry);
|
|
59
|
+
}
|
|
60
|
+
const targetId =
|
|
61
|
+
(mutation as { targetSelector?: string }).targetSelector?.match(/^#([\w-]+)/)?.[1] ??
|
|
62
|
+
selectionId;
|
|
63
|
+
if (targetId && !idsWithKeyframes.has(targetId)) {
|
|
64
|
+
setKeyframeCache(`${targetPath}#${targetId}`, undefined);
|
|
65
|
+
if (targetPath !== "index.html") setKeyframeCache(`index.html#${targetId}`, undefined);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildCacheKey(sourceFile: string, elementId: string): string {
|
|
70
|
+
return `${sourceFile}#${elementId}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function readKeyframeSnapshot(
|
|
74
|
+
sourceFile: string,
|
|
75
|
+
elementId: string | null | undefined,
|
|
76
|
+
): KeyframeCacheEntry | undefined {
|
|
77
|
+
if (!elementId) return undefined;
|
|
78
|
+
return usePlayerStore.getState().keyframeCache.get(buildCacheKey(sourceFile, elementId));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function writeKeyframeCache(
|
|
82
|
+
sourceFile: string,
|
|
83
|
+
elementId: string | null | undefined,
|
|
84
|
+
data: KeyframeCacheEntry | undefined,
|
|
85
|
+
): void {
|
|
86
|
+
if (!elementId) return;
|
|
87
|
+
usePlayerStore.getState().setKeyframeCache(buildCacheKey(sourceFile, elementId), data);
|
|
88
|
+
}
|