@hyperframes/studio 0.6.90 → 0.6.92
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-DSLrl2tB.js → index-CDy8BuGq.js} +24 -24
- package/dist/assets/index-CmRIkCwI.js +251 -0
- package/dist/assets/index-rm9tn9nH.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +2 -0
- package/src/components/StudioPreviewArea.tsx +54 -13
- package/src/components/TimelineToolbar.tsx +52 -35
- package/src/components/editor/DomEditOverlay.tsx +79 -0
- package/src/components/editor/PropertyPanel.tsx +19 -10
- package/src/components/editor/gsapAnimatesProperty.ts +30 -0
- package/src/components/editor/manualEditingAvailability.test.ts +12 -0
- package/src/components/editor/manualEditingAvailability.ts +16 -0
- package/src/components/editor/manualEditsDom.ts +25 -5
- package/src/components/editor/manualEditsDomPatches.test.ts +1 -0
- package/src/components/editor/manualEditsDomPatches.ts +17 -1
- package/src/components/editor/manualEditsSnapshot.ts +16 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +19 -4
- package/src/components/editor/useOffScreenIndicators.ts +197 -0
- package/src/components/nle/NLELayout.tsx +22 -32
- package/src/components/nle/TimelineEditorNotice.tsx +2 -25
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/hooks/gsapDragCommit.ts +119 -43
- package/src/hooks/gsapKeyframeCacheHelpers.ts +9 -4
- package/src/hooks/gsapRuntimeBridge.ts +266 -41
- package/src/hooks/gsapRuntimeReaders.ts +16 -2
- package/src/hooks/useAnimatedPropertyCommit.ts +11 -5
- package/src/hooks/useAppHotkeys.ts +48 -1
- package/src/hooks/useContextMenuDismiss.ts +29 -0
- package/src/hooks/useDomEditCommits.ts +7 -1
- package/src/hooks/useDomEditSession.ts +20 -4
- package/src/hooks/useEnableKeyframes.ts +3 -1
- package/src/hooks/useGestureCommit.ts +99 -13
- package/src/hooks/useGestureRecording.ts +18 -2
- package/src/hooks/useGsapScriptCommits.ts +24 -3
- package/src/hooks/useGsapSelectionHandlers.ts +19 -3
- package/src/hooks/useGsapTweenCache.ts +30 -10
- package/src/hooks/useRazorSplit.ts +298 -0
- package/src/hooks/useTimelineEditing.ts +15 -98
- package/src/player/components/ClipContextMenu.tsx +14 -25
- package/src/player/components/KeyframeDiamondContextMenu.tsx +16 -112
- package/src/player/components/PlayheadIndicator.tsx +43 -0
- package/src/player/components/Timeline.tsx +45 -38
- package/src/player/components/TimelineCanvas.tsx +29 -22
- package/src/player/components/TimelineClipDiamonds.tsx +3 -1
- package/src/player/components/timelineCallbacks.ts +44 -0
- package/src/player/components/timelineDragDrop.ts +2 -14
- package/src/player/components/useTimelineZoom.ts +18 -0
- package/src/player/store/playerStore.ts +20 -0
- package/src/utils/globalTimeCompiler.test.ts +2 -2
- package/src/utils/globalTimeCompiler.ts +2 -1
- package/src/utils/gsapSoftReload.test.ts +16 -0
- package/src/utils/gsapSoftReload.ts +43 -8
- package/src/utils/rdpSimplify.ts +3 -2
- package/src/utils/timelineElementSplit.test.ts +50 -0
- package/src/utils/timelineElementSplit.ts +32 -0
- package/dist/assets/index-BKuDHMYl.js +0 -146
- package/dist/assets/index-D2NkPomd.css +0 -1
|
@@ -11,8 +11,6 @@ import {
|
|
|
11
11
|
resolveTweenStart,
|
|
12
12
|
resolveTweenDuration,
|
|
13
13
|
} from "../utils/globalTimeCompiler";
|
|
14
|
-
import { readAllAnimatedProperties } from "./gsapRuntimeReaders";
|
|
15
|
-
|
|
16
14
|
export interface GsapDragCommitCallbacks {
|
|
17
15
|
commitMutation: (
|
|
18
16
|
selection: DomEditSelection,
|
|
@@ -25,6 +23,7 @@ export interface GsapDragCommitCallbacks {
|
|
|
25
23
|
beforeReload?: () => void;
|
|
26
24
|
},
|
|
27
25
|
) => Promise<void>;
|
|
26
|
+
fetchAnimations?: () => Promise<GsapAnimation[]>;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
// ── Percentage computation ─────────────────────────────────────────────────
|
|
@@ -114,7 +113,6 @@ async function extendTweenAndAddKeyframe(
|
|
|
114
113
|
const newStart = Math.min(targetTime, tweenStart);
|
|
115
114
|
const newEnd = Math.max(targetTime, tweenEnd);
|
|
116
115
|
const newDuration = Math.max(0.01, newEnd - newStart);
|
|
117
|
-
|
|
118
116
|
const existingKfs = anim.keyframes?.keyframes ?? [];
|
|
119
117
|
const remappedKfs: Array<{ percentage: number; properties: Record<string, number | string> }> =
|
|
120
118
|
[];
|
|
@@ -126,20 +124,15 @@ async function extendTweenAndAddKeyframe(
|
|
|
126
124
|
|
|
127
125
|
const targetPct = Math.round(((targetTime - newStart) / newDuration) * 1000) / 10;
|
|
128
126
|
remappedKfs.push({ percentage: targetPct, properties });
|
|
129
|
-
remappedKfs.sort((a, b) => a.percentage - b.percentage);
|
|
130
127
|
|
|
131
|
-
|
|
132
|
-
selection,
|
|
133
|
-
{ type: "delete", animationId: anim.id },
|
|
134
|
-
{ label: "Extend tween range", skipReload: true },
|
|
135
|
-
);
|
|
128
|
+
remappedKfs.sort((a, b) => a.percentage - b.percentage);
|
|
136
129
|
|
|
137
|
-
const selector = anim.targetSelector;
|
|
138
130
|
await callbacks.commitMutation(
|
|
139
131
|
selection,
|
|
140
132
|
{
|
|
141
|
-
type: "
|
|
142
|
-
|
|
133
|
+
type: "replace-with-keyframes",
|
|
134
|
+
animationId: anim.id,
|
|
135
|
+
targetSelector: anim.targetSelector,
|
|
143
136
|
position: Math.round(newStart * 1000) / 1000,
|
|
144
137
|
duration: Math.round(newDuration * 1000) / 1000,
|
|
145
138
|
keyframes: remappedKfs,
|
|
@@ -156,8 +149,8 @@ async function commitKeyframedPosition(
|
|
|
156
149
|
callbacks: GsapDragCommitCallbacks,
|
|
157
150
|
beforeReload?: () => void,
|
|
158
151
|
): Promise<void> {
|
|
159
|
-
const
|
|
160
|
-
|
|
152
|
+
const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
|
|
153
|
+
const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
|
|
161
154
|
await callbacks.commitMutation(
|
|
162
155
|
selection,
|
|
163
156
|
{
|
|
@@ -168,6 +161,7 @@ async function commitKeyframedPosition(
|
|
|
168
161
|
},
|
|
169
162
|
{ label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload },
|
|
170
163
|
);
|
|
164
|
+
if (activeKeyframePct != null) setActiveKeyframePct(null);
|
|
171
165
|
}
|
|
172
166
|
|
|
173
167
|
/**
|
|
@@ -182,10 +176,11 @@ async function commitFlatViaKeyframes(
|
|
|
182
176
|
callbacks: GsapDragCommitCallbacks,
|
|
183
177
|
beforeReload?: () => void,
|
|
184
178
|
): Promise<void> {
|
|
179
|
+
const coalesceKey = `gsap:convert-drag:${anim.id}`;
|
|
185
180
|
await callbacks.commitMutation(
|
|
186
181
|
selection,
|
|
187
182
|
{ type: "convert-to-keyframes", animationId: anim.id },
|
|
188
|
-
{ label: "Convert to keyframes for drag", skipReload: true },
|
|
183
|
+
{ label: "Convert to keyframes for drag", skipReload: true, coalesceKey },
|
|
189
184
|
);
|
|
190
185
|
|
|
191
186
|
const pct = computeCurrentPercentage(selection, anim);
|
|
@@ -198,7 +193,7 @@ async function commitFlatViaKeyframes(
|
|
|
198
193
|
percentage: pct,
|
|
199
194
|
properties,
|
|
200
195
|
},
|
|
201
|
-
{ label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload },
|
|
196
|
+
{ label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload, coalesceKey },
|
|
202
197
|
);
|
|
203
198
|
}
|
|
204
199
|
|
|
@@ -243,19 +238,20 @@ export async function commitGsapPositionFromDrag(
|
|
|
243
238
|
el.removeAttribute("data-hf-drag-initial-offset-y");
|
|
244
239
|
};
|
|
245
240
|
|
|
241
|
+
const ct = usePlayerStore.getState().currentTime;
|
|
246
242
|
if (anim.keyframes) {
|
|
247
243
|
const newId = await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
|
|
248
244
|
const effectiveAnim = newId ? { ...anim, id: newId } : anim;
|
|
249
|
-
const
|
|
245
|
+
const dragProps: Record<string, number> = { x: newX, y: newY };
|
|
250
246
|
|
|
251
|
-
const ct = usePlayerStore.getState().currentTime;
|
|
252
247
|
const ts = resolveTweenStart(effectiveAnim);
|
|
253
248
|
const td = resolveTweenDuration(effectiveAnim);
|
|
254
|
-
|
|
249
|
+
const outsideRange = ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
|
|
250
|
+
if (outsideRange) {
|
|
255
251
|
await extendTweenAndAddKeyframe(
|
|
256
252
|
selection,
|
|
257
253
|
effectiveAnim,
|
|
258
|
-
|
|
254
|
+
dragProps,
|
|
259
255
|
ct,
|
|
260
256
|
ts,
|
|
261
257
|
td,
|
|
@@ -263,32 +259,112 @@ export async function commitGsapPositionFromDrag(
|
|
|
263
259
|
restoreOffset,
|
|
264
260
|
);
|
|
265
261
|
} else {
|
|
266
|
-
await commitKeyframedPosition(
|
|
262
|
+
await commitKeyframedPosition(selection, effectiveAnim, dragProps, callbacks, restoreOffset);
|
|
263
|
+
}
|
|
264
|
+
} else if (anim.method === "from" || anim.method === "fromTo") {
|
|
265
|
+
const ct = usePlayerStore.getState().currentTime;
|
|
266
|
+
const ts = resolveTweenStart(anim);
|
|
267
|
+
const td = resolveTweenDuration(anim);
|
|
268
|
+
const outsideRange = ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
|
|
269
|
+
const dragProps: Record<string, number> = { x: newX, y: newY };
|
|
270
|
+
|
|
271
|
+
if (outsideRange && ts !== null) {
|
|
272
|
+
// Split the original from() tween into property groups first.
|
|
273
|
+
await callbacks.commitMutation(
|
|
267
274
|
selection,
|
|
268
|
-
|
|
269
|
-
{
|
|
270
|
-
|
|
271
|
-
|
|
275
|
+
{ type: "split-into-property-groups", animationId: anim.id },
|
|
276
|
+
{ label: "Split from() for drag", skipReload: true },
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const allAnims = callbacks.fetchAnimations ? await callbacks.fetchAnimations() : [];
|
|
280
|
+
const existingPosAnim = allAnims.find(
|
|
281
|
+
(a) => a.propertyGroup === "position" && a.targetSelector === anim.targetSelector,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
if (existingPosAnim?.keyframes) {
|
|
285
|
+
// Extend the existing position tween
|
|
286
|
+
const posTs = resolveTweenStart(existingPosAnim);
|
|
287
|
+
const posTd = resolveTweenDuration(existingPosAnim);
|
|
288
|
+
if (posTs !== null) {
|
|
289
|
+
await extendTweenAndAddKeyframe(
|
|
290
|
+
selection,
|
|
291
|
+
existingPosAnim,
|
|
292
|
+
{ x: newX, y: newY },
|
|
293
|
+
ct,
|
|
294
|
+
posTs,
|
|
295
|
+
posTd,
|
|
296
|
+
callbacks,
|
|
297
|
+
restoreOffset,
|
|
298
|
+
);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// No existing position tween — create one
|
|
304
|
+
const newStart = Math.min(ct, ts);
|
|
305
|
+
const newEnd = Math.max(ct, ts + td);
|
|
306
|
+
const newDuration = Math.max(0.01, newEnd - newStart);
|
|
307
|
+
const dragBefore = ct < ts;
|
|
308
|
+
const origStartPct = Math.round(((ts - newStart) / newDuration) * 1000) / 10;
|
|
309
|
+
const origEndPct = Math.round(((ts + td - newStart) / newDuration) * 1000) / 10;
|
|
310
|
+
|
|
311
|
+
const keyframes: Array<{ percentage: number; properties: Record<string, number | string> }> =
|
|
312
|
+
[];
|
|
313
|
+
if (dragBefore) {
|
|
314
|
+
keyframes.push({ percentage: 0, properties: { x: newX, y: newY } });
|
|
315
|
+
if (origStartPct > 0.5 && origStartPct < 99.5) {
|
|
316
|
+
keyframes.push({ percentage: origStartPct, properties: { x: 0, y: 0 } });
|
|
317
|
+
}
|
|
318
|
+
keyframes.push({ percentage: 100, properties: { x: 0, y: 0 } });
|
|
319
|
+
} else {
|
|
320
|
+
keyframes.push({ percentage: 0, properties: { x: 0, y: 0 } });
|
|
321
|
+
if (origEndPct > 0.5 && origEndPct < 99.5) {
|
|
322
|
+
keyframes.push({ percentage: origEndPct, properties: { x: 0, y: 0 } });
|
|
323
|
+
}
|
|
324
|
+
keyframes.push({ percentage: 100, properties: { x: newX, y: newY } });
|
|
325
|
+
}
|
|
326
|
+
keyframes.sort((a, b) => a.percentage - b.percentage);
|
|
327
|
+
|
|
328
|
+
await callbacks.commitMutation(
|
|
329
|
+
selection,
|
|
330
|
+
{
|
|
331
|
+
type: "add-with-keyframes",
|
|
332
|
+
targetSelector: anim.targetSelector,
|
|
333
|
+
position: Math.round(newStart * 1000) / 1000,
|
|
334
|
+
duration: Math.round(newDuration * 1000) / 1000,
|
|
335
|
+
keyframes,
|
|
336
|
+
},
|
|
337
|
+
{ label: "Move layer (from extended)", softReload: true, beforeReload: restoreOffset },
|
|
338
|
+
);
|
|
339
|
+
} else {
|
|
340
|
+
// Inside tween range: convert then add keyframe at current time
|
|
341
|
+
const coalesceKey = `gsap:convert-drag:${anim.id}`;
|
|
342
|
+
await callbacks.commitMutation(
|
|
343
|
+
selection,
|
|
344
|
+
{
|
|
345
|
+
type: "convert-to-keyframes",
|
|
346
|
+
animationId: anim.id,
|
|
347
|
+
},
|
|
348
|
+
{ label: "Convert from() for drag", skipReload: true, coalesceKey },
|
|
349
|
+
);
|
|
350
|
+
const pct = computeCurrentPercentage(selection, anim);
|
|
351
|
+
await callbacks.commitMutation(
|
|
352
|
+
selection,
|
|
353
|
+
{
|
|
354
|
+
type: "add-keyframe",
|
|
355
|
+
animationId: anim.id,
|
|
356
|
+
percentage: pct,
|
|
357
|
+
properties: dragProps,
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
label: `Move layer (keyframe ${pct}%)`,
|
|
361
|
+
softReload: true,
|
|
362
|
+
beforeReload: restoreOffset,
|
|
363
|
+
coalesceKey,
|
|
364
|
+
},
|
|
272
365
|
);
|
|
273
366
|
}
|
|
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
367
|
} else {
|
|
285
|
-
|
|
286
|
-
await commitFlatViaKeyframes(
|
|
287
|
-
selection,
|
|
288
|
-
anim,
|
|
289
|
-
{ ...runtimeProps, x: newX, y: newY },
|
|
290
|
-
callbacks,
|
|
291
|
-
restoreOffset,
|
|
292
|
-
);
|
|
368
|
+
await commitFlatViaKeyframes(selection, anim, { x: newX, y: newY }, callbacks, restoreOffset);
|
|
293
369
|
}
|
|
294
370
|
}
|
|
@@ -21,18 +21,23 @@ export function updateKeyframeCacheFromParsed(
|
|
|
21
21
|
|
|
22
22
|
// Convert tween-relative percentages to clip-relative so diamonds
|
|
23
23
|
// render at the correct position within the timeline clip.
|
|
24
|
-
const tweenPos = typeof anim.position === "number" ? anim.position : 0;
|
|
24
|
+
const tweenPos = anim.resolvedStart ?? (typeof anim.position === "number" ? anim.position : 0);
|
|
25
25
|
const tweenDur = anim.duration ?? 1;
|
|
26
26
|
const timelineEl = elements.find(
|
|
27
27
|
(el) => el.domId === id || (el.key ?? el.id) === `${targetPath}#${id}`,
|
|
28
28
|
);
|
|
29
29
|
const elStart = timelineEl?.start ?? 0;
|
|
30
|
-
const elDuration = timelineEl?.duration ??
|
|
30
|
+
const elDuration = timelineEl?.duration ?? 1;
|
|
31
31
|
const clipKeyframes = anim.keyframes.keyframes.map((kf) => {
|
|
32
32
|
const absTime = tweenPos + (kf.percentage / 100) * tweenDur;
|
|
33
33
|
const clipPct =
|
|
34
34
|
elDuration > 0 ? Math.round(((absTime - elStart) / elDuration) * 1000) / 10 : kf.percentage;
|
|
35
|
-
return {
|
|
35
|
+
return {
|
|
36
|
+
...kf,
|
|
37
|
+
percentage: clipPct,
|
|
38
|
+
tweenPercentage: kf.percentage,
|
|
39
|
+
propertyGroup: anim.propertyGroup,
|
|
40
|
+
};
|
|
36
41
|
});
|
|
37
42
|
|
|
38
43
|
const existing = merged.get(id);
|
|
@@ -66,7 +71,7 @@ export function updateKeyframeCacheFromParsed(
|
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
|
|
74
|
+
function buildCacheKey(sourceFile: string, elementId: string): string {
|
|
70
75
|
return `${sourceFile}#${elementId}`;
|
|
71
76
|
}
|
|
72
77
|
|