@hyperframes/studio 0.7.6 → 0.7.8
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-B_NnmU6q.js → index--lOAjFJl.js} +1 -1
- package/dist/assets/{index-ysftPins.js → index-BSyZKYmH.js} +204 -203
- package/dist/assets/{index-DsBhGFPe.js → index-Dgeszckd.js} +1 -1
- package/dist/assets/index-svYFaNuq.css +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.html +2 -2
- package/dist/index.js +3650 -2431
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/components/StudioRightPanel.tsx +7 -1
- package/src/components/editor/AnimationCard.tsx +6 -0
- package/src/components/editor/DomEditOverlay.tsx +4 -12
- package/src/components/editor/EaseCurveSection.tsx +175 -99
- package/src/components/editor/GsapAnimationSection.tsx +2 -0
- package/src/components/editor/KeyframeEaseList.tsx +67 -3
- package/src/components/editor/MarqueeOverlay.tsx +40 -0
- package/src/components/editor/OffCanvasIndicators.tsx +2 -7
- package/src/components/editor/PropertyPanel.tsx +23 -2
- package/src/components/editor/Transform3DCube.tsx +313 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +2 -0
- package/src/components/editor/gsapAnimationConstants.ts +11 -35
- package/src/components/editor/gsapAnimationHelpers.test.ts +1 -1
- package/src/components/editor/marqueeCommit.ts +63 -20
- package/src/components/editor/propertyPanel3dTransform.tsx +311 -92
- package/src/components/editor/propertyPanelHelpers.ts +46 -18
- package/src/components/editor/propertyPanelTimingSection.tsx +35 -2
- package/src/components/editor/transform3dProjection.test.ts +99 -0
- package/src/components/editor/transform3dProjection.ts +172 -0
- package/src/components/editor/useMotionPathData.ts +2 -1
- package/src/components/sidebar/AssetContextMenu.tsx +97 -0
- package/src/components/sidebar/AssetsTab.tsx +364 -266
- package/src/components/sidebar/AudioRow.tsx +202 -0
- package/src/components/sidebar/assetHelpers.ts +40 -0
- package/src/contexts/DomEditContext.tsx +8 -0
- package/src/hooks/gsapDragCommit.test.ts +9 -5
- package/src/hooks/gsapDragCommit.ts +129 -9
- package/src/hooks/gsapRuntimeBridge.ts +22 -0
- package/src/hooks/gsapRuntimeKeyframes.test.ts +47 -0
- package/src/hooks/gsapRuntimeKeyframes.ts +63 -5
- package/src/hooks/gsapRuntimePatch.ts +162 -35
- package/src/hooks/useAnimatedPropertyCommit.ts +256 -78
- package/src/hooks/useDomEditCommits.ts +0 -2
- package/src/hooks/useDomEditSession.ts +24 -2
- package/src/hooks/useDomEditWiring.ts +3 -0
- package/src/hooks/useDomGeometryCommits.ts +3 -31
- package/src/hooks/useGestureCommit.ts +0 -4
- package/src/hooks/useGsapAwareEditing.ts +31 -5
- package/src/hooks/useGsapKeyframeOps.ts +4 -1
- package/src/hooks/useGsapScriptCommits.ts +0 -12
- package/src/hooks/useGsapSelectionHandlers.ts +12 -9
- package/src/hooks/useGsapTweenCache.test.ts +45 -1
- package/src/hooks/useGsapTweenCache.ts +125 -42
- package/src/hooks/useMusicBeatAnalysis.ts +36 -21
- package/src/hooks/useRazorSplit.ts +0 -3
- package/src/utils/marqueeGeometry.test.ts +15 -98
- package/src/utils/marqueeGeometry.ts +1 -158
- package/dist/assets/index-wJZf6FK3.css +0 -1
- package/src/utils/editDebugLog.ts +0 -9
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
1
2
|
import type { DomEditSelection } from "./domEditingTypes";
|
|
2
3
|
import { STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
|
|
3
4
|
import { MetricField } from "./propertyPanelPrimitives";
|
|
4
5
|
import { KeyframeNavigation } from "./KeyframeNavigation";
|
|
5
6
|
import { formatPxMetricValue, parsePxMetricValue, RESPONSIVE_GRID } from "./propertyPanelHelpers";
|
|
7
|
+
import { Transform3DCube, type CubePose } from "./Transform3DCube";
|
|
6
8
|
|
|
7
9
|
type KeyframeEntry = Array<{
|
|
8
10
|
percentage: number;
|
|
@@ -24,9 +26,216 @@ interface PropertyPanel3dTransformProps {
|
|
|
24
26
|
property: string,
|
|
25
27
|
value: number,
|
|
26
28
|
) => Promise<void>;
|
|
29
|
+
/** Batched commit — several props into one keyframe (the cube's rotationX/Y/Z). */
|
|
30
|
+
onCommitAnimatedProperties?: (
|
|
31
|
+
element: DomEditSelection,
|
|
32
|
+
props: Record<string, number | string>,
|
|
33
|
+
) => Promise<void>;
|
|
27
34
|
onSeekToTime?: (time: number) => void;
|
|
28
35
|
onRemoveKeyframe?: (animId: string, pct: number) => void;
|
|
29
|
-
onConvertToKeyframes?: (animId: string) => void;
|
|
36
|
+
onConvertToKeyframes?: (animId: string, duration?: number) => void;
|
|
37
|
+
/** Live-set props on the preview element during a cube drag (no source write). */
|
|
38
|
+
onLivePreviewProps?: (element: DomEditSelection, props: Record<string, number>) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type CommitAnimatedProperty = (
|
|
42
|
+
element: DomEditSelection,
|
|
43
|
+
property: string,
|
|
44
|
+
value: number,
|
|
45
|
+
) => Promise<void>;
|
|
46
|
+
|
|
47
|
+
/** The draggable cube + its commit/recenter/live-preview wiring. */
|
|
48
|
+
function Cube3dControl({
|
|
49
|
+
element,
|
|
50
|
+
gsapRuntimeValues,
|
|
51
|
+
onCommitAnimatedProperty,
|
|
52
|
+
onCommitAnimatedProperties,
|
|
53
|
+
onLivePreviewProps,
|
|
54
|
+
onKeyframe,
|
|
55
|
+
keyframed,
|
|
56
|
+
}: {
|
|
57
|
+
element: DomEditSelection;
|
|
58
|
+
gsapRuntimeValues: Record<string, number>;
|
|
59
|
+
onCommitAnimatedProperty: CommitAnimatedProperty;
|
|
60
|
+
onCommitAnimatedProperties?: (
|
|
61
|
+
element: DomEditSelection,
|
|
62
|
+
props: Record<string, number | string>,
|
|
63
|
+
) => Promise<void>;
|
|
64
|
+
onLivePreviewProps?: (element: DomEditSelection, props: Record<string, number>) => void;
|
|
65
|
+
onKeyframe?: () => void;
|
|
66
|
+
keyframed?: boolean;
|
|
67
|
+
}) {
|
|
68
|
+
const pose: CubePose = {
|
|
69
|
+
rotationX: gsapRuntimeValues.rotationX ?? 0,
|
|
70
|
+
rotationY: gsapRuntimeValues.rotationY ?? 0,
|
|
71
|
+
rotationZ: gsapRuntimeValues.rotationZ ?? 0,
|
|
72
|
+
};
|
|
73
|
+
// Commit only the rotation axes the drag actually changed (each rounded to a
|
|
74
|
+
// whole degree). Reuses the keyframe-aware animated-property commit, so a drag
|
|
75
|
+
// at the playhead writes/updates a keyframe just like the numeric fields.
|
|
76
|
+
const commitPose = (next: CubePose) => {
|
|
77
|
+
const changedProps: Record<string, number> = {};
|
|
78
|
+
for (const axis of ["rotationX", "rotationY", "rotationZ"] as const) {
|
|
79
|
+
const rounded = Math.round(next[axis]);
|
|
80
|
+
if (rounded !== Math.round(pose[axis])) changedProps[axis] = rounded;
|
|
81
|
+
}
|
|
82
|
+
const axes = Object.keys(changedProps);
|
|
83
|
+
if (axes.length === 0) return;
|
|
84
|
+
// ONE keyframe for the whole pose change — avoids per-axis commits racing into
|
|
85
|
+
// adjacent duplicate keyframes. Fall back to per-axis if no batched commit.
|
|
86
|
+
if (onCommitAnimatedProperties) {
|
|
87
|
+
void onCommitAnimatedProperties(element, changedProps);
|
|
88
|
+
} else {
|
|
89
|
+
for (const [axis, v] of Object.entries(changedProps))
|
|
90
|
+
onCommitAnimatedProperty(element, axis, v);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const recenter = () => {
|
|
94
|
+
// ONE commit for the whole reset — six per-axis commits meant six soft-reloads
|
|
95
|
+
// (six flashes) for a single click. Batch like commitPose does.
|
|
96
|
+
const identity = {
|
|
97
|
+
rotationX: 0,
|
|
98
|
+
rotationY: 0,
|
|
99
|
+
rotationZ: 0,
|
|
100
|
+
z: 0,
|
|
101
|
+
scale: 1,
|
|
102
|
+
transformPerspective: 0,
|
|
103
|
+
};
|
|
104
|
+
if (onCommitAnimatedProperties) {
|
|
105
|
+
void onCommitAnimatedProperties(element, identity);
|
|
106
|
+
} else {
|
|
107
|
+
for (const [prop, v] of Object.entries(identity))
|
|
108
|
+
void onCommitAnimatedProperty(element, prop, v);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
// Immediate element feedback while dragging — set the live transform without a
|
|
112
|
+
// source write; the release commits via onCommitAnimatedProperty.
|
|
113
|
+
const livePreview = (next: CubePose) =>
|
|
114
|
+
onLivePreviewProps?.(element, {
|
|
115
|
+
rotationX: next.rotationX,
|
|
116
|
+
rotationY: next.rotationY,
|
|
117
|
+
rotationZ: next.rotationZ,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="mb-2 px-2">
|
|
122
|
+
<div className="mx-auto max-w-[184px]">
|
|
123
|
+
<Transform3DCube
|
|
124
|
+
pose={pose}
|
|
125
|
+
perspective={gsapRuntimeValues.transformPerspective ?? 0}
|
|
126
|
+
onPoseDraft={livePreview}
|
|
127
|
+
onPoseCommit={commitPose}
|
|
128
|
+
onPerspectiveDraft={(px) => onLivePreviewProps?.(element, { transformPerspective: px })}
|
|
129
|
+
onPerspectiveCommit={(px) =>
|
|
130
|
+
void onCommitAnimatedProperty(element, "transformPerspective", px)
|
|
131
|
+
}
|
|
132
|
+
onRecenter={recenter}
|
|
133
|
+
onKeyframe={onKeyframe}
|
|
134
|
+
keyframed={keyframed}
|
|
135
|
+
/>
|
|
136
|
+
<p className="mt-1 text-center text-[9px] leading-snug text-neutral-600">
|
|
137
|
+
Drag to tilt · Shift-drag to roll
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface FieldCtx {
|
|
145
|
+
element: DomEditSelection;
|
|
146
|
+
gsapRuntimeValues: Record<string, number>;
|
|
147
|
+
gsapKeyframes: KeyframeEntry;
|
|
148
|
+
gsapAnimId: string | null;
|
|
149
|
+
currentPct: number;
|
|
150
|
+
elStart: number;
|
|
151
|
+
elDuration: number;
|
|
152
|
+
resolveAnimIdForProp?: (prop: string) => string | null;
|
|
153
|
+
onCommitAnimatedProperty?: (
|
|
154
|
+
element: DomEditSelection,
|
|
155
|
+
property: string,
|
|
156
|
+
value: number,
|
|
157
|
+
) => Promise<void>;
|
|
158
|
+
onSeekToTime?: (time: number) => void;
|
|
159
|
+
onRemoveKeyframe?: (animId: string, pct: number) => void;
|
|
160
|
+
onConvertToKeyframes?: (animId: string, duration?: number) => void;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const parseDeg = (s: string): number | null => {
|
|
164
|
+
const n = Number.parseFloat(s.replace("°", ""));
|
|
165
|
+
return Number.isFinite(n) ? n : null;
|
|
166
|
+
};
|
|
167
|
+
const parseScale = (s: string): number | null => {
|
|
168
|
+
const n = Number.parseFloat(s);
|
|
169
|
+
return Number.isFinite(n) ? n : null;
|
|
170
|
+
};
|
|
171
|
+
const parsePxNonNeg = (s: string): number | null => {
|
|
172
|
+
const v = parsePxMetricValue(s);
|
|
173
|
+
return v != null && v >= 0 ? v : null;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* One 3D-transform field: a number/scrub input plus its keyframe diamond, so
|
|
178
|
+
* rotation / perspective / Z / scale can each be keyframed just like Layout's
|
|
179
|
+
* X / Y — the diamond was previously missing on the rotation + perspective rows.
|
|
180
|
+
*/
|
|
181
|
+
function Transform3dField({
|
|
182
|
+
label,
|
|
183
|
+
prop,
|
|
184
|
+
scrub,
|
|
185
|
+
format,
|
|
186
|
+
parse,
|
|
187
|
+
defaultValue,
|
|
188
|
+
ctx,
|
|
189
|
+
}: {
|
|
190
|
+
label: string;
|
|
191
|
+
prop: string;
|
|
192
|
+
scrub?: boolean;
|
|
193
|
+
format: (v: number) => string;
|
|
194
|
+
parse: (s: string) => number | null;
|
|
195
|
+
defaultValue: number;
|
|
196
|
+
ctx: FieldCtx;
|
|
197
|
+
}) {
|
|
198
|
+
const { gsapAnimId, onCommitAnimatedProperty } = ctx;
|
|
199
|
+
const idFor = (p: string) => ctx.resolveAnimIdForProp?.(p) ?? gsapAnimId;
|
|
200
|
+
const current = ctx.gsapRuntimeValues[prop] ?? defaultValue;
|
|
201
|
+
return (
|
|
202
|
+
<div className="flex items-center gap-1">
|
|
203
|
+
<div className="flex-1">
|
|
204
|
+
<MetricField
|
|
205
|
+
label={label}
|
|
206
|
+
value={format(current)}
|
|
207
|
+
scrub={scrub}
|
|
208
|
+
onCommit={(next) => {
|
|
209
|
+
const v = parse(next);
|
|
210
|
+
if (v != null && onCommitAnimatedProperty) {
|
|
211
|
+
void onCommitAnimatedProperty(ctx.element, prop, v);
|
|
212
|
+
}
|
|
213
|
+
}}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
{STUDIO_KEYFRAMES_ENABLED && (gsapAnimId || onCommitAnimatedProperty) && (
|
|
217
|
+
<KeyframeNavigation
|
|
218
|
+
property={prop}
|
|
219
|
+
keyframes={ctx.gsapKeyframes}
|
|
220
|
+
currentPercentage={ctx.currentPct}
|
|
221
|
+
onSeek={(pct) => ctx.onSeekToTime?.(ctx.elStart + (pct / 100) * ctx.elDuration)}
|
|
222
|
+
onAddKeyframe={() => {
|
|
223
|
+
if (onCommitAnimatedProperty) void onCommitAnimatedProperty(ctx.element, prop, current);
|
|
224
|
+
}}
|
|
225
|
+
onRemoveKeyframe={(pct) => {
|
|
226
|
+
const id = idFor(prop);
|
|
227
|
+
if (id) ctx.onRemoveKeyframe?.(id, pct);
|
|
228
|
+
}}
|
|
229
|
+
onConvertToKeyframes={() => {
|
|
230
|
+
const id = idFor(prop);
|
|
231
|
+
// Pass the element's clip duration so a converted static 3D `set`
|
|
232
|
+
// spans the whole clip (keyframes land in range at any playhead).
|
|
233
|
+
if (id) ctx.onConvertToKeyframes?.(id, ctx.elDuration);
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
30
239
|
}
|
|
31
240
|
|
|
32
241
|
export function PropertyPanel3dTransform({
|
|
@@ -39,110 +248,120 @@ export function PropertyPanel3dTransform({
|
|
|
39
248
|
elDuration,
|
|
40
249
|
element,
|
|
41
250
|
onCommitAnimatedProperty,
|
|
251
|
+
onCommitAnimatedProperties,
|
|
42
252
|
onSeekToTime,
|
|
43
253
|
onRemoveKeyframe,
|
|
44
254
|
onConvertToKeyframes,
|
|
255
|
+
onLivePreviewProps,
|
|
45
256
|
}: PropertyPanel3dTransformProps) {
|
|
46
|
-
|
|
257
|
+
// Expanded by default — the cube gizmo is the headline of this panel, so show
|
|
258
|
+
// it up front rather than hiding it behind a collapsed header.
|
|
259
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
260
|
+
const ctx: FieldCtx = {
|
|
261
|
+
element,
|
|
262
|
+
gsapRuntimeValues,
|
|
263
|
+
gsapKeyframes,
|
|
264
|
+
gsapAnimId,
|
|
265
|
+
currentPct,
|
|
266
|
+
elStart,
|
|
267
|
+
elDuration,
|
|
268
|
+
resolveAnimIdForProp,
|
|
269
|
+
onCommitAnimatedProperty,
|
|
270
|
+
onSeekToTime,
|
|
271
|
+
onRemoveKeyframe,
|
|
272
|
+
onConvertToKeyframes,
|
|
273
|
+
};
|
|
274
|
+
|
|
47
275
|
return (
|
|
48
276
|
<div className="mt-3 border-t border-neutral-800/40 pt-3">
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const id = idFor("z");
|
|
80
|
-
if (id) onRemoveKeyframe?.(id, pct);
|
|
81
|
-
}}
|
|
82
|
-
onConvertToKeyframes={() => {
|
|
83
|
-
const id = idFor("z");
|
|
84
|
-
if (id) onConvertToKeyframes?.(id);
|
|
277
|
+
<button
|
|
278
|
+
type="button"
|
|
279
|
+
onClick={() => setCollapsed((v) => !v)}
|
|
280
|
+
className="mb-2 flex w-full items-center justify-between text-[10px] font-medium uppercase tracking-wider text-neutral-600 hover:text-neutral-400"
|
|
281
|
+
>
|
|
282
|
+
<span>3D Transform</span>
|
|
283
|
+
<svg width="9" height="9" viewBox="0 0 10 10" fill="currentColor" aria-hidden>
|
|
284
|
+
{collapsed ? <path d="M3 2l4 3-4 3z" /> : <path d="M2 3l3 4 3-4z" />}
|
|
285
|
+
</svg>
|
|
286
|
+
</button>
|
|
287
|
+
{collapsed ? null : (
|
|
288
|
+
<>
|
|
289
|
+
{onCommitAnimatedProperty && (
|
|
290
|
+
<Cube3dControl
|
|
291
|
+
element={element}
|
|
292
|
+
gsapRuntimeValues={gsapRuntimeValues}
|
|
293
|
+
onCommitAnimatedProperty={onCommitAnimatedProperty}
|
|
294
|
+
onCommitAnimatedProperties={onCommitAnimatedProperties}
|
|
295
|
+
onLivePreviewProps={onLivePreviewProps}
|
|
296
|
+
keyframed={(gsapKeyframes ?? []).some(
|
|
297
|
+
(kf) =>
|
|
298
|
+
"rotationX" in kf.properties ||
|
|
299
|
+
"rotationY" in kf.properties ||
|
|
300
|
+
"rotationZ" in kf.properties,
|
|
301
|
+
)}
|
|
302
|
+
onKeyframe={() => {
|
|
303
|
+
// Convert the 3D ("other"-group) static set to keyframes so the
|
|
304
|
+
// cube can animate; spans the element's clip via elDuration.
|
|
305
|
+
const id = resolveAnimIdForProp?.("rotationX") ?? gsapAnimId;
|
|
306
|
+
if (id) onConvertToKeyframes?.(id, elDuration);
|
|
85
307
|
}}
|
|
86
308
|
/>
|
|
87
309
|
)}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
310
|
+
<div className={RESPONSIVE_GRID}>
|
|
311
|
+
<Transform3dField
|
|
312
|
+
ctx={ctx}
|
|
313
|
+
label="Z"
|
|
314
|
+
prop="z"
|
|
315
|
+
scrub
|
|
316
|
+
format={formatPxMetricValue}
|
|
317
|
+
parse={parsePxMetricValue}
|
|
318
|
+
defaultValue={0}
|
|
319
|
+
/>
|
|
320
|
+
<Transform3dField
|
|
321
|
+
ctx={ctx}
|
|
92
322
|
label="Scale"
|
|
93
|
-
|
|
323
|
+
prop="scale"
|
|
94
324
|
scrub
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
void onCommitAnimatedProperty(element, "scale", v);
|
|
99
|
-
}
|
|
100
|
-
}}
|
|
325
|
+
format={(v) => String(v)}
|
|
326
|
+
parse={parseScale}
|
|
327
|
+
defaultValue={1}
|
|
101
328
|
/>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
onAddKeyframe={() => {
|
|
110
|
-
if (onCommitAnimatedProperty) {
|
|
111
|
-
void onCommitAnimatedProperty(element, "scale", gsapRuntimeValues?.scale ?? 1);
|
|
112
|
-
}
|
|
113
|
-
}}
|
|
114
|
-
onRemoveKeyframe={(pct) => {
|
|
115
|
-
const id = idFor("scale");
|
|
116
|
-
if (id) onRemoveKeyframe?.(id, pct);
|
|
117
|
-
}}
|
|
118
|
-
onConvertToKeyframes={() => {
|
|
119
|
-
const id = idFor("scale");
|
|
120
|
-
if (id) onConvertToKeyframes?.(id);
|
|
121
|
-
}}
|
|
329
|
+
<Transform3dField
|
|
330
|
+
ctx={ctx}
|
|
331
|
+
label="RotX"
|
|
332
|
+
prop="rotationX"
|
|
333
|
+
format={(v) => `${v}°`}
|
|
334
|
+
parse={parseDeg}
|
|
335
|
+
defaultValue={0}
|
|
122
336
|
/>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
337
|
+
<Transform3dField
|
|
338
|
+
ctx={ctx}
|
|
339
|
+
label="RotY"
|
|
340
|
+
prop="rotationY"
|
|
341
|
+
format={(v) => `${v}°`}
|
|
342
|
+
parse={parseDeg}
|
|
343
|
+
defaultValue={0}
|
|
344
|
+
/>
|
|
345
|
+
<Transform3dField
|
|
346
|
+
ctx={ctx}
|
|
347
|
+
label="RotZ"
|
|
348
|
+
prop="rotationZ"
|
|
349
|
+
format={(v) => `${v}°`}
|
|
350
|
+
parse={parseDeg}
|
|
351
|
+
defaultValue={0}
|
|
352
|
+
/>
|
|
353
|
+
<Transform3dField
|
|
354
|
+
ctx={ctx}
|
|
355
|
+
label="Perspective"
|
|
356
|
+
prop="transformPerspective"
|
|
357
|
+
scrub
|
|
358
|
+
format={formatPxMetricValue}
|
|
359
|
+
parse={parsePxNonNeg}
|
|
360
|
+
defaultValue={0}
|
|
361
|
+
/>
|
|
362
|
+
</div>
|
|
363
|
+
</>
|
|
364
|
+
)}
|
|
146
365
|
</div>
|
|
147
366
|
);
|
|
148
367
|
}
|
|
@@ -67,12 +67,19 @@ export interface PropertyPanelProps {
|
|
|
67
67
|
) => void;
|
|
68
68
|
onRemoveKeyframe?: (animationId: string, percentage: number) => void;
|
|
69
69
|
onUpdateKeyframeEase?: (animationId: string, percentage: number, ease: string) => void;
|
|
70
|
-
|
|
70
|
+
onSetAllKeyframeEases?: (animationId: string, ease: string) => void;
|
|
71
|
+
onConvertToKeyframes?: (animationId: string, duration?: number) => void;
|
|
71
72
|
onCommitAnimatedProperty?: (
|
|
72
73
|
selection: DomEditSelection,
|
|
73
74
|
property: string,
|
|
74
75
|
value: number | string,
|
|
75
76
|
) => Promise<void>;
|
|
77
|
+
/** Batched variant: commit several props into ONE keyframe (e.g. the 3D cube's
|
|
78
|
+
* rotationX/Y/Z) so multi-axis edits don't race into adjacent duplicates. */
|
|
79
|
+
onCommitAnimatedProperties?: (
|
|
80
|
+
selection: DomEditSelection,
|
|
81
|
+
props: Record<string, number | string>,
|
|
82
|
+
) => Promise<void>;
|
|
76
83
|
onSeekToTime?: (time: number) => void;
|
|
77
84
|
recordingState?: "idle" | "recording" | "preview";
|
|
78
85
|
recordingDuration?: number;
|
|
@@ -211,8 +218,8 @@ export const LABEL = "text-[11px] font-medium text-panel-text-3";
|
|
|
211
218
|
export const RESPONSIVE_GRID = "grid grid-cols-[repeat(auto-fit,minmax(118px,1fr))] gap-3";
|
|
212
219
|
export const EMPTY_STYLES: Record<string, string> = {};
|
|
213
220
|
|
|
214
|
-
|
|
215
|
-
|
|
221
|
+
const EMPTY_FILTER_VALUE = "none";
|
|
222
|
+
const BOX_SHADOW_PRESETS = {
|
|
216
223
|
none: "none",
|
|
217
224
|
soft: "0 12px 36px rgba(0, 0, 0, 0.28)",
|
|
218
225
|
lift: "0 18px 54px rgba(0, 0, 0, 0.38)",
|
|
@@ -272,12 +279,7 @@ export function parsePxMetricValue(value: string): number | null {
|
|
|
272
279
|
return token.value;
|
|
273
280
|
}
|
|
274
281
|
|
|
275
|
-
|
|
276
|
-
value: number,
|
|
277
|
-
min: number,
|
|
278
|
-
max: number,
|
|
279
|
-
fallback: number,
|
|
280
|
-
): number {
|
|
282
|
+
function clampPanelNumber(value: number, min: number, max: number, fallback: number): number {
|
|
281
283
|
if (!Number.isFinite(value)) return fallback;
|
|
282
284
|
return Math.max(min, Math.min(max, value));
|
|
283
285
|
}
|
|
@@ -320,6 +322,7 @@ export function normalizeTextMetricValue(
|
|
|
320
322
|
function splitCssFunctions(value: string): string[] {
|
|
321
323
|
const functions: string[] = [];
|
|
322
324
|
let current = "";
|
|
325
|
+
// fallow-ignore-next-line code-duplication -- pre-existing; surfaced in this file's diff by an unrelated line shift
|
|
323
326
|
let depth = 0;
|
|
324
327
|
|
|
325
328
|
for (const char of value.trim()) {
|
|
@@ -485,6 +488,39 @@ export function extractBackgroundImageUrl(value: string | undefined): string {
|
|
|
485
488
|
|
|
486
489
|
// ── GSAP runtime value readers (used by PropertyPanel) ────────────────────
|
|
487
490
|
|
|
491
|
+
// fallow-ignore-next-line complexity -- pre-existing; surfaced in this file's diff by an unrelated line shift
|
|
492
|
+
// Core transform channels the panel ALWAYS reads live — even before a just-set
|
|
493
|
+
// value (e.g. rotationX) has re-parsed into `gsapAnimations`. Without this the
|
|
494
|
+
// cube + fields drop the prop and flicker to 0 on every commit; gsap.getProperty
|
|
495
|
+
// reflects the in-place instant patch, so it's the true current value.
|
|
496
|
+
const ALWAYS_READ_CHANNELS = [
|
|
497
|
+
"x",
|
|
498
|
+
"y",
|
|
499
|
+
"rotation",
|
|
500
|
+
"rotationX",
|
|
501
|
+
"rotationY",
|
|
502
|
+
"rotationZ",
|
|
503
|
+
"z",
|
|
504
|
+
"scale",
|
|
505
|
+
"transformPerspective",
|
|
506
|
+
"opacity",
|
|
507
|
+
];
|
|
508
|
+
|
|
509
|
+
/** Every property key the panel should read for an element: animated props + the
|
|
510
|
+
* always-read transform channels. */
|
|
511
|
+
function collectPanelPropKeys(gsapAnimations: GsapAnimation[]): Set<string> {
|
|
512
|
+
const keys = new Set<string>(ALWAYS_READ_CHANNELS);
|
|
513
|
+
for (const anim of gsapAnimations) {
|
|
514
|
+
if (anim.keyframes) {
|
|
515
|
+
for (const kf of anim.keyframes.keyframes) {
|
|
516
|
+
for (const p of Object.keys(kf.properties)) keys.add(p);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
for (const p of Object.keys(anim.properties)) keys.add(p);
|
|
520
|
+
}
|
|
521
|
+
return keys;
|
|
522
|
+
}
|
|
523
|
+
|
|
488
524
|
export function readGsapRuntimeValuesForPanel(
|
|
489
525
|
gsapAnimId: string | null,
|
|
490
526
|
gsapAnimations: GsapAnimation[],
|
|
@@ -505,15 +541,7 @@ export function readGsapRuntimeValuesForPanel(
|
|
|
505
541
|
if (!gsap?.getProperty) return null;
|
|
506
542
|
const el = iframe.contentDocument?.querySelector(selector);
|
|
507
543
|
if (!el) return null;
|
|
508
|
-
const propKeys =
|
|
509
|
-
for (const anim of gsapAnimations) {
|
|
510
|
-
if (anim.keyframes) {
|
|
511
|
-
for (const kf of anim.keyframes.keyframes) {
|
|
512
|
-
for (const p of Object.keys(kf.properties)) propKeys.add(p);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
for (const p of Object.keys(anim.properties)) propKeys.add(p);
|
|
516
|
-
}
|
|
544
|
+
const propKeys = collectPanelPropKeys(gsapAnimations);
|
|
517
545
|
const result: Record<string, number> = {};
|
|
518
546
|
for (const prop of propKeys) {
|
|
519
547
|
const v = Number(gsap.getProperty(el, prop));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
1
2
|
import { Clock } from "../../icons/SystemIcons";
|
|
2
3
|
import type { DomEditSelection } from "./domEditing";
|
|
3
4
|
import { formatTimingValue, RESPONSIVE_GRID } from "./propertyPanelHelpers";
|
|
@@ -9,18 +10,45 @@ function parseTimingValue(input: string): number | null {
|
|
|
9
10
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Derive a time range from the element's GSAP tweens (earliest start → latest
|
|
15
|
+
* end) so an element animated purely by GSAP — with no `data-start` /
|
|
16
|
+
* `data-duration` — still shows a meaningful Timing range instead of 0s.
|
|
17
|
+
*/
|
|
18
|
+
function deriveTimingFromAnimations(
|
|
19
|
+
animations: GsapAnimation[],
|
|
20
|
+
): { start: number; duration: number } | null {
|
|
21
|
+
let lo = Infinity;
|
|
22
|
+
let hi = -Infinity;
|
|
23
|
+
for (const a of animations) {
|
|
24
|
+
const s = a.resolvedStart ?? (typeof a.position === "number" ? a.position : 0);
|
|
25
|
+
const d = a.duration ?? 0;
|
|
26
|
+
lo = Math.min(lo, s);
|
|
27
|
+
hi = Math.max(hi, s + d);
|
|
28
|
+
}
|
|
29
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi) || hi <= lo) return null;
|
|
30
|
+
return { start: lo, duration: hi - lo };
|
|
31
|
+
}
|
|
32
|
+
|
|
12
33
|
export function TimingSection({
|
|
13
34
|
element,
|
|
35
|
+
animations = [],
|
|
14
36
|
onSetAttribute,
|
|
15
37
|
}: {
|
|
16
38
|
element: DomEditSelection;
|
|
39
|
+
animations?: GsapAnimation[];
|
|
17
40
|
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
|
|
18
41
|
}) {
|
|
19
|
-
const
|
|
20
|
-
const
|
|
42
|
+
const explicitStart = Number.parseFloat(element.dataAttributes.start ?? "0") || 0;
|
|
43
|
+
const explicitDuration =
|
|
21
44
|
Number.parseFloat(
|
|
22
45
|
element.dataAttributes.duration ?? element.dataAttributes["hf-authored-duration"] ?? "0",
|
|
23
46
|
) || 0;
|
|
47
|
+
|
|
48
|
+
// No authored clip timing → infer the range from the element's animations.
|
|
49
|
+
const derived = explicitDuration > 0 ? null : deriveTimingFromAnimations(animations);
|
|
50
|
+
const start = derived ? derived.start : explicitStart;
|
|
51
|
+
const duration = derived ? derived.duration : explicitDuration;
|
|
24
52
|
const end = start + duration;
|
|
25
53
|
|
|
26
54
|
const commitStart = (nextValue: string) => {
|
|
@@ -54,6 +82,11 @@ export function TimingSection({
|
|
|
54
82
|
onCommit={commitDuration}
|
|
55
83
|
/>
|
|
56
84
|
</div>
|
|
85
|
+
{derived && (
|
|
86
|
+
<p className="mt-2 text-[10px] leading-snug text-neutral-500">
|
|
87
|
+
Inferred from this element’s animation — edit to pin an explicit clip range.
|
|
88
|
+
</p>
|
|
89
|
+
)}
|
|
57
90
|
</Section>
|
|
58
91
|
);
|
|
59
92
|
}
|