@hyperframes/studio 0.6.97 → 0.6.99
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/hyperframes-player-DgsMQSvV.js +418 -0
- package/dist/assets/index-B62bDCQv.css +1 -0
- package/dist/assets/{index-HveJ0MuV.js → index-C52IT_lp.js} +1 -1
- package/dist/assets/index-DOh7E1uj.js +1 -0
- package/dist/assets/index-DrwSRbsl.js +252 -0
- package/dist/index.html +2 -2
- package/package.json +7 -5
- package/src/App.tsx +182 -177
- package/src/captions/store.ts +11 -11
- package/src/components/StudioHeader.tsx +4 -4
- package/src/components/StudioLeftSidebar.tsx +2 -2
- package/src/components/StudioPreviewArea.tsx +225 -183
- package/src/components/StudioRightPanel.tsx +3 -3
- package/src/components/TimelineToolbar.tsx +25 -0
- package/src/components/editor/DomEditOverlay.tsx +2 -5
- package/src/components/editor/EaseCurveSection.tsx +2 -3
- package/src/components/editor/GestureTrailOverlay.tsx +4 -3
- package/src/components/editor/LayersPanel.tsx +3 -9
- package/src/components/editor/PropertyPanel.tsx +20 -61
- package/src/components/editor/colorValue.ts +3 -1
- package/src/components/editor/domEditOverlayGestures.ts +54 -1
- package/src/components/editor/domEditOverlayStartGesture.ts +5 -2
- package/src/components/editor/gradientValue.ts +3 -3
- package/src/components/editor/keyframeMove.test.ts +101 -0
- package/src/components/editor/keyframeMove.ts +151 -0
- package/src/components/editor/manualEditsDom.ts +0 -12
- package/src/components/editor/propertyPanelHelpers.ts +10 -38
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -5
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -6
- package/src/components/editor/propertyPanelTransformCommit.ts +129 -0
- package/src/components/editor/studioMotionOps.test.ts +1 -1
- package/src/components/editor/studioMotionOps.ts +2 -1
- package/src/components/editor/useDomEditOverlayGestures.ts +1 -46
- package/src/components/nle/NLELayout.tsx +1 -24
- package/src/components/sidebar/BlocksTab.tsx +2 -2
- package/src/contexts/DomEditContext.tsx +134 -31
- package/src/contexts/StudioContext.tsx +90 -40
- package/src/contexts/TimelineEditContext.tsx +47 -0
- package/src/hooks/domEditCommitTypes.ts +14 -0
- package/src/hooks/gsapDragCommit.ts +9 -24
- package/src/hooks/gsapKeyframeCacheHelpers.ts +2 -1
- package/src/hooks/gsapKeyframeCommit.ts +5 -15
- package/src/hooks/gsapRuntimeBridge.ts +18 -52
- package/src/hooks/gsapRuntimeKeyframes.ts +8 -57
- package/src/hooks/gsapRuntimeReaders.ts +19 -26
- package/src/hooks/gsapScriptCommitHelpers.ts +1 -11
- package/src/hooks/gsapScriptCommitTypes.ts +58 -0
- package/src/hooks/gsapShared.ts +157 -0
- package/src/hooks/timelineEditingHelpers.ts +63 -2
- package/src/hooks/useAnimatedPropertyCommit.ts +3 -25
- package/src/hooks/useAppHotkeys.ts +299 -377
- package/src/hooks/useConsoleErrorCapture.ts +33 -5
- package/src/hooks/useDomEditCommits.ts +35 -293
- package/src/hooks/useDomEditPositionPatchCommit.ts +1 -1
- package/src/hooks/useDomEditSession.ts +78 -249
- package/src/hooks/useDomEditTextCommits.ts +1 -1
- package/src/hooks/useDomEditWiring.ts +255 -0
- package/src/hooks/useDomGeometryCommits.ts +181 -0
- package/src/hooks/useDomSelection.ts +10 -27
- package/src/hooks/useEditorSave.ts +82 -0
- package/src/hooks/useElementLifecycleOps.ts +177 -0
- package/src/hooks/useEnableKeyframes.ts +10 -15
- package/src/hooks/useFileManager.ts +32 -114
- package/src/hooks/useFileTree.ts +80 -0
- package/src/hooks/useGestureCommit.ts +7 -5
- package/src/hooks/useGestureRecording.ts +1 -1
- package/src/hooks/useGsapAnimationOps.ts +122 -0
- package/src/hooks/useGsapArcPathOps.ts +61 -0
- package/src/hooks/useGsapAwareEditing.ts +242 -0
- package/src/hooks/useGsapKeyframeOps.ts +167 -0
- package/src/hooks/useGsapPropertyDebounce.ts +135 -0
- package/src/hooks/useGsapScriptCommits.ts +58 -570
- package/src/hooks/useGsapSelectionHandlers.ts +22 -9
- package/src/hooks/useGsapTweenCache.ts +35 -29
- package/src/hooks/useLintModal.ts +7 -0
- package/src/hooks/useMusicBeatAnalysis.ts +152 -0
- package/src/hooks/useRazorSplit.ts +1 -1
- package/src/hooks/useRenderClipContent.ts +46 -21
- package/src/hooks/useTimelineEditing.ts +48 -4
- package/src/player/components/AudioWaveform.tsx +29 -4
- package/src/player/components/BeatStrip.tsx +166 -0
- package/src/player/components/Timeline.tsx +39 -18
- package/src/player/components/TimelineCanvas.tsx +52 -12
- package/src/player/components/TimelineClipDiamonds.tsx +130 -20
- package/src/player/components/TimelinePropertyRows.tsx +8 -2
- package/src/player/components/TimelineRuler.tsx +36 -2
- package/src/player/components/timelineEditing.ts +30 -5
- package/src/player/components/useTimelineClipDrag.ts +155 -4
- package/src/player/components/useTimelinePlayhead.ts +30 -1
- package/src/player/hooks/useTimelinePlayer.ts +47 -45
- package/src/player/lib/mediaProbe.ts +46 -3
- package/src/player/lib/playbackScrub.ts +16 -0
- package/src/player/lib/timelineDOM.ts +10 -2
- package/src/player/lib/timelineIframeHelpers.ts +89 -0
- package/src/player/store/playerStore.ts +92 -33
- package/src/utils/beatEditActions.ts +109 -0
- package/src/utils/beatEditing.ts +136 -0
- package/src/utils/clipboardPayload.ts +3 -2
- package/src/utils/compositionPatterns.ts +2 -0
- package/src/utils/keyframeSelection.test.ts +45 -0
- package/src/utils/keyframeSelection.ts +29 -0
- package/src/utils/rounding.ts +9 -0
- package/src/utils/studioHelpers.ts +5 -2
- package/src/utils/studioUrlState.ts +2 -1
- package/src/utils/timelineAssetDrop.ts +6 -5
- package/src/utils/timelineInspector.ts +15 -100
- package/dist/assets/hyperframes-player-Daj5djxa.js +0 -418
- package/dist/assets/index-B0twsRu0.css +0 -1
- package/dist/assets/index-Cfye9xzo.js +0 -251
- package/src/components/editor/DopesheetStrip.tsx +0 -141
- package/src/components/editor/StaggerControls.tsx +0 -61
- package/src/components/editor/TimelineLayerPanel.test.ts +0 -42
- package/src/components/editor/TimelineLayerPanel.tsx +0 -15
- package/src/components/nle/TimelineEditorNotice.tsx +0 -133
- package/src/hooks/gsapRuntimePreview.ts +0 -19
- package/src/player/components/timelineUtils.ts +0 -211
- package/src/utils/audioBeatDetection.ts +0 -58
- package/src/utils/keyframeSnapping.test.ts +0 -74
- package/src/utils/keyframeSnapping.ts +0 -63
- package/src/utils/timelineInspector.test.ts +0 -79
|
@@ -3,31 +3,29 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
5
5
|
import { classifyPropertyGroup, type PropertyGroupName } from "@hyperframes/core/gsap-parser";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
getProperty: (el: Element, prop: string) => number;
|
|
9
|
-
}
|
|
6
|
+
import { getIframeGsap, queryIframeElement } from "./gsapShared";
|
|
7
|
+
import { roundTo3 } from "../utils/rounding";
|
|
10
8
|
|
|
11
9
|
export function readGsapProperty(
|
|
12
10
|
iframe: HTMLIFrameElement | null,
|
|
13
11
|
selector: string | null,
|
|
14
12
|
prop: string,
|
|
15
13
|
): number | null {
|
|
16
|
-
if (!
|
|
14
|
+
if (!selector) return null;
|
|
15
|
+
const gsap = getIframeGsap(iframe);
|
|
16
|
+
if (!gsap) return null;
|
|
17
|
+
const el = queryIframeElement(iframe, selector);
|
|
18
|
+
if (!el) return null;
|
|
17
19
|
try {
|
|
18
|
-
const gsap = (iframe.contentWindow as unknown as { gsap?: IframeGsap }).gsap;
|
|
19
|
-
if (!gsap?.getProperty) return null;
|
|
20
|
-
const el = iframe.contentDocument?.querySelector(selector);
|
|
21
|
-
if (!el) return null;
|
|
22
20
|
const val = Number(gsap.getProperty(el, prop));
|
|
23
21
|
if (!Number.isFinite(val)) return null;
|
|
24
|
-
return POSITION_PROPS.has(prop) ? Math.round(val) :
|
|
22
|
+
return POSITION_PROPS.has(prop) ? Math.round(val) : roundTo3(val);
|
|
25
23
|
} catch {
|
|
26
24
|
return null;
|
|
27
25
|
}
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
const POSITION_PROPS = new Set(["x", "y", "xPercent", "yPercent"]);
|
|
28
|
+
export const POSITION_PROPS = new Set(["x", "y", "xPercent", "yPercent"]);
|
|
31
29
|
const GSAP_CONFIG_KEYS = new Set([
|
|
32
30
|
"duration",
|
|
33
31
|
"ease",
|
|
@@ -56,22 +54,17 @@ export function readAllAnimatedProperties(
|
|
|
56
54
|
group?: PropertyGroupName,
|
|
57
55
|
): Record<string, number> {
|
|
58
56
|
const result: Record<string, number> = {};
|
|
59
|
-
if (!iframe
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return result;
|
|
65
|
-
}
|
|
66
|
-
if (!gsap?.getProperty) return result;
|
|
57
|
+
if (!iframe) return result;
|
|
58
|
+
const gsap = getIframeGsap(iframe);
|
|
59
|
+
if (!gsap) return result;
|
|
60
|
+
const el = queryIframeElement(iframe, selector);
|
|
61
|
+
if (!el) return result;
|
|
67
62
|
let doc: Document | null = null;
|
|
68
63
|
try {
|
|
69
|
-
doc = iframe
|
|
64
|
+
doc = iframe?.contentDocument ?? null;
|
|
70
65
|
} catch {
|
|
71
|
-
|
|
66
|
+
/* cross-origin guard — doc stays null */
|
|
72
67
|
}
|
|
73
|
-
const el = doc?.querySelector(selector);
|
|
74
|
-
if (!el) return result;
|
|
75
68
|
|
|
76
69
|
const propKeys = new Set<string>();
|
|
77
70
|
if (anim.keyframes) {
|
|
@@ -94,7 +87,7 @@ export function readAllAnimatedProperties(
|
|
|
94
87
|
for (const prop of propKeys) {
|
|
95
88
|
const val = Number(gsap.getProperty(el, prop));
|
|
96
89
|
if (Number.isFinite(val)) {
|
|
97
|
-
result[prop] = POSITION_PROPS.has(prop) ? Math.round(val) :
|
|
90
|
+
result[prop] = POSITION_PROPS.has(prop) ? Math.round(val) : roundTo3(val);
|
|
98
91
|
}
|
|
99
92
|
}
|
|
100
93
|
|
|
@@ -166,7 +159,7 @@ export function readAllAnimatedProperties(
|
|
|
166
159
|
if (!allTweenedProps.has(prop)) continue;
|
|
167
160
|
const val = Number(gsap.getProperty(el, prop));
|
|
168
161
|
if (Number.isFinite(val) && Math.round(val * 1000) !== Math.round(defaultVal * 1000)) {
|
|
169
|
-
result[prop] =
|
|
162
|
+
result[prop] = roundTo3(val);
|
|
170
163
|
}
|
|
171
164
|
}
|
|
172
165
|
|
|
@@ -208,7 +201,7 @@ export function readAllAnimatedProperties(
|
|
|
208
201
|
}
|
|
209
202
|
if (Number.isFinite(cssVal) && Math.round(gsapVal * 1000) === Math.round(cssVal * 1000))
|
|
210
203
|
continue;
|
|
211
|
-
result[prop] =
|
|
204
|
+
result[prop] = roundTo3(gsapVal);
|
|
212
205
|
}
|
|
213
206
|
|
|
214
207
|
return result;
|
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
import { findUnsafeDomPatchValues } from "@hyperframes/core/studio-api/finite-mutation";
|
|
2
2
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
opacity: 1,
|
|
6
|
-
x: 0,
|
|
7
|
-
y: 0,
|
|
8
|
-
scale: 1,
|
|
9
|
-
scaleX: 1,
|
|
10
|
-
scaleY: 1,
|
|
11
|
-
rotation: 0,
|
|
12
|
-
width: 100,
|
|
13
|
-
height: 100,
|
|
14
|
-
};
|
|
4
|
+
export { PROPERTY_DEFAULTS } from "./gsapShared";
|
|
15
5
|
|
|
16
6
|
export function ensureElementAddressable(selection: DomEditSelection): {
|
|
17
7
|
selector: string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ParsedGsap } from "@hyperframes/core/gsap-parser";
|
|
2
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
3
|
+
import type { EditHistoryKind } from "../utils/editHistory";
|
|
4
|
+
|
|
5
|
+
export interface MutationResult {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
changed?: boolean;
|
|
8
|
+
parsed?: ParsedGsap;
|
|
9
|
+
before?: string;
|
|
10
|
+
after?: string;
|
|
11
|
+
scriptText?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CommitMutationOptions {
|
|
15
|
+
label: string;
|
|
16
|
+
coalesceKey?: string;
|
|
17
|
+
softReload?: boolean;
|
|
18
|
+
skipReload?: boolean;
|
|
19
|
+
beforeReload?: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type CommitMutation = (
|
|
23
|
+
selection: DomEditSelection,
|
|
24
|
+
mutation: Record<string, unknown>,
|
|
25
|
+
options: CommitMutationOptions,
|
|
26
|
+
) => Promise<void>;
|
|
27
|
+
|
|
28
|
+
export type SafeGsapCommitMutation = (
|
|
29
|
+
selection: DomEditSelection,
|
|
30
|
+
mutation: Record<string, unknown>,
|
|
31
|
+
options: CommitMutationOptions,
|
|
32
|
+
) => void;
|
|
33
|
+
|
|
34
|
+
export type TrackGsapSaveFailure = (
|
|
35
|
+
error: unknown,
|
|
36
|
+
selection: DomEditSelection,
|
|
37
|
+
mutation: Record<string, unknown>,
|
|
38
|
+
label?: string,
|
|
39
|
+
) => void;
|
|
40
|
+
|
|
41
|
+
export interface GsapScriptCommitsParams {
|
|
42
|
+
projectIdRef: React.MutableRefObject<string | null>;
|
|
43
|
+
activeCompPath: string | null;
|
|
44
|
+
previewIframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
45
|
+
editHistory: {
|
|
46
|
+
recordEdit: (entry: {
|
|
47
|
+
label: string;
|
|
48
|
+
kind: EditHistoryKind;
|
|
49
|
+
coalesceKey?: string;
|
|
50
|
+
files: Record<string, { before: string; after: string }>;
|
|
51
|
+
}) => Promise<void>;
|
|
52
|
+
};
|
|
53
|
+
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
54
|
+
reloadPreview: () => void;
|
|
55
|
+
onCacheInvalidate: () => void;
|
|
56
|
+
onFileContentChanged?: (path: string, content: string) => void;
|
|
57
|
+
showToast: (message: string, tone?: "error" | "info") => void;
|
|
58
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared GSAP primitives used across multiple hook files.
|
|
3
|
+
* Centralises duplicated interfaces, constants, and small utilities
|
|
4
|
+
* to reduce drift risk.
|
|
5
|
+
*/
|
|
6
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
7
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
8
|
+
import {
|
|
9
|
+
absoluteToPercentage,
|
|
10
|
+
resolveTweenStart,
|
|
11
|
+
resolveTweenDuration,
|
|
12
|
+
} from "../utils/globalTimeCompiler";
|
|
13
|
+
|
|
14
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/** Canonical interface for the iframe-hosted GSAP runtime. */
|
|
17
|
+
export interface IframeGsap {
|
|
18
|
+
getProperty: (el: Element, prop: string) => number;
|
|
19
|
+
set?: (target: string, vars: Record<string, number | string>) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export const PROPERTY_DEFAULTS: Record<string, number> = {
|
|
25
|
+
opacity: 1,
|
|
26
|
+
x: 0,
|
|
27
|
+
y: 0,
|
|
28
|
+
scale: 1,
|
|
29
|
+
scaleX: 1,
|
|
30
|
+
scaleY: 1,
|
|
31
|
+
rotation: 0,
|
|
32
|
+
width: 100,
|
|
33
|
+
height: 100,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ── Selector resolution ───────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get a CSS selector string from a DomEditSelection.
|
|
40
|
+
* Returns `#id` if the selection has an id, otherwise the raw selector,
|
|
41
|
+
* or null if neither exists.
|
|
42
|
+
*/
|
|
43
|
+
export function selectorFromSelection(selection: DomEditSelection): string | null {
|
|
44
|
+
if (selection.id) return `#${selection.id}`;
|
|
45
|
+
if (selection.selector) return selection.selector;
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Percentage computation ────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Compute the current playback percentage within an element's animation range.
|
|
53
|
+
* Uses the animation's resolved timing if available, otherwise falls back to
|
|
54
|
+
* the element's data-start / data-duration attributes.
|
|
55
|
+
*/
|
|
56
|
+
export function computeElementPercentage(
|
|
57
|
+
currentTime: number,
|
|
58
|
+
selection: DomEditSelection,
|
|
59
|
+
animation?: GsapAnimation | null,
|
|
60
|
+
): number {
|
|
61
|
+
if (animation) {
|
|
62
|
+
const start = resolveTweenStart(animation);
|
|
63
|
+
const duration = resolveTweenDuration(animation);
|
|
64
|
+
if (start !== null) {
|
|
65
|
+
return absoluteToPercentage(currentTime, start, duration);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const elStart = Number.parseFloat(selection.dataAttributes?.start ?? "0") || 0;
|
|
69
|
+
const elDuration = Number.parseFloat(selection.dataAttributes?.duration ?? "1") || 1;
|
|
70
|
+
return elDuration > 0
|
|
71
|
+
? Math.max(0, Math.min(100, Math.round(((currentTime - elStart) / elDuration) * 1000) / 10))
|
|
72
|
+
: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Iframe accessors ──────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/** Safely retrieve the GSAP runtime from the preview iframe. */
|
|
78
|
+
export function getIframeGsap(iframe: HTMLIFrameElement | null): IframeGsap | null {
|
|
79
|
+
if (!iframe?.contentWindow) return null;
|
|
80
|
+
try {
|
|
81
|
+
const gsap = (iframe.contentWindow as unknown as { gsap?: IframeGsap }).gsap;
|
|
82
|
+
return gsap?.getProperty ? gsap : null;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Safely query an element inside the preview iframe's document. */
|
|
89
|
+
export function queryIframeElement(
|
|
90
|
+
iframe: HTMLIFrameElement | null,
|
|
91
|
+
selector: string,
|
|
92
|
+
): Element | null {
|
|
93
|
+
try {
|
|
94
|
+
return iframe?.contentDocument?.querySelector(selector) ?? null;
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Safely access an iframe's contentDocument, returning null on cross-origin errors. */
|
|
101
|
+
export function getIframeDocument(iframe: HTMLIFrameElement | null): Document | null {
|
|
102
|
+
if (!iframe) return null;
|
|
103
|
+
try {
|
|
104
|
+
return iframe.contentDocument;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Keyframe parsing ──────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
export interface ParsedPercentageKeyframes {
|
|
113
|
+
keyframes: Array<{ percentage: number; properties: Record<string, number | string> }>;
|
|
114
|
+
easeEach?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Parse a GSAP percentage-keyframe object (`{ "0%": { x: 10 }, "100%": { x: 200 } }`)
|
|
119
|
+
* into a sorted array of `{ percentage, properties }` entries.
|
|
120
|
+
* Returns `null` when the object contains no valid keyframe entries.
|
|
121
|
+
*/
|
|
122
|
+
export function parsePercentageKeyframes(
|
|
123
|
+
kfObj: Record<string, unknown>,
|
|
124
|
+
): ParsedPercentageKeyframes | null {
|
|
125
|
+
const keyframes: ParsedPercentageKeyframes["keyframes"] = [];
|
|
126
|
+
let easeEach: string | undefined;
|
|
127
|
+
|
|
128
|
+
for (const [key, val] of Object.entries(kfObj)) {
|
|
129
|
+
if (key === "easeEach") {
|
|
130
|
+
if (typeof val === "string") easeEach = val;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const pctMatch = key.match(/^(\d+(?:\.\d+)?)%$/);
|
|
134
|
+
if (!pctMatch || !val || typeof val !== "object") continue;
|
|
135
|
+
const percentage = parseFloat(pctMatch[1]);
|
|
136
|
+
const properties: Record<string, number | string> = {};
|
|
137
|
+
for (const [pk, pv] of Object.entries(val as Record<string, unknown>)) {
|
|
138
|
+
if (pk === "ease") continue;
|
|
139
|
+
if (typeof pv === "number") properties[pk] = Math.round(pv * 1000) / 1000;
|
|
140
|
+
else if (typeof pv === "string") properties[pk] = pv;
|
|
141
|
+
}
|
|
142
|
+
if (Object.keys(properties).length > 0) {
|
|
143
|
+
keyframes.push({ percentage, properties });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (keyframes.length === 0) return null;
|
|
148
|
+
keyframes.sort((a, b) => a.percentage - b.percentage);
|
|
149
|
+
return { keyframes, easeEach };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Time conversion ───────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/** Convert a tween-relative percentage to an absolute time. */
|
|
155
|
+
export function toAbsoluteTime(tweenPos: number, tweenDur: number, percentage: number): number {
|
|
156
|
+
return tweenPos + (percentage / 100) * tweenDur;
|
|
157
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TimelineElement } from "../player";
|
|
1
|
+
import type { TimelineElement } from "../player/store/playerStore";
|
|
2
2
|
import { applyPatchByTarget, readAttributeByTarget } from "../utils/sourcePatcher";
|
|
3
3
|
import { formatTimelineAttributeNumber } from "../player/components/timelineEditing";
|
|
4
4
|
import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
|
|
@@ -6,7 +6,7 @@ import type { EditHistoryKind } from "../utils/editHistory";
|
|
|
6
6
|
|
|
7
7
|
// ── Types ──
|
|
8
8
|
|
|
9
|
-
interface RecordEditInput {
|
|
9
|
+
export interface RecordEditInput {
|
|
10
10
|
label: string;
|
|
11
11
|
kind: EditHistoryKind;
|
|
12
12
|
coalesceKey?: string;
|
|
@@ -144,5 +144,66 @@ export async function readFileContent(projectId: string, targetPath: string): Pr
|
|
|
144
144
|
return data.content;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Shift all GSAP animation positions targeting a given element by a time delta.
|
|
149
|
+
* Calls the server-side GSAP mutation endpoint which uses the AST-based parser.
|
|
150
|
+
*/
|
|
151
|
+
export async function shiftGsapPositions(
|
|
152
|
+
projectId: string,
|
|
153
|
+
filePath: string,
|
|
154
|
+
elementId: string,
|
|
155
|
+
delta: number,
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
if (delta === 0 || !elementId) return;
|
|
158
|
+
const res = await fetch(
|
|
159
|
+
`/api/projects/${projectId}/gsap-mutations/${encodeURIComponent(filePath)}`,
|
|
160
|
+
{
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: { "Content-Type": "application/json" },
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
type: "shift-positions",
|
|
165
|
+
targetSelector: `#${elementId}`,
|
|
166
|
+
delta,
|
|
167
|
+
}),
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
const err = await res.json().catch(() => null);
|
|
172
|
+
throw new Error((err as { error?: string })?.error ?? "shift-positions failed");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function scaleGsapPositions(
|
|
177
|
+
projectId: string,
|
|
178
|
+
filePath: string,
|
|
179
|
+
elementId: string,
|
|
180
|
+
oldStart: number,
|
|
181
|
+
oldDuration: number,
|
|
182
|
+
newStart: number,
|
|
183
|
+
newDuration: number,
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
if (!elementId || oldDuration <= 0 || newDuration <= 0) return;
|
|
186
|
+
if (oldStart === newStart && oldDuration === newDuration) return;
|
|
187
|
+
const res = await fetch(
|
|
188
|
+
`/api/projects/${projectId}/gsap-mutations/${encodeURIComponent(filePath)}`,
|
|
189
|
+
{
|
|
190
|
+
method: "POST",
|
|
191
|
+
headers: { "Content-Type": "application/json" },
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
type: "scale-positions",
|
|
194
|
+
targetSelector: `#${elementId}`,
|
|
195
|
+
oldStart,
|
|
196
|
+
oldDuration,
|
|
197
|
+
newStart,
|
|
198
|
+
newDuration,
|
|
199
|
+
}),
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
if (!res.ok) {
|
|
203
|
+
const err = await res.json().catch(() => null);
|
|
204
|
+
throw new Error((err as { error?: string })?.error ?? "scale-positions failed");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
147
208
|
// Re-export applyPatchByTarget for use in the hook (avoids double import in callers)
|
|
148
209
|
export { applyPatchByTarget, formatTimelineAttributeNumber };
|
|
@@ -12,6 +12,7 @@ import { classifyPropertyGroup } from "@hyperframes/core/gsap-parser";
|
|
|
12
12
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
13
13
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
14
14
|
import { readAllAnimatedProperties, readGsapProperty } from "./gsapRuntimeBridge";
|
|
15
|
+
import { selectorFromSelection, computeElementPercentage } from "./gsapShared";
|
|
15
16
|
|
|
16
17
|
interface CommitAnimatedPropertyDeps {
|
|
17
18
|
selectedGsapAnimations: GsapAnimation[];
|
|
@@ -37,23 +38,6 @@ interface CommitAnimatedPropertyDeps {
|
|
|
37
38
|
bumpGsapCache: () => void;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function computePercentage(selection: DomEditSelection, anim?: GsapAnimation): number {
|
|
41
|
-
const currentTime = usePlayerStore.getState().currentTime;
|
|
42
|
-
const tweenPos = anim?.resolvedStart ?? (typeof anim?.position === "number" ? anim.position : 0);
|
|
43
|
-
const tweenDur = anim?.duration ?? 0;
|
|
44
|
-
if (tweenDur > 0) {
|
|
45
|
-
return Math.max(
|
|
46
|
-
0,
|
|
47
|
-
Math.min(100, Math.round(((currentTime - tweenPos) / tweenDur) * 1000) / 10),
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
const elStart = Number.parseFloat(selection.dataAttributes?.start ?? "0") || 0;
|
|
51
|
-
const elDuration = Number.parseFloat(selection.dataAttributes?.duration ?? "1") || 1;
|
|
52
|
-
return elDuration > 0
|
|
53
|
-
? Math.max(0, Math.min(100, Math.round(((currentTime - elStart) / elDuration) * 1000) / 10))
|
|
54
|
-
: 0;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
41
|
function pickBestAnimation(
|
|
58
42
|
animations: GsapAnimation[],
|
|
59
43
|
selector: string | null,
|
|
@@ -78,12 +62,6 @@ function pickBestAnimation(
|
|
|
78
62
|
return scored[0]?.anim;
|
|
79
63
|
}
|
|
80
64
|
|
|
81
|
-
function selectorFor(selection: DomEditSelection): string | null {
|
|
82
|
-
if (selection.id) return `#${selection.id}`;
|
|
83
|
-
if (selection.selector) return selection.selector;
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
65
|
export function useAnimatedPropertyCommit(deps: CommitAnimatedPropertyDeps) {
|
|
88
66
|
const {
|
|
89
67
|
selectedGsapAnimations,
|
|
@@ -102,7 +80,7 @@ export function useAnimatedPropertyCommit(deps: CommitAnimatedPropertyDeps) {
|
|
|
102
80
|
if (!gsapCommitMutation) return;
|
|
103
81
|
|
|
104
82
|
const iframe = previewIframeRef.current;
|
|
105
|
-
const selector =
|
|
83
|
+
const selector = selectorFromSelection(selection);
|
|
106
84
|
|
|
107
85
|
let anim: GsapAnimation | undefined = pickBestAnimation(
|
|
108
86
|
selectedGsapAnimations,
|
|
@@ -132,7 +110,7 @@ export function useAnimatedPropertyCommit(deps: CommitAnimatedPropertyDeps) {
|
|
|
132
110
|
);
|
|
133
111
|
}
|
|
134
112
|
|
|
135
|
-
const pct =
|
|
113
|
+
const pct = computeElementPercentage(usePlayerStore.getState().currentTime, selection, anim);
|
|
136
114
|
|
|
137
115
|
// Read all currently animated properties from runtime for backfill
|
|
138
116
|
const runtimeProps = selector ? readAllAnimatedProperties(iframe, selector, anim) : {};
|