@hyperframes/studio 0.7.5 → 0.7.6
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-BwFzbjZQ.js → index-B_NnmU6q.js} +1 -1
- package/dist/assets/{index-C5NAfiPa.js → index-DsBhGFPe.js} +1 -1
- package/dist/assets/{index-DzWIinxk.css → index-wJZf6FK3.css} +1 -1
- package/dist/assets/index-ysftPins.js +374 -0
- package/dist/chunk-KZXYQYIU.js +876 -0
- package/dist/chunk-KZXYQYIU.js.map +1 -0
- package/dist/domEditingLayers-SSXQZHHQ.js +41 -0
- package/dist/domEditingLayers-SSXQZHHQ.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.html +2 -2
- package/dist/index.js +2298 -2483
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/captions/hooks/useCaptionSync.ts +6 -1
- package/src/components/StudioPreviewArea.tsx +10 -3
- package/src/components/StudioRightPanel.tsx +2 -0
- package/src/components/editor/AnimationCard.tsx +72 -251
- package/src/components/editor/AnimationCardParts.tsx +220 -0
- package/src/components/editor/BlockParamsPanel.tsx +4 -8
- package/src/components/editor/DomEditOverlay.tsx +132 -48
- package/src/components/editor/EaseCurveSection.tsx +6 -6
- package/src/components/editor/GsapAnimationSection.tsx +2 -0
- package/src/components/editor/KeyframeEaseList.tsx +63 -0
- package/src/components/editor/OffCanvasIndicators.tsx +111 -0
- package/src/components/editor/PropertyPanel.tsx +52 -54
- package/src/components/editor/gsapAnimationCallbacks.ts +1 -0
- package/src/components/editor/gsapAnimationConstants.ts +8 -0
- package/src/components/editor/manualOffsetDrag.ts +8 -0
- package/src/components/editor/marqueeCommit.ts +168 -0
- package/src/components/editor/propertyPanelHelpers.ts +1 -0
- package/src/components/editor/snapTargetCollection.ts +0 -5
- package/src/components/panels/SlideshowPanel.tsx +0 -1
- package/src/contexts/DomEditContext.tsx +8 -0
- package/src/hooks/gsapDragPositionCommit.ts +1 -2
- package/src/hooks/gsapRuntimeBridge.ts +6 -2
- package/src/hooks/gsapRuntimeReaders.ts +1 -6
- package/src/hooks/useDomEditCommits.ts +0 -4
- package/src/hooks/useDomEditPreviewSync.ts +5 -0
- package/src/hooks/useDomEditSession.ts +23 -0
- package/src/hooks/useDomEditTextCommits.ts +3 -12
- package/src/hooks/useDomSelection.ts +46 -0
- package/src/hooks/useGestureCommit.ts +26 -3
- package/src/hooks/useGestureRecording.ts +2 -16
- package/src/hooks/useGsapAnimationOps.ts +2 -2
- package/src/hooks/useGsapTweenCache.ts +32 -4
- package/src/player/components/Player.tsx +0 -5
- package/src/player/components/timelineIcons.tsx +2 -1
- package/src/player/hooks/useTimelinePlayer.ts +4 -14
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -9
- package/src/player/lib/timelineIframeHelpers.ts +2 -6
- package/src/telemetry/client.ts +2 -0
- package/src/utils/editDebugLog.ts +3 -10
- package/src/utils/gestureSmoother.test.ts +48 -0
- package/src/utils/gestureSmoother.ts +46 -0
- package/src/utils/marqueeGeometry.test.ts +123 -0
- package/src/utils/marqueeGeometry.ts +172 -0
- package/src/utils/optimisticUpdate.ts +1 -2
- package/src/utils/sourcePatcher.ts +0 -10
- package/src/utils/velocityEaseFitter.test.ts +58 -0
- package/src/utils/velocityEaseFitter.ts +121 -0
- package/dist/assets/index-D_JGXmfx.js +0 -374
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"dompurify": "^3.2.4",
|
|
46
46
|
"marked": "^14.1.4",
|
|
47
47
|
"mediabunny": "^1.45.3",
|
|
48
|
-
"@hyperframes/
|
|
49
|
-
"@hyperframes/sdk": "0.7.
|
|
50
|
-
"@hyperframes/
|
|
48
|
+
"@hyperframes/core": "0.7.6",
|
|
49
|
+
"@hyperframes/sdk": "0.7.6",
|
|
50
|
+
"@hyperframes/player": "0.7.6"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/react": "19",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"vite": "^6.4.2",
|
|
63
63
|
"vitest": "^3.2.4",
|
|
64
64
|
"zustand": "^5.0.0",
|
|
65
|
-
"@hyperframes/producer": "0.7.
|
|
65
|
+
"@hyperframes/producer": "0.7.6"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"react": "19",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useRef } from "react";
|
|
2
2
|
import { useCaptionStore } from "../store";
|
|
3
3
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
4
|
+
import { trackEvent } from "../../telemetry/client";
|
|
4
5
|
import type { CaptionStyle } from "../types";
|
|
5
6
|
|
|
6
7
|
interface CaptionOverrideEntry {
|
|
@@ -78,7 +79,11 @@ export function useCaptionSync(projectId: string | null) {
|
|
|
78
79
|
method: "PUT",
|
|
79
80
|
headers: { "Content-Type": "text/plain" },
|
|
80
81
|
body: JSON.stringify(overrides, null, 2),
|
|
81
|
-
}).catch((
|
|
82
|
+
}).catch((error: unknown) => {
|
|
83
|
+
// Caption auto-save is a data-loss path; surface failures via telemetry
|
|
84
|
+
// so a silently-dropped edit isn't invisible (no console in studio).
|
|
85
|
+
trackEvent("studio_caption_autosave_failed", { error: String(error) });
|
|
86
|
+
});
|
|
82
87
|
}, []);
|
|
83
88
|
|
|
84
89
|
// Auto-save on model changes with 800ms debounce
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
STUDIO_PREVIEW_SELECTION_ENABLED,
|
|
18
18
|
} from "./editor/manualEditingAvailability";
|
|
19
19
|
import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/StudioContext";
|
|
20
|
-
import {
|
|
20
|
+
import { useDomEditActionsContext, useDomEditSelectionContext } from "../contexts/DomEditContext";
|
|
21
21
|
import { TimelineEditProvider } from "../contexts/TimelineEditContext";
|
|
22
22
|
import type { BlockPreviewInfo } from "./sidebar/BlocksTab";
|
|
23
23
|
import { readStudioUiPreferences } from "../utils/studioUiPreferences";
|
|
@@ -117,6 +117,9 @@ export function StudioPreviewArea({
|
|
|
117
117
|
domEditHoverSelection,
|
|
118
118
|
domEditSelection,
|
|
119
119
|
domEditGroupSelections,
|
|
120
|
+
selectedGsapAnimations,
|
|
121
|
+
} = useDomEditSelectionContext();
|
|
122
|
+
const {
|
|
120
123
|
handleTimelineElementSelect,
|
|
121
124
|
handlePreviewCanvasMouseDown,
|
|
122
125
|
handlePreviewCanvasPointerMove,
|
|
@@ -128,15 +131,16 @@ export function StudioPreviewArea({
|
|
|
128
131
|
handleDomGroupPathOffsetCommit,
|
|
129
132
|
handleDomBoxSizeCommit,
|
|
130
133
|
handleDomRotationCommit,
|
|
131
|
-
selectedGsapAnimations,
|
|
132
134
|
handleGsapRemoveKeyframe,
|
|
133
135
|
handleGsapUpdateMeta,
|
|
134
136
|
handleGsapAddKeyframe,
|
|
135
137
|
handleGsapConvertToKeyframes,
|
|
136
138
|
handleGsapDeleteAllForElement,
|
|
137
139
|
buildDomSelectionForTimelineElement,
|
|
138
|
-
|
|
140
|
+
applyMarqueeSelection,
|
|
141
|
+
} = useDomEditActionsContext();
|
|
139
142
|
|
|
143
|
+
// fallow-ignore-next-line complexity
|
|
140
144
|
const [snapPrefs, setSnapPrefs] = useState(() => {
|
|
141
145
|
const p = readStudioUiPreferences();
|
|
142
146
|
return {
|
|
@@ -160,6 +164,7 @@ export function StudioPreviewArea({
|
|
|
160
164
|
const rawId = elId.includes("#") ? (elId.split("#").pop() ?? elId) : elId;
|
|
161
165
|
handleGsapDeleteAllForElement(`#${rawId}`);
|
|
162
166
|
},
|
|
167
|
+
// fallow-ignore-next-line complexity
|
|
163
168
|
onDeleteKeyframe: (_elId: string, pct: number) => {
|
|
164
169
|
const cacheKey = domEditSelection?.id ?? "";
|
|
165
170
|
const cached = usePlayerStore.getState().keyframeCache.get(cacheKey);
|
|
@@ -215,6 +220,7 @@ export function StudioPreviewArea({
|
|
|
215
220
|
}
|
|
216
221
|
}
|
|
217
222
|
},
|
|
223
|
+
// fallow-ignore-next-line complexity
|
|
218
224
|
onToggleKeyframeAtPlayhead: (el: TimelineElement) => {
|
|
219
225
|
const currentTime = usePlayerStore.getState().currentTime;
|
|
220
226
|
const pct =
|
|
@@ -339,6 +345,7 @@ export function StudioPreviewArea({
|
|
|
339
345
|
gridSpacing={snapPrefs.gridSpacing}
|
|
340
346
|
recordingState={recordingState}
|
|
341
347
|
onToggleRecording={onToggleRecording}
|
|
348
|
+
onMarqueeSelect={applyMarqueeSelection}
|
|
342
349
|
/>
|
|
343
350
|
<SnapToolbar onSnapChange={setSnapPrefs} />
|
|
344
351
|
{STUDIO_KEYFRAMES_ENABLED && (
|
|
@@ -121,6 +121,7 @@ export function StudioRightPanel({
|
|
|
121
121
|
handleSetArcPath,
|
|
122
122
|
handleUpdateArcSegment,
|
|
123
123
|
handleUnroll,
|
|
124
|
+
handleUpdateKeyframeEase,
|
|
124
125
|
handleGsapAddKeyframe,
|
|
125
126
|
handleGsapRemoveKeyframe,
|
|
126
127
|
handleGsapConvertToKeyframes,
|
|
@@ -274,6 +275,7 @@ export function StudioRightPanel({
|
|
|
274
275
|
onSetArcPath={handleSetArcPath}
|
|
275
276
|
onUpdateArcSegment={handleUpdateArcSegment}
|
|
276
277
|
onUnroll={handleUnroll}
|
|
278
|
+
onUpdateKeyframeEase={handleUpdateKeyframeEase}
|
|
277
279
|
recordingState={recordingState}
|
|
278
280
|
recordingDuration={recordingDuration}
|
|
279
281
|
onToggleRecording={onToggleRecording}
|
|
@@ -4,237 +4,19 @@ import { SUPPORTED_EASES, SUPPORTED_PROPS } from "@hyperframes/core/gsap-constan
|
|
|
4
4
|
import { RESPONSIVE_GRID } from "./propertyPanelHelpers";
|
|
5
5
|
import { MetricField, SelectField } from "./propertyPanelPrimitives";
|
|
6
6
|
import { controlPointsForGsapEase } from "./studioMotion";
|
|
7
|
-
import {
|
|
8
|
-
EASE_LABELS,
|
|
9
|
-
METHOD_LABELS,
|
|
10
|
-
METHOD_TOOLTIPS,
|
|
11
|
-
PERCENT_PROPS,
|
|
12
|
-
PROP_CONSTRAINTS,
|
|
13
|
-
PROP_LABELS,
|
|
14
|
-
PROP_TOOLTIPS,
|
|
15
|
-
PROP_UNITS,
|
|
16
|
-
clampPropertyValue,
|
|
17
|
-
} from "./gsapAnimationConstants";
|
|
7
|
+
import { EASE_LABELS, METHOD_LABELS, METHOD_TOOLTIPS, PROP_LABELS } from "./gsapAnimationConstants";
|
|
18
8
|
import { buildTweenSummary } from "./gsapAnimationHelpers";
|
|
19
9
|
import { EaseCurveSection } from "./EaseCurveSection";
|
|
20
10
|
import { ArcPathControls } from "./ArcPathControls";
|
|
21
11
|
import type { GsapAnimationEditCallbacks } from "./gsapAnimationCallbacks";
|
|
22
12
|
import { ComputedTweenNotice } from "./ComputedTweenNotice";
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
{ label: "Gray", value: "grayscale(1)" },
|
|
31
|
-
{ label: "None", value: "none" },
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
const CLIP_PATH_PRESETS = [
|
|
35
|
-
{ label: "Circle", value: "circle(50% at 50% 50%)" },
|
|
36
|
-
{ label: "Inset", value: "inset(10%)" },
|
|
37
|
-
{ label: "None", value: "none" },
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
function isPercentProp(prop: string): boolean {
|
|
41
|
-
return PERCENT_PROPS.has(prop);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function displayValue(prop: string, val: number | string): string {
|
|
45
|
-
if (isPercentProp(prop)) return String(Math.round(Math.max(0, Math.min(1, Number(val))) * 100));
|
|
46
|
-
return String(val);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function adjustedValue(prop: string, raw: string): string {
|
|
50
|
-
if (isPercentProp(prop)) return String(clampPropertyValue(prop, Number(raw) / 100));
|
|
51
|
-
const num = Number(raw);
|
|
52
|
-
if (!Number.isNaN(num) && PROP_CONSTRAINTS[prop]) {
|
|
53
|
-
return String(clampPropertyValue(prop, num));
|
|
54
|
-
}
|
|
55
|
-
return raw;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function RemoveButton({ onClick, title }: { onClick: () => void; title: string }) {
|
|
59
|
-
return (
|
|
60
|
-
<button
|
|
61
|
-
type="button"
|
|
62
|
-
onClick={onClick}
|
|
63
|
-
className="flex-shrink-0 rounded p-0.5 text-neutral-600 transition-colors hover:bg-neutral-800 hover:text-red-400"
|
|
64
|
-
title={title}
|
|
65
|
-
>
|
|
66
|
-
<svg
|
|
67
|
-
width="12"
|
|
68
|
-
height="12"
|
|
69
|
-
viewBox="0 0 12 12"
|
|
70
|
-
fill="none"
|
|
71
|
-
stroke="currentColor"
|
|
72
|
-
strokeWidth="1.5"
|
|
73
|
-
>
|
|
74
|
-
<path d="M3 3l6 6M9 3l-6 6" />
|
|
75
|
-
</svg>
|
|
76
|
-
</button>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function PropertyRow({
|
|
81
|
-
prop,
|
|
82
|
-
val,
|
|
83
|
-
onCommit,
|
|
84
|
-
onRemove,
|
|
85
|
-
removeTitle,
|
|
86
|
-
}: {
|
|
87
|
-
prop: string;
|
|
88
|
-
val: number | string;
|
|
89
|
-
onCommit: (adjusted: string) => void;
|
|
90
|
-
onRemove: () => void;
|
|
91
|
-
removeTitle: string;
|
|
92
|
-
}) {
|
|
93
|
-
if (BOOLEAN_PROPS.has(prop)) {
|
|
94
|
-
const isVisible = val === "visible" || val === 1;
|
|
95
|
-
return (
|
|
96
|
-
<div className="flex items-center gap-1">
|
|
97
|
-
<div className="min-w-0 flex-1 flex items-center gap-2 px-2 py-1 rounded-lg bg-neutral-900 border border-neutral-800">
|
|
98
|
-
<span className="flex-1 text-[11px] font-medium text-neutral-500">
|
|
99
|
-
{PROP_LABELS[prop] ?? prop}
|
|
100
|
-
</span>
|
|
101
|
-
<button
|
|
102
|
-
type="button"
|
|
103
|
-
onClick={() => onCommit(isVisible ? "hidden" : "visible")}
|
|
104
|
-
className={`flex-shrink-0 rounded-full transition-all duration-150 relative`}
|
|
105
|
-
style={{ width: 28, height: 16, background: isVisible ? P.accent : P.borderInput }}
|
|
106
|
-
title={isVisible ? "Visible — click to hide" : "Hidden — click to show"}
|
|
107
|
-
>
|
|
108
|
-
<span
|
|
109
|
-
className="absolute top-[2px] left-0 rounded-full transition-transform duration-150"
|
|
110
|
-
style={{
|
|
111
|
-
width: 12,
|
|
112
|
-
height: 12,
|
|
113
|
-
background: isVisible ? P.white : P.textMuted,
|
|
114
|
-
transform: isVisible ? "translateX(14px)" : "translateX(2px)",
|
|
115
|
-
}}
|
|
116
|
-
/>
|
|
117
|
-
</button>
|
|
118
|
-
</div>
|
|
119
|
-
<RemoveButton onClick={onRemove} title={removeTitle} />
|
|
120
|
-
</div>
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (STRING_PROPS.has(prop)) {
|
|
125
|
-
const presets =
|
|
126
|
-
prop === "filter" ? FILTER_PRESETS : prop === "clipPath" ? CLIP_PATH_PRESETS : [];
|
|
127
|
-
return (
|
|
128
|
-
<div className="flex flex-col gap-1">
|
|
129
|
-
<div className="flex items-center gap-1">
|
|
130
|
-
<div className="min-w-0 flex-1 flex items-center gap-2 px-2 py-1 rounded-lg bg-neutral-900 border border-neutral-800">
|
|
131
|
-
<span className="flex-shrink-0 text-[11px] font-medium text-neutral-500">
|
|
132
|
-
{PROP_LABELS[prop] ?? prop}
|
|
133
|
-
</span>
|
|
134
|
-
<input
|
|
135
|
-
type="text"
|
|
136
|
-
defaultValue={String(val)}
|
|
137
|
-
className="flex-1 bg-transparent text-[11px] text-neutral-200 outline-none"
|
|
138
|
-
onBlur={(e) => onCommit(e.currentTarget.value)}
|
|
139
|
-
onKeyDown={(e) => {
|
|
140
|
-
if (e.key === "Enter") {
|
|
141
|
-
e.currentTarget.blur();
|
|
142
|
-
}
|
|
143
|
-
}}
|
|
144
|
-
/>
|
|
145
|
-
</div>
|
|
146
|
-
<RemoveButton onClick={onRemove} title={removeTitle} />
|
|
147
|
-
</div>
|
|
148
|
-
{presets.length > 0 && (
|
|
149
|
-
<div className="flex gap-1 pl-1">
|
|
150
|
-
{presets.map((p) => (
|
|
151
|
-
<button
|
|
152
|
-
key={p.value}
|
|
153
|
-
type="button"
|
|
154
|
-
onClick={() => onCommit(p.value)}
|
|
155
|
-
className="px-1.5 py-0.5 rounded text-[9px] font-medium text-neutral-500 bg-neutral-800/50 hover:bg-neutral-800 hover:text-neutral-300 transition-colors"
|
|
156
|
-
>
|
|
157
|
-
{p.label}
|
|
158
|
-
</button>
|
|
159
|
-
))}
|
|
160
|
-
</div>
|
|
161
|
-
)}
|
|
162
|
-
</div>
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return (
|
|
167
|
-
<div className="flex items-center gap-1">
|
|
168
|
-
<div className="min-w-0 flex-1">
|
|
169
|
-
<MetricField
|
|
170
|
-
label={PROP_LABELS[prop] ?? prop}
|
|
171
|
-
value={displayValue(prop, val)}
|
|
172
|
-
suffix={PROP_UNITS[prop]}
|
|
173
|
-
tooltip={PROP_TOOLTIPS[prop]}
|
|
174
|
-
scrub
|
|
175
|
-
liveCommit
|
|
176
|
-
onCommit={(raw) => onCommit(adjustedValue(prop, raw))}
|
|
177
|
-
/>
|
|
178
|
-
</div>
|
|
179
|
-
<RemoveButton onClick={onRemove} title={removeTitle} />
|
|
180
|
-
</div>
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function AddPropertyTrigger({
|
|
185
|
-
adding,
|
|
186
|
-
available,
|
|
187
|
-
addLabel,
|
|
188
|
-
addTitle,
|
|
189
|
-
onAdd,
|
|
190
|
-
onOpen,
|
|
191
|
-
onClose,
|
|
192
|
-
buttonClassName,
|
|
193
|
-
}: {
|
|
194
|
-
adding: boolean;
|
|
195
|
-
available: string[];
|
|
196
|
-
addLabel: string;
|
|
197
|
-
addTitle: string;
|
|
198
|
-
onAdd: (prop: string) => void;
|
|
199
|
-
onOpen: () => void;
|
|
200
|
-
onClose: () => void;
|
|
201
|
-
buttonClassName: string;
|
|
202
|
-
}) {
|
|
203
|
-
if (adding && available.length > 0) {
|
|
204
|
-
return (
|
|
205
|
-
<select
|
|
206
|
-
autoFocus
|
|
207
|
-
className="min-w-0 rounded-lg border border-neutral-700 bg-neutral-900 px-2 py-1 text-[11px] text-neutral-100 outline-none"
|
|
208
|
-
defaultValue=""
|
|
209
|
-
onChange={(e) => {
|
|
210
|
-
if (e.target.value) onAdd(e.target.value);
|
|
211
|
-
onClose();
|
|
212
|
-
}}
|
|
213
|
-
onBlur={onClose}
|
|
214
|
-
>
|
|
215
|
-
<option value="" disabled>
|
|
216
|
-
Choose property…
|
|
217
|
-
</option>
|
|
218
|
-
{available.map((p) => (
|
|
219
|
-
<option key={p} value={p}>
|
|
220
|
-
{PROP_LABELS[p] ?? p}
|
|
221
|
-
</option>
|
|
222
|
-
))}
|
|
223
|
-
</select>
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
if (available.length === 0) return null;
|
|
227
|
-
return (
|
|
228
|
-
<button type="button" onClick={onOpen} className={buttonClassName} title={addTitle}>
|
|
229
|
-
{addLabel}
|
|
230
|
-
</button>
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function parseNumericOrString(raw: string): number | string {
|
|
235
|
-
const num = Number(raw);
|
|
236
|
-
return Number.isFinite(num) ? num : raw;
|
|
237
|
-
}
|
|
13
|
+
import { KeyframeEaseList } from "./KeyframeEaseList";
|
|
14
|
+
import {
|
|
15
|
+
PropertyRow,
|
|
16
|
+
AddPropertyTrigger,
|
|
17
|
+
parseNumericOrString,
|
|
18
|
+
BOOLEAN_PROPS,
|
|
19
|
+
} from "./AnimationCardParts";
|
|
238
20
|
|
|
239
21
|
interface AnimationCardProps extends GsapAnimationEditCallbacks {
|
|
240
22
|
animation: GsapAnimation;
|
|
@@ -257,11 +39,13 @@ export const AnimationCard = memo(function AnimationCard({
|
|
|
257
39
|
onLivePreviewEnd,
|
|
258
40
|
onSetArcPath,
|
|
259
41
|
onUpdateArcSegment,
|
|
42
|
+
onUpdateKeyframeEase,
|
|
260
43
|
onUnroll,
|
|
261
44
|
}: AnimationCardProps) {
|
|
262
45
|
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
263
46
|
const [addingProp, setAddingProp] = useState(false);
|
|
264
47
|
const [addingFromProp, setAddingFromProp] = useState(false);
|
|
48
|
+
const [expandedKfPct, setExpandedKfPct] = useState<number | null>(null);
|
|
265
49
|
|
|
266
50
|
const usedProps = useMemo(
|
|
267
51
|
() => new Set(Object.keys(animation.properties)),
|
|
@@ -330,7 +114,8 @@ export const AnimationCard = memo(function AnimationCard({
|
|
|
330
114
|
const [copied, setCopied] = useState(false);
|
|
331
115
|
|
|
332
116
|
const methodLabel = METHOD_LABELS[animation.method] ?? animation.method;
|
|
333
|
-
const easeName =
|
|
117
|
+
const easeName =
|
|
118
|
+
(animation.keyframes ? animation.keyframes.easeEach : undefined) ?? animation.ease ?? "none";
|
|
334
119
|
const easeLabel = easeName.startsWith("custom(")
|
|
335
120
|
? "Custom curve"
|
|
336
121
|
: (EASE_LABELS[easeName] ?? easeName);
|
|
@@ -340,6 +125,28 @@ export const AnimationCard = memo(function AnimationCard({
|
|
|
340
125
|
: animation.position;
|
|
341
126
|
|
|
342
127
|
const summary = useMemo(() => buildTweenSummary(animation), [animation]);
|
|
128
|
+
const setKeys = Object.keys(animation.properties);
|
|
129
|
+
if (
|
|
130
|
+
animation.method === "set" &&
|
|
131
|
+
// `every` is vacuously true on an empty bag — require at least one key so a
|
|
132
|
+
// property-less set doesn't masquerade as a position row.
|
|
133
|
+
(setKeys.includes("x") || setKeys.includes("y")) &&
|
|
134
|
+
setKeys.every((k) => k === "x" || k === "y" || k === "immediateRender")
|
|
135
|
+
)
|
|
136
|
+
return (
|
|
137
|
+
<div className="border-b border-neutral-800 pb-2">
|
|
138
|
+
<div className="flex items-center gap-2 py-1.5">
|
|
139
|
+
<span className="rounded bg-neutral-800 px-1.5 py-0.5 text-[10px] font-medium text-neutral-400">
|
|
140
|
+
Position
|
|
141
|
+
</span>
|
|
142
|
+
<span className="text-[11px] text-neutral-500">
|
|
143
|
+
x: {Math.round(Number(animation.properties.x ?? 0))}, y:{" "}
|
|
144
|
+
{Math.round(Number(animation.properties.y ?? 0))}
|
|
145
|
+
</span>
|
|
146
|
+
<span className="ml-auto text-[9px] text-neutral-600">drag to move</span>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
343
150
|
|
|
344
151
|
return (
|
|
345
152
|
<div className="border-b border-neutral-800 pb-3">
|
|
@@ -393,7 +200,7 @@ export const AnimationCard = memo(function AnimationCard({
|
|
|
393
200
|
clipPath: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)",
|
|
394
201
|
}}
|
|
395
202
|
/>
|
|
396
|
-
Keyframed —
|
|
203
|
+
Keyframed — click a segment below to edit its curve
|
|
397
204
|
</p>
|
|
398
205
|
)}
|
|
399
206
|
</div>
|
|
@@ -435,29 +242,43 @@ export const AnimationCard = memo(function AnimationCard({
|
|
|
435
242
|
|
|
436
243
|
{animation.method !== "set" && (
|
|
437
244
|
<>
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
245
|
+
{animation.keyframes && onUpdateKeyframeEase ? (
|
|
246
|
+
<KeyframeEaseList
|
|
247
|
+
keyframes={animation.keyframes.keyframes}
|
|
248
|
+
globalEase={animation.keyframes.easeEach ?? animation.ease ?? "none"}
|
|
249
|
+
expandedPct={expandedKfPct}
|
|
250
|
+
onToggle={setExpandedKfPct}
|
|
251
|
+
onEaseCommit={(pct, ease) => onUpdateKeyframeEase(animation.id, pct, ease)}
|
|
252
|
+
/>
|
|
253
|
+
) : (
|
|
254
|
+
<>
|
|
255
|
+
<SelectField
|
|
256
|
+
label="Speed"
|
|
257
|
+
value={easeName.startsWith("custom(") ? "custom" : easeName}
|
|
258
|
+
options={[...SUPPORTED_EASES, "custom"]}
|
|
259
|
+
onChange={(next) => {
|
|
260
|
+
const easeKey = animation.keyframes ? "easeEach" : "ease";
|
|
261
|
+
if (next === "custom") {
|
|
262
|
+
const points = controlPointsForGsapEase(
|
|
263
|
+
easeName !== "none" ? easeName : "power2.out",
|
|
264
|
+
);
|
|
265
|
+
const path = `M0,0 C${points.x1},${points.y1} ${points.x2},${points.y2} 1,1`;
|
|
266
|
+
onUpdateMeta(animation.id, { [easeKey]: `custom(${path})` });
|
|
267
|
+
} else {
|
|
268
|
+
onUpdateMeta(animation.id, { [easeKey]: next });
|
|
269
|
+
}
|
|
270
|
+
}}
|
|
271
|
+
/>
|
|
272
|
+
<EaseCurveSection
|
|
273
|
+
ease={easeName}
|
|
274
|
+
duration={animation.duration}
|
|
275
|
+
onCustomEaseCommit={(customEase) => {
|
|
276
|
+
const easeKey = animation.keyframes ? "easeEach" : "ease";
|
|
277
|
+
onUpdateMeta(animation.id, { [easeKey]: customEase });
|
|
278
|
+
}}
|
|
279
|
+
/>
|
|
280
|
+
</>
|
|
281
|
+
)}
|
|
461
282
|
</>
|
|
462
283
|
)}
|
|
463
284
|
|