@hyperframes/studio 0.7.81 → 0.7.83
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-B32VouCm.js → hyperframes-player-DSRuJMfh.js} +1 -1
- package/dist/assets/{index-DCTAKfpa.js → index-BrWVfpCQ.js} +1 -1
- package/dist/assets/{index-B5u2_pSh.js → index-DRtvHA1J.js} +150 -150
- package/dist/assets/{index-HeUw0EKI.js → index-YZR6OfQ5.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/index.js +3057 -3101
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +2 -5
- package/src/components/StudioHeader.tsx +3 -18
- package/src/components/StudioRightPanel.tsx +14 -22
- package/src/components/TimelineToolbar.tsx +120 -132
- package/src/components/editor/EaseCurveSection.tsx +9 -90
- package/src/components/editor/EaseModeControls.tsx +110 -0
- package/src/components/editor/PropertyPanel.test.tsx +2 -3
- package/src/components/editor/PropertyPanel.tsx +7 -12
- package/src/components/editor/PropertyPanelFlat.tsx +0 -2
- package/src/components/editor/floatingPanel.test.ts +20 -1
- package/src/components/editor/floatingPanel.ts +15 -0
- package/src/components/editor/manualEditingAvailability.test.ts +13 -40
- package/src/components/editor/manualEditingAvailability.ts +0 -41
- package/src/components/editor/propertyPanel3dTransform.tsx +1 -2
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +1 -2
- package/src/components/nle/PreviewOverlays.tsx +8 -20
- package/src/components/renders/RenderQueue.test.tsx +62 -0
- package/src/components/renders/RenderQueue.tsx +60 -38
- package/src/components/renders/useRenderQueue.ts +5 -11
- package/src/components/sidebar/LeftSidebar.tsx +15 -22
- package/src/components/ui/Tooltip.tsx +25 -7
- package/src/hooks/useAppHotkeys.ts +1 -2
- package/src/hooks/useDomEditPreviewSync.ts +1 -2
- package/src/hooks/useDomEditWiring.ts +2 -3
- package/src/hooks/useDomSelection.ts +0 -27
- package/src/hooks/usePreviewInteraction.ts +2 -3
- package/src/hooks/useStudioContextValue.ts +3 -7
- package/src/player/components/Timeline.tsx +1 -2
- package/src/player/components/TimelineLanes.tsx +33 -35
- package/src/player/components/useAutoExpandKeyframedClips.ts +0 -2
- package/src/player/components/useTimelineTrackLayout.ts +1 -5
- package/src/utils/studioUrlState.test.ts +5 -8
- package/src/utils/studioUrlState.ts +1 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.83",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
"gsap": "^3.13.0",
|
|
47
47
|
"marked": "^14.1.4",
|
|
48
48
|
"mediabunny": "^1.45.3",
|
|
49
|
-
"@hyperframes/core": "0.7.
|
|
50
|
-
"@hyperframes/
|
|
51
|
-
"@hyperframes/
|
|
52
|
-
"@hyperframes/
|
|
53
|
-
"@hyperframes/
|
|
49
|
+
"@hyperframes/core": "0.7.83",
|
|
50
|
+
"@hyperframes/sdk": "0.7.83",
|
|
51
|
+
"@hyperframes/parsers": "0.7.83",
|
|
52
|
+
"@hyperframes/studio-server": "0.7.83",
|
|
53
|
+
"@hyperframes/player": "0.7.83"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/react": "19",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"vite": "^6.4.2",
|
|
66
66
|
"vitest": "^3.2.4",
|
|
67
67
|
"zustand": "^5.0.0",
|
|
68
|
-
"@hyperframes/producer": "0.7.
|
|
68
|
+
"@hyperframes/producer": "0.7.83"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -43,7 +43,6 @@ import {
|
|
|
43
43
|
import type { DomEditSelection } from "./components/editor/domEditing";
|
|
44
44
|
import { StudioHeader } from "./components/StudioHeader";
|
|
45
45
|
import { useGestureCommit } from "./hooks/useGestureCommit";
|
|
46
|
-
import { STUDIO_KEYFRAMES_ENABLED } from "./components/editor/manualEditingAvailability";
|
|
47
46
|
import { GestureTrailOverlay } from "./components/editor/GestureTrailOverlay";
|
|
48
47
|
import { StudioLeftSidebar } from "./components/StudioLeftSidebar";
|
|
49
48
|
import { EditorShell } from "./components/EditorShell";
|
|
@@ -263,9 +262,7 @@ export function StudioApp() {
|
|
|
263
262
|
onUngroupSelection: () => domEditSessionRef.current.handleUngroupSelection(),
|
|
264
263
|
activeCompPath,
|
|
265
264
|
forceReloadSdkSession: sdkHandle.forceReload,
|
|
266
|
-
onToggleRecording:
|
|
267
|
-
? () => handleToggleRecordingRef.current()
|
|
268
|
-
: undefined,
|
|
265
|
+
onToggleRecording: () => handleToggleRecordingRef.current(),
|
|
269
266
|
});
|
|
270
267
|
const sidebarTabRef = useRef({
|
|
271
268
|
select: (t: SidebarTab) => leftSidebarRef.current?.selectTab(t),
|
|
@@ -367,7 +364,7 @@ export function StudioApp() {
|
|
|
367
364
|
isGestureRecordingRef,
|
|
368
365
|
});
|
|
369
366
|
handleToggleRecordingRef.current = handleToggleRecording;
|
|
370
|
-
const recordingToggle =
|
|
367
|
+
const recordingToggle = handleToggleRecording;
|
|
371
368
|
const canvasRectRef = useRef<DOMRect | null>(null);
|
|
372
369
|
useLayoutEffect(() => {
|
|
373
370
|
if (gestureState !== "recording" || !previewIframe) {
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { useRef, type MouseEvent } from "react";
|
|
2
2
|
import { RotateCcw, RotateCw, Camera } from "../icons/SystemIcons";
|
|
3
|
-
import {
|
|
4
|
-
STUDIO_INSPECTOR_PANELS_ENABLED,
|
|
5
|
-
STUDIO_MANUAL_EDITING_DISABLED_TITLE,
|
|
6
|
-
} from "./editor/manualEditingAvailability";
|
|
7
3
|
import { getHistoryShortcutLabel } from "../utils/studioHelpers";
|
|
8
4
|
import { useStudioShellContext } from "../contexts/StudioContext";
|
|
9
5
|
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
@@ -328,16 +324,10 @@ export function StudioHeader({
|
|
|
328
324
|
<span>{capturing ? "Capturing…" : "Capture"}</span>
|
|
329
325
|
</a>
|
|
330
326
|
</Tooltip>
|
|
331
|
-
<Tooltip
|
|
332
|
-
label={
|
|
333
|
-
STUDIO_INSPECTOR_PANELS_ENABLED ? "Inspector" : STUDIO_MANUAL_EDITING_DISABLED_TITLE
|
|
334
|
-
}
|
|
335
|
-
side="bottom"
|
|
336
|
-
>
|
|
327
|
+
<Tooltip label="Inspector" side="bottom">
|
|
337
328
|
<button
|
|
338
329
|
type="button"
|
|
339
330
|
onClick={() => {
|
|
340
|
-
if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
|
|
341
331
|
if (rightCollapsed || !inspectorPanelActive) {
|
|
342
332
|
trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: false });
|
|
343
333
|
setRightPanelTab("design");
|
|
@@ -349,18 +339,13 @@ export function StudioHeader({
|
|
|
349
339
|
// the panel shouldn't deselect the element.
|
|
350
340
|
setRightCollapsed(true);
|
|
351
341
|
}}
|
|
352
|
-
disabled={!STUDIO_INSPECTOR_PANELS_ENABLED}
|
|
353
342
|
aria-pressed={inspectorButtonActive}
|
|
354
343
|
className={`h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border transition-colors active:scale-[0.98] ${
|
|
355
344
|
inspectorButtonActive
|
|
356
345
|
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
|
|
357
|
-
:
|
|
358
|
-
? "text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800 border-transparent"
|
|
359
|
-
: "cursor-not-allowed border-transparent text-neutral-700"
|
|
346
|
+
: "text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800 border-transparent"
|
|
360
347
|
}`}
|
|
361
|
-
aria-label=
|
|
362
|
-
STUDIO_INSPECTOR_PANELS_ENABLED ? "Inspector" : STUDIO_MANUAL_EDITING_DISABLED_TITLE
|
|
363
|
-
}
|
|
348
|
+
aria-label="Inspector"
|
|
364
349
|
>
|
|
365
350
|
<svg
|
|
366
351
|
width="12"
|
|
@@ -10,10 +10,7 @@ import { PanelTabButton } from "./PanelTabButton";
|
|
|
10
10
|
import { usePreviewVariablesStore } from "../hooks/previewVariablesStore";
|
|
11
11
|
import type { RenderJob } from "./renders/useRenderQueue";
|
|
12
12
|
import type { BlockParam } from "@hyperframes/core/registry";
|
|
13
|
-
import {
|
|
14
|
-
STUDIO_FLAT_INSPECTOR_ENABLED,
|
|
15
|
-
STUDIO_INSPECTOR_PANELS_ENABLED,
|
|
16
|
-
} from "./editor/manualEditingAvailability";
|
|
13
|
+
import { STUDIO_FLAT_INSPECTOR_ENABLED } from "./editor/manualEditingAvailability";
|
|
17
14
|
import type { Composition } from "@hyperframes/sdk";
|
|
18
15
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
19
16
|
import { useSlideshowPersist, type UseSlideshowPersistParams } from "../hooks/useSlideshowPersist";
|
|
@@ -229,8 +226,7 @@ export function StudioRightPanel({
|
|
|
229
226
|
setRightPanelTab,
|
|
230
227
|
});
|
|
231
228
|
const designPaneOpen = inspectorTabActive && rightInspectorPanes.design && designPanelActive;
|
|
232
|
-
const layersPaneOpen =
|
|
233
|
-
inspectorTabActive && rightInspectorPanes.layers && STUDIO_INSPECTOR_PANELS_ENABLED;
|
|
229
|
+
const layersPaneOpen = inspectorTabActive && rightInspectorPanes.layers;
|
|
234
230
|
|
|
235
231
|
const handleInspectorPaneButtonClick = (pane: "design" | "layers") => {
|
|
236
232
|
if (!inspectorTabActive) {
|
|
@@ -483,22 +479,18 @@ export function StudioRightPanel({
|
|
|
483
479
|
) : (
|
|
484
480
|
<>
|
|
485
481
|
<div className="flex min-w-0 items-center gap-1 overflow-hidden border-b border-neutral-800 px-3 py-2">
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
onClick={() => handleInspectorPaneButtonClick("layers")}
|
|
499
|
-
/>
|
|
500
|
-
</>
|
|
501
|
-
)}
|
|
482
|
+
<PanelTabButton
|
|
483
|
+
label="Design"
|
|
484
|
+
tooltip="Element styles and properties"
|
|
485
|
+
active={designPaneOpen}
|
|
486
|
+
onClick={() => handleInspectorPaneButtonClick("design")}
|
|
487
|
+
/>
|
|
488
|
+
<PanelTabButton
|
|
489
|
+
label="Layers"
|
|
490
|
+
tooltip="Composition layer stack"
|
|
491
|
+
active={layersPaneOpen}
|
|
492
|
+
onClick={() => handleInspectorPaneButtonClick("layers")}
|
|
493
|
+
/>
|
|
502
494
|
<PanelTabButton
|
|
503
495
|
label={renderJobs.length > 0 ? `Renders (${renderJobs.length})` : "Renders"}
|
|
504
496
|
tooltip="Render queue and exports"
|
|
@@ -15,10 +15,6 @@ import {
|
|
|
15
15
|
} from "../player/components/timelineZoom";
|
|
16
16
|
import { useTimelineZoom } from "../player/components/useTimelineZoom";
|
|
17
17
|
import { usePlayerStore, type TimelineElement } from "../player";
|
|
18
|
-
import {
|
|
19
|
-
STUDIO_KEYFRAMES_ENABLED,
|
|
20
|
-
STUDIO_RAZOR_TOOL_ENABLED,
|
|
21
|
-
} from "./editor/manualEditingAvailability";
|
|
22
18
|
import { Tooltip } from "./ui";
|
|
23
19
|
import { Scissors } from "../icons/SystemIcons";
|
|
24
20
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
@@ -135,7 +131,7 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
|
|
|
135
131
|
// Wire the "Add keyframe (K)" shortcut the toolbar advertises. Active only when
|
|
136
132
|
// there's a keyframeable selection; otherwise K stays JKL-pause in playback.
|
|
137
133
|
useKeyframeKeyboard({
|
|
138
|
-
enabled:
|
|
134
|
+
enabled: Boolean(onToggleKeyframe),
|
|
139
135
|
onAddKeyframe: onToggleKeyframe,
|
|
140
136
|
});
|
|
141
137
|
|
|
@@ -170,36 +166,32 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
|
|
|
170
166
|
<div className="border-b border-neutral-800/60">
|
|
171
167
|
<div className="flex items-center justify-between px-2 py-0.5">
|
|
172
168
|
<div className="flex items-center gap-0.5">
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
{/* Divider: tool-mode | editing-actions */}
|
|
200
|
-
<div aria-hidden="true" className="mx-1 h-4 w-px bg-neutral-800" />
|
|
201
|
-
</>
|
|
202
|
-
)}
|
|
169
|
+
<Tooltip label="Selection tool (V)">
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
onClick={() => setActiveTool("select")}
|
|
173
|
+
aria-label="Selection tool"
|
|
174
|
+
aria-pressed={activeTool === "select"}
|
|
175
|
+
className={activeTool === "select" ? flatActive : flatIdle}
|
|
176
|
+
>
|
|
177
|
+
<svg width="16" height="16" viewBox="0 0 12 12" fill="currentColor">
|
|
178
|
+
<path d="M2 0.5L10 6L6.5 6.5L8.5 11L6.5 11.5L4.5 7L2 9Z" />
|
|
179
|
+
</svg>
|
|
180
|
+
</button>
|
|
181
|
+
</Tooltip>
|
|
182
|
+
<Tooltip label="Razor tool (B) — Shift+click splits all tracks">
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
onClick={() => setActiveTool("razor")}
|
|
186
|
+
aria-label="Razor tool"
|
|
187
|
+
aria-pressed={activeTool === "razor"}
|
|
188
|
+
className={activeTool === "razor" ? flatActive : flatIdle}
|
|
189
|
+
>
|
|
190
|
+
<Scissors size={16} />
|
|
191
|
+
</button>
|
|
192
|
+
</Tooltip>
|
|
193
|
+
{/* Divider: tool-mode | editing-actions */}
|
|
194
|
+
<div aria-hidden="true" className="mx-1 h-4 w-px bg-neutral-800" />
|
|
203
195
|
<Tooltip label={timelineSnapEnabled ? "Snapping on (N)" : "Snapping off (N)"}>
|
|
204
196
|
<button
|
|
205
197
|
type="button"
|
|
@@ -211,111 +203,107 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
|
|
|
211
203
|
<Magnet size={16} weight="bold" aria-hidden="true" />
|
|
212
204
|
</button>
|
|
213
205
|
</Tooltip>
|
|
214
|
-
{
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
?
|
|
226
|
-
? "Extend motion path to playhead (K)"
|
|
227
|
-
: keyframeState === "active"
|
|
228
|
-
? "Remove waypoint from motion path (K)"
|
|
229
|
-
: "Add waypoint to motion path (K)"
|
|
206
|
+
{/* Always rendered (CapCut-style): with no keyframeable selection the
|
|
207
|
+
button fades to a disabled state instead of unmounting, so the
|
|
208
|
+
toolbar layout never shifts. */}
|
|
209
|
+
<Tooltip
|
|
210
|
+
label={
|
|
211
|
+
keyframePathEndpoint
|
|
212
|
+
? "Motion path endpoints cannot be removed"
|
|
213
|
+
: !onToggleKeyframe
|
|
214
|
+
? "Select an animated element to add keyframes"
|
|
215
|
+
: keyframeIsMotionPath
|
|
216
|
+
? keyframeWillExtend
|
|
217
|
+
? "Extend motion path to playhead (K)"
|
|
230
218
|
: keyframeState === "active"
|
|
231
|
-
? "Remove
|
|
219
|
+
? "Remove waypoint from motion path (K)"
|
|
220
|
+
: "Add waypoint to motion path (K)"
|
|
221
|
+
: keyframeState === "active"
|
|
222
|
+
? "Remove keyframe at playhead (K)"
|
|
223
|
+
: keyframeState === "inactive"
|
|
224
|
+
? keyframeWillExtend
|
|
225
|
+
? "Add keyframe at playhead, extends animation (K)"
|
|
226
|
+
: "Add keyframe at playhead (K)"
|
|
227
|
+
: "Add keyframe (K)"
|
|
228
|
+
}
|
|
229
|
+
>
|
|
230
|
+
<button
|
|
231
|
+
type="button"
|
|
232
|
+
disabled={!onToggleKeyframe}
|
|
233
|
+
onClick={onToggleKeyframe}
|
|
234
|
+
aria-label={
|
|
235
|
+
keyframePathEndpoint
|
|
236
|
+
? "Motion path endpoint"
|
|
237
|
+
: keyframeIsMotionPath
|
|
238
|
+
? keyframeState === "active"
|
|
239
|
+
? "Remove motion path waypoint"
|
|
240
|
+
: keyframeWillExtend
|
|
241
|
+
? "Extend motion path to playhead"
|
|
242
|
+
: "Add motion path waypoint"
|
|
243
|
+
: keyframeState === "active"
|
|
244
|
+
? "Remove keyframe at playhead"
|
|
245
|
+
: "Add keyframe at playhead"
|
|
246
|
+
}
|
|
247
|
+
className={
|
|
248
|
+
!onToggleKeyframe
|
|
249
|
+
? flatDisabled
|
|
250
|
+
: `${flatBtn} active:scale-[0.98] hover:bg-white/[0.06] ${
|
|
251
|
+
keyframeState === "active"
|
|
252
|
+
? "text-studio-accent"
|
|
232
253
|
: keyframeState === "inactive"
|
|
233
|
-
?
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
: "Add keyframe (K)"
|
|
254
|
+
? "text-neutral-400 hover:text-studio-accent"
|
|
255
|
+
: "text-neutral-600 hover:text-neutral-400"
|
|
256
|
+
}`
|
|
237
257
|
}
|
|
238
258
|
>
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
>
|
|
268
|
-
<svg width="16" height="16" viewBox="0 0 10 10" fill="currentColor">
|
|
269
|
-
{keyframeState === "active" ? (
|
|
270
|
-
<path d="M5 0.5L9.5 5L5 9.5L0.5 5Z" />
|
|
271
|
-
) : (
|
|
272
|
-
<path
|
|
273
|
-
d="M5 1.2L8.8 5L5 8.8L1.2 5Z"
|
|
274
|
-
fill="none"
|
|
275
|
-
stroke="currentColor"
|
|
276
|
-
strokeWidth="1.2"
|
|
277
|
-
/>
|
|
278
|
-
)}
|
|
279
|
-
</svg>
|
|
280
|
-
</button>
|
|
281
|
-
</Tooltip>
|
|
282
|
-
)}
|
|
283
|
-
{STUDIO_KEYFRAMES_ENABLED && (
|
|
284
|
-
<Tooltip
|
|
285
|
-
label={
|
|
259
|
+
<svg width="16" height="16" viewBox="0 0 10 10" fill="currentColor">
|
|
260
|
+
{keyframeState === "active" ? (
|
|
261
|
+
<path d="M5 0.5L9.5 5L5 9.5L0.5 5Z" />
|
|
262
|
+
) : (
|
|
263
|
+
<path
|
|
264
|
+
d="M5 1.2L8.8 5L5 8.8L1.2 5Z"
|
|
265
|
+
fill="none"
|
|
266
|
+
stroke="currentColor"
|
|
267
|
+
strokeWidth="1.2"
|
|
268
|
+
/>
|
|
269
|
+
)}
|
|
270
|
+
</svg>
|
|
271
|
+
</button>
|
|
272
|
+
</Tooltip>
|
|
273
|
+
<Tooltip
|
|
274
|
+
label={
|
|
275
|
+
autoKeyframeEnabled
|
|
276
|
+
? "Auto-record manual edits as keyframes (click to turn off)"
|
|
277
|
+
: "Manual edits will not be recorded as keyframes (click to turn on)"
|
|
278
|
+
}
|
|
279
|
+
>
|
|
280
|
+
<button
|
|
281
|
+
type="button"
|
|
282
|
+
onClick={() => setAutoKeyframeEnabled(!autoKeyframeEnabled)}
|
|
283
|
+
aria-label="Auto-record manual edits as keyframes"
|
|
284
|
+
aria-pressed={autoKeyframeEnabled}
|
|
285
|
+
className={`${flatBtn} active:scale-[0.98] hover:bg-white/[0.06] ${
|
|
286
286
|
autoKeyframeEnabled
|
|
287
|
-
? "
|
|
288
|
-
: "
|
|
289
|
-
}
|
|
287
|
+
? "text-red-400 hover:text-red-300"
|
|
288
|
+
: "text-neutral-600 hover:text-neutral-400"
|
|
289
|
+
}`}
|
|
290
290
|
>
|
|
291
|
-
<
|
|
292
|
-
|
|
293
|
-
onClick={() => setAutoKeyframeEnabled(!autoKeyframeEnabled)}
|
|
294
|
-
aria-label="Auto-record manual edits as keyframes"
|
|
295
|
-
aria-pressed={autoKeyframeEnabled}
|
|
296
|
-
className={`${flatBtn} active:scale-[0.98] hover:bg-white/[0.06] ${
|
|
297
|
-
autoKeyframeEnabled
|
|
298
|
-
? "text-red-400 hover:text-red-300"
|
|
299
|
-
: "text-neutral-600 hover:text-neutral-400"
|
|
300
|
-
}`}
|
|
301
|
-
>
|
|
302
|
-
<svg width="16" height="16" viewBox="0 0 10 10" fill="none">
|
|
303
|
-
{/* Same diamond outline as the Add-keyframe icon, with a
|
|
291
|
+
<svg width="16" height="16" viewBox="0 0 10 10" fill="none">
|
|
292
|
+
{/* Same diamond outline as the Add-keyframe icon, with a
|
|
304
293
|
record-style dot inside: filled = auto-recording,
|
|
305
294
|
hollow = manual edits won't be keyframed. */}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
)}
|
|
295
|
+
<path d="M5 0.7L9.3 5L5 9.3L0.7 5Z" stroke="currentColor" strokeWidth="1" />
|
|
296
|
+
<circle
|
|
297
|
+
cx="5"
|
|
298
|
+
cy="5"
|
|
299
|
+
r="1.8"
|
|
300
|
+
fill={autoKeyframeEnabled ? "currentColor" : "none"}
|
|
301
|
+
stroke="currentColor"
|
|
302
|
+
strokeWidth="1"
|
|
303
|
+
/>
|
|
304
|
+
</svg>
|
|
305
|
+
</button>
|
|
306
|
+
</Tooltip>
|
|
319
307
|
{onSplitElement &&
|
|
320
308
|
(() => {
|
|
321
309
|
// Render the button unconditionally (disabled when unusable):
|
|
@@ -5,7 +5,15 @@ import {
|
|
|
5
5
|
parseWiggleEase,
|
|
6
6
|
type WiggleEaseConfig,
|
|
7
7
|
} from "@hyperframes/core/wiggle-ease";
|
|
8
|
-
import {
|
|
8
|
+
import { easePresetLabel } from "./easePresetLibrary";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_CURVE,
|
|
11
|
+
MODE_LABELS,
|
|
12
|
+
EaseModeToggle,
|
|
13
|
+
EasePresetGrid,
|
|
14
|
+
type EaseMode,
|
|
15
|
+
type Pts,
|
|
16
|
+
} from "./EaseModeControls";
|
|
9
17
|
import { holdCurvePath, MiniCurveSvg, sampledPath } from "./easeCurveSvg";
|
|
10
18
|
import { EaseBezierField, SpringBounceField, WiggleField } from "./EaseParamFields";
|
|
11
19
|
import { EASE_CURVES, EASE_LABELS, resolveEaseCurveTuple } from "./gsapAnimationConstants";
|
|
@@ -14,51 +22,6 @@ import type { AnimationKeyframeTarget } from "../../hooks/gsapTweenSynth";
|
|
|
14
22
|
|
|
15
23
|
export { MiniCurveSvg } from "./easeCurveSvg";
|
|
16
24
|
|
|
17
|
-
const EASE_MODES = ["curve", "spring", "wiggle"] as const;
|
|
18
|
-
type EaseMode = (typeof EASE_MODES)[number];
|
|
19
|
-
|
|
20
|
-
const EasePresetGrid = function EasePresetGrid({
|
|
21
|
-
kind,
|
|
22
|
-
currentEase,
|
|
23
|
-
onSelect,
|
|
24
|
-
}: {
|
|
25
|
-
kind: EaseMode;
|
|
26
|
-
currentEase: string;
|
|
27
|
-
onSelect: (ease: string) => void;
|
|
28
|
-
}) {
|
|
29
|
-
return (
|
|
30
|
-
<div className="mb-2 grid max-h-56 grid-cols-4 gap-1 overflow-y-auto pr-0.5">
|
|
31
|
-
{EASE_PRESETS.filter((preset) => preset.kind === kind).map((preset) => {
|
|
32
|
-
const isActive = currentEase === preset.ease;
|
|
33
|
-
return (
|
|
34
|
-
<button
|
|
35
|
-
key={preset.id}
|
|
36
|
-
type="button"
|
|
37
|
-
data-ease-preset-id={preset.id}
|
|
38
|
-
role="menuitemradio"
|
|
39
|
-
aria-checked={isActive}
|
|
40
|
-
tabIndex={isActive ? 0 : -1}
|
|
41
|
-
onClick={() => onSelect(preset.ease)}
|
|
42
|
-
className={`flex flex-col items-center gap-0.5 rounded-md p-1 transition-colors ${
|
|
43
|
-
isActive ? "bg-panel-accent/10 ring-1 ring-panel-accent/30" : "hover:bg-neutral-800"
|
|
44
|
-
}`}
|
|
45
|
-
title={preset.label}
|
|
46
|
-
>
|
|
47
|
-
<MiniCurveSvg ease={preset.ease} active={isActive} />
|
|
48
|
-
<span
|
|
49
|
-
className={`text-center text-[8px] leading-none ${
|
|
50
|
-
isActive ? "text-panel-accent" : "text-neutral-500"
|
|
51
|
-
}`}
|
|
52
|
-
>
|
|
53
|
-
{preset.label}
|
|
54
|
-
</span>
|
|
55
|
-
</button>
|
|
56
|
-
);
|
|
57
|
-
})}
|
|
58
|
-
</div>
|
|
59
|
-
);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
25
|
const round2 = roundToCenti;
|
|
63
26
|
|
|
64
27
|
// ── Graph geometry (Figma-style easing box) ─────────────────────────────────
|
|
@@ -81,50 +44,6 @@ const DRAG_VMAX = 2;
|
|
|
81
44
|
const DRAG_VMIN = -1;
|
|
82
45
|
const ACCENT = "#3CE6AC";
|
|
83
46
|
|
|
84
|
-
type Pts = [number, number, number, number];
|
|
85
|
-
const DEFAULT_CURVE: Pts = EASE_CURVES["power2.out"];
|
|
86
|
-
const MODE_LABELS = { curve: "Curve", spring: "Spring", wiggle: "Wiggle" } satisfies Record<
|
|
87
|
-
EaseMode,
|
|
88
|
-
string
|
|
89
|
-
>;
|
|
90
|
-
const DEFAULT_EASE_BY_MODE = {
|
|
91
|
-
curve: `custom(M0,0 C${DEFAULT_CURVE[0]},${DEFAULT_CURVE[1]} ${DEFAULT_CURVE[2]},${DEFAULT_CURVE[3]} 1,1)`,
|
|
92
|
-
spring: "spring(0.42)",
|
|
93
|
-
wiggle: "wiggle(3,easeInOut,0.12)",
|
|
94
|
-
} satisfies Record<EaseMode, string>;
|
|
95
|
-
|
|
96
|
-
function EaseModeToggle({ mode, onCommit }: { mode: EaseMode; onCommit: (ease: string) => void }) {
|
|
97
|
-
return (
|
|
98
|
-
<div
|
|
99
|
-
className="mb-2 grid grid-cols-3 rounded-md bg-black/20 p-0.5"
|
|
100
|
-
role="radiogroup"
|
|
101
|
-
aria-label="Ease editor mode"
|
|
102
|
-
>
|
|
103
|
-
{EASE_MODES.map((candidateMode) => {
|
|
104
|
-
const active = candidateMode === mode;
|
|
105
|
-
return (
|
|
106
|
-
<button
|
|
107
|
-
key={candidateMode}
|
|
108
|
-
type="button"
|
|
109
|
-
data-ease-mode={candidateMode}
|
|
110
|
-
role="radio"
|
|
111
|
-
aria-checked={active}
|
|
112
|
-
onClick={() => {
|
|
113
|
-
if (active) return;
|
|
114
|
-
onCommit(DEFAULT_EASE_BY_MODE[candidateMode]);
|
|
115
|
-
}}
|
|
116
|
-
className={`rounded px-2 py-1 text-[10px] font-medium transition-colors ${
|
|
117
|
-
active ? "bg-neutral-700 text-neutral-100" : "text-neutral-500 hover:text-neutral-300"
|
|
118
|
-
}`}
|
|
119
|
-
>
|
|
120
|
-
{MODE_LABELS[candidateMode]}
|
|
121
|
-
</button>
|
|
122
|
-
);
|
|
123
|
-
})}
|
|
124
|
-
</div>
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
47
|
// Figma-style ease-type dropdown: the current ease (glyph + name) as a button
|
|
129
48
|
// that opens the preset grid in a popover. This is where a preset is selected —
|
|
130
49
|
// the grid is no longer shown inline.
|