@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
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/
|
|
50
|
-
"@hyperframes/
|
|
48
|
+
"@hyperframes/player": "0.7.8",
|
|
49
|
+
"@hyperframes/core": "0.7.8",
|
|
50
|
+
"@hyperframes/sdk": "0.7.8"
|
|
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.8"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"react": "19",
|
|
@@ -118,10 +118,12 @@ export function StudioRightPanel({
|
|
|
118
118
|
handleGsapAddFromProperty,
|
|
119
119
|
handleGsapRemoveFromProperty,
|
|
120
120
|
commitAnimatedProperty,
|
|
121
|
+
commitAnimatedProperties,
|
|
121
122
|
handleSetArcPath,
|
|
122
123
|
handleUpdateArcSegment,
|
|
123
124
|
handleUnroll,
|
|
124
125
|
handleUpdateKeyframeEase,
|
|
126
|
+
handleSetAllKeyframeEases,
|
|
125
127
|
handleGsapAddKeyframe,
|
|
126
128
|
handleGsapRemoveKeyframe,
|
|
127
129
|
handleGsapConvertToKeyframes,
|
|
@@ -268,14 +270,18 @@ export function StudioRightPanel({
|
|
|
268
270
|
onRemoveGsapFromProperty={handleGsapRemoveFromProperty}
|
|
269
271
|
onAddGsapAnimation={handleGsapAddAnimation}
|
|
270
272
|
onCommitAnimatedProperty={commitAnimatedProperty}
|
|
273
|
+
onCommitAnimatedProperties={commitAnimatedProperties}
|
|
271
274
|
onAddKeyframe={handleGsapAddKeyframe}
|
|
272
275
|
onRemoveKeyframe={handleGsapRemoveKeyframe}
|
|
273
|
-
onConvertToKeyframes={
|
|
276
|
+
onConvertToKeyframes={(animId, duration) =>
|
|
277
|
+
handleGsapConvertToKeyframes(animId, undefined, duration)
|
|
278
|
+
}
|
|
274
279
|
onSeekToTime={(t) => usePlayerStore.getState().requestSeek(t)}
|
|
275
280
|
onSetArcPath={handleSetArcPath}
|
|
276
281
|
onUpdateArcSegment={handleUpdateArcSegment}
|
|
277
282
|
onUnroll={handleUnroll}
|
|
278
283
|
onUpdateKeyframeEase={handleUpdateKeyframeEase}
|
|
284
|
+
onSetAllKeyframeEases={handleSetAllKeyframeEases}
|
|
279
285
|
recordingState={recordingState}
|
|
280
286
|
recordingDuration={recordingDuration}
|
|
281
287
|
onToggleRecording={onToggleRecording}
|
|
@@ -40,6 +40,7 @@ export const AnimationCard = memo(function AnimationCard({
|
|
|
40
40
|
onSetArcPath,
|
|
41
41
|
onUpdateArcSegment,
|
|
42
42
|
onUpdateKeyframeEase,
|
|
43
|
+
onSetAllKeyframeEases,
|
|
43
44
|
onUnroll,
|
|
44
45
|
}: AnimationCardProps) {
|
|
45
46
|
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
@@ -249,6 +250,11 @@ export const AnimationCard = memo(function AnimationCard({
|
|
|
249
250
|
expandedPct={expandedKfPct}
|
|
250
251
|
onToggle={setExpandedKfPct}
|
|
251
252
|
onEaseCommit={(pct, ease) => onUpdateKeyframeEase(animation.id, pct, ease)}
|
|
253
|
+
onApplyAll={
|
|
254
|
+
onSetAllKeyframeEases
|
|
255
|
+
? (ease) => onSetAllKeyframeEases(animation.id, ease)
|
|
256
|
+
: undefined
|
|
257
|
+
}
|
|
252
258
|
/>
|
|
253
259
|
) : (
|
|
254
260
|
<>
|
|
@@ -2,6 +2,7 @@ import { memo, useEffect, useMemo, useRef, useState, type RefObject } from "reac
|
|
|
2
2
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
3
3
|
import { type DomEditSelection } from "./domEditing";
|
|
4
4
|
import { useMarqueeGestures } from "./marqueeCommit";
|
|
5
|
+
import { MarqueeOverlay } from "./MarqueeOverlay";
|
|
5
6
|
import { resolveDomEditGroupOverlayRect, toOverlayRect } from "./domEditOverlayGeometry";
|
|
6
7
|
import { collectDomEditLayerItems } from "./domEditingLayers";
|
|
7
8
|
import { isElementComputedVisible } from "./domEditingElement";
|
|
@@ -101,6 +102,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
101
102
|
const onMarqueeSelectRef = useRef(onMarqueeSelect);
|
|
102
103
|
onMarqueeSelectRef.current = onMarqueeSelect;
|
|
103
104
|
|
|
105
|
+
// fallow-ignore-next-line complexity
|
|
104
106
|
const selectionShapeStyles = (() => {
|
|
105
107
|
const fallback = {
|
|
106
108
|
borderRadius: 8 as string | number,
|
|
@@ -226,6 +228,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
226
228
|
// outside the composition bounds so users can find them.
|
|
227
229
|
const offCanvasElementsRef = useRef<Map<string, HTMLElement>>(new Map());
|
|
228
230
|
const [offCanvasRects, setOffCanvasRects] = useState<OffCanvasRect[]>([]);
|
|
231
|
+
// fallow-ignore-next-line complexity
|
|
229
232
|
useEffect(() => {
|
|
230
233
|
const iframe = iframeRef.current;
|
|
231
234
|
const overlay = overlayRef.current;
|
|
@@ -568,18 +571,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
568
571
|
activeCompositionPathRef={activeCompositionPathRef}
|
|
569
572
|
onSelectionChangeRef={onSelectionChangeRef}
|
|
570
573
|
/>
|
|
571
|
-
{marquee.marqueeRect
|
|
572
|
-
<div
|
|
573
|
-
aria-hidden="true"
|
|
574
|
-
className="pointer-events-none absolute border border-dashed border-studio-accent bg-studio-accent/10"
|
|
575
|
-
style={{
|
|
576
|
-
left: marquee.marqueeRect.left,
|
|
577
|
-
top: marquee.marqueeRect.top,
|
|
578
|
-
width: marquee.marqueeRect.width,
|
|
579
|
-
height: marquee.marqueeRect.height,
|
|
580
|
-
}}
|
|
581
|
-
/>
|
|
582
|
-
)}
|
|
574
|
+
<MarqueeOverlay candidateRects={marquee.candidateRects} marqueeRect={marquee.marqueeRect} />
|
|
583
575
|
<GridOverlay
|
|
584
576
|
visible={gridVisible}
|
|
585
577
|
spacing={gridSpacing}
|
|
@@ -2,14 +2,16 @@ import { useCallback, useRef, useState } from "react";
|
|
|
2
2
|
import { EASE_CURVES, EASE_LABELS, parseCustomEaseFromString } from "./gsapAnimationConstants";
|
|
3
3
|
import { roundToCenti } from "../../utils/rounding";
|
|
4
4
|
|
|
5
|
+
// Figma-canonical ordering: linear, the three core eases, then the expressive
|
|
6
|
+
// (back / snappy) family. Each maps to a GSAP ease so it round-trips cleanly.
|
|
5
7
|
const PRESET_GRID_EASES = [
|
|
6
|
-
"ae-ease",
|
|
7
|
-
"ae-ease-in",
|
|
8
|
-
"ae-ease-out",
|
|
9
8
|
"none",
|
|
10
|
-
"power2.out",
|
|
11
9
|
"power2.in",
|
|
10
|
+
"power2.out",
|
|
11
|
+
"power2.inOut",
|
|
12
|
+
"back.in",
|
|
12
13
|
"back.out",
|
|
14
|
+
"back.inOut",
|
|
13
15
|
"expo.out",
|
|
14
16
|
] as const;
|
|
15
17
|
|
|
@@ -78,6 +80,37 @@ const EasePresetGrid = function EasePresetGrid({
|
|
|
78
80
|
|
|
79
81
|
const round2 = roundToCenti;
|
|
80
82
|
|
|
83
|
+
// ── Graph geometry (Figma-style easing box) ─────────────────────────────────
|
|
84
|
+
// A geometrically-square unit plot ([0,1]×[0,1], equal X/Y scale so the curve
|
|
85
|
+
// isn't distorted), with fixed overshoot headroom above 1 and below 0 for
|
|
86
|
+
// back/elastic eases. The view is fixed (no per-curve zoom); handles are clamped
|
|
87
|
+
// to the visible range so they never drift off-screen.
|
|
88
|
+
const S = 184; // side of the unit (0..1) square, in viewBox units
|
|
89
|
+
const HR = 52; // overshoot headroom (top & bottom)
|
|
90
|
+
const PADH = 16; // horizontal breathing room
|
|
91
|
+
const SVGW = S + PADH * 2;
|
|
92
|
+
const SVGH = S + HR * 2;
|
|
93
|
+
const VMAX = 1 + HR / S; // top of visible view (progress overshoot headroom)
|
|
94
|
+
const VMIN = -HR / S; // bottom of visible view (undershoot headroom)
|
|
95
|
+
// Committed control points may extend PAST the visible view — heavy back/elastic
|
|
96
|
+
// presets reach ~1.55 / -0.55. Dragging clamps to this wider bound (cursor can
|
|
97
|
+
// leave the box via pointer capture) so those curves keep their fidelity instead
|
|
98
|
+
// of snapping to the view edge; the handle DOT is still clampView'd into view.
|
|
99
|
+
const DRAG_VMAX = 2;
|
|
100
|
+
const DRAG_VMIN = -1;
|
|
101
|
+
const ACCENT = "#3CE6AC";
|
|
102
|
+
|
|
103
|
+
type Pts = [number, number, number, number];
|
|
104
|
+
|
|
105
|
+
const xToSvg = (px: number) => PADH + S * px;
|
|
106
|
+
const yToSvg = (py: number) => HR + S * (1 - py);
|
|
107
|
+
const clampView = (py: number) => Math.max(VMIN, Math.min(VMAX, py));
|
|
108
|
+
|
|
109
|
+
function cubicAt(t: number, c0: number, c1: number, c2: number, c3: number): number {
|
|
110
|
+
const mt = 1 - t;
|
|
111
|
+
return mt * mt * mt * c0 + 3 * mt * mt * t * c1 + 3 * mt * t * t * c2 + t * t * t * c3;
|
|
112
|
+
}
|
|
113
|
+
|
|
81
114
|
export function EaseCurveSection({
|
|
82
115
|
ease,
|
|
83
116
|
duration,
|
|
@@ -90,25 +123,26 @@ export function EaseCurveSection({
|
|
|
90
123
|
const isCustom = ease.startsWith("custom(");
|
|
91
124
|
const curveFromPreset = EASE_CURVES[ease];
|
|
92
125
|
const customPoints = isCustom ? parseCustomEaseFromString(ease) : null;
|
|
93
|
-
const curve:
|
|
126
|
+
const curve: Pts | null =
|
|
94
127
|
isCustom && customPoints
|
|
95
128
|
? [customPoints.x1, customPoints.y1, customPoints.x2, customPoints.y2]
|
|
96
129
|
: (curveFromPreset ?? null);
|
|
97
130
|
|
|
98
|
-
const [draft, setDraft] = useState<
|
|
131
|
+
const [draft, setDraft] = useState<Pts | null>(null);
|
|
99
132
|
const [progress, setProgress] = useState<number | null>(null);
|
|
133
|
+
const [hover, setHover] = useState<"p1" | "p2" | null>(null);
|
|
100
134
|
const draggingRef = useRef<"p1" | "p2" | null>(null);
|
|
101
135
|
const svgRef = useRef<SVGSVGElement | null>(null);
|
|
102
136
|
const rafRef = useRef<number>(0);
|
|
103
137
|
|
|
104
138
|
const play = useCallback(() => {
|
|
105
139
|
const start = performance.now();
|
|
106
|
-
const dur =
|
|
140
|
+
const dur = 1100;
|
|
107
141
|
const tick = (now: number) => {
|
|
108
142
|
const t = Math.min((now - start) / dur, 1);
|
|
109
143
|
setProgress(t);
|
|
110
144
|
if (t < 1) rafRef.current = requestAnimationFrame(tick);
|
|
111
|
-
else setTimeout(() => setProgress(null),
|
|
145
|
+
else setTimeout(() => setProgress(null), 450);
|
|
112
146
|
};
|
|
113
147
|
cancelAnimationFrame(rafRef.current);
|
|
114
148
|
rafRef.current = requestAnimationFrame(tick);
|
|
@@ -118,27 +152,23 @@ export function EaseCurveSection({
|
|
|
118
152
|
if (!active) return null;
|
|
119
153
|
const [x1, y1, x2, y2] = active;
|
|
120
154
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
155
|
+
// Anchors + control handles. Handle *display* is clamped to the view so an
|
|
156
|
+
// extreme loaded overshoot rides the edge instead of disappearing.
|
|
157
|
+
const a0 = { x: xToSvg(0), y: yToSvg(0) };
|
|
158
|
+
const a1 = { x: xToSvg(1), y: yToSvg(1) };
|
|
159
|
+
const p1 = { x: xToSvg(x1), y: yToSvg(clampView(y1)) };
|
|
160
|
+
const p2 = { x: xToSvg(x2), y: yToSvg(clampView(y2)) };
|
|
161
|
+
// Curve drawn from the true control points (so its shape is exact).
|
|
162
|
+
const cp1 = { x: xToSvg(x1), y: yToSvg(y1) };
|
|
163
|
+
const cp2 = { x: xToSvg(x2), y: yToSvg(y2) };
|
|
164
|
+
const curvePath = `M${a0.x},${a0.y} C${cp1.x},${cp1.y} ${cp2.x},${cp2.y} ${a1.x},${a1.y}`;
|
|
131
165
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
let dotX = pad;
|
|
135
|
-
let dotY = h - pad;
|
|
166
|
+
let dot: { x: number; y: number } | null = null;
|
|
136
167
|
if (progress !== null) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
h - pad - gh * (mt * mt * mt * 0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t);
|
|
168
|
+
dot = {
|
|
169
|
+
x: xToSvg(cubicAt(progress, 0, x1, x2, 1)),
|
|
170
|
+
y: yToSvg(cubicAt(progress, 0, y1, y2, 1)),
|
|
171
|
+
};
|
|
142
172
|
}
|
|
143
173
|
|
|
144
174
|
const handlePointerDown = (handle: "p1" | "p2", e: React.PointerEvent) => {
|
|
@@ -153,12 +183,16 @@ export function EaseCurveSection({
|
|
|
153
183
|
if (!draggingRef.current || !svgRef.current) return;
|
|
154
184
|
e.preventDefault();
|
|
155
185
|
const rect = svgRef.current.getBoundingClientRect();
|
|
156
|
-
const sx = ((e.clientX - rect.left) / rect.width) *
|
|
157
|
-
const sy = ((e.clientY - rect.top) / rect.height) *
|
|
158
|
-
|
|
159
|
-
|
|
186
|
+
const sx = ((e.clientX - rect.left) / rect.width) * SVGW;
|
|
187
|
+
const sy = ((e.clientY - rect.top) / rect.height) * SVGH;
|
|
188
|
+
// px is clamped to [0,1] on purpose: a cubic-bezier ease must be monotonic in
|
|
189
|
+
// time (handle1.x ≤ handle2.x), so handles can't pass each other or invert.
|
|
190
|
+
const px = Math.max(0, Math.min(1, (sx - PADH) / S));
|
|
191
|
+
// py uses the WIDER drag bound (not clampView), so dragging keeps overshoot
|
|
192
|
+
// fidelity instead of pinning the committed value to the visible view edge.
|
|
193
|
+
const py = Math.max(DRAG_VMIN, Math.min(DRAG_VMAX, 1 - (sy - HR) / S));
|
|
160
194
|
const prev = draft ?? [x1, y1, x2, y2];
|
|
161
|
-
const next:
|
|
195
|
+
const next: Pts =
|
|
162
196
|
draggingRef.current === "p1"
|
|
163
197
|
? [round2(px), round2(py), prev[2], prev[3]]
|
|
164
198
|
: [prev[0], prev[1], round2(px), round2(py)];
|
|
@@ -173,11 +207,12 @@ export function EaseCurveSection({
|
|
|
173
207
|
setDraft(null);
|
|
174
208
|
};
|
|
175
209
|
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
const
|
|
210
|
+
const top = yToSvg(1);
|
|
211
|
+
const bottom = yToSvg(0);
|
|
212
|
+
const left = xToSvg(0);
|
|
213
|
+
const right = xToSvg(1);
|
|
180
214
|
const label = isCustom ? "Custom curve" : (EASE_LABELS[ease] ?? ease);
|
|
215
|
+
const bezierText = `${x1} · ${y1} · ${x2} · ${y2}`;
|
|
181
216
|
|
|
182
217
|
return (
|
|
183
218
|
<div className="rounded-lg bg-neutral-900/50 p-2">
|
|
@@ -193,98 +228,139 @@ export function EaseCurveSection({
|
|
|
193
228
|
</button>
|
|
194
229
|
</div>
|
|
195
230
|
<div
|
|
196
|
-
className="overflow-hidden rounded
|
|
197
|
-
style={{ aspectRatio: `${
|
|
231
|
+
className="mx-auto overflow-hidden rounded-md border border-white/5 bg-black/20"
|
|
232
|
+
style={{ aspectRatio: `${SVGW} / ${SVGH}`, width: "100%", maxWidth: 230 }}
|
|
198
233
|
>
|
|
199
234
|
<svg
|
|
200
235
|
ref={svgRef}
|
|
201
236
|
width="100%"
|
|
202
237
|
height="100%"
|
|
203
|
-
viewBox={`0 0 ${
|
|
238
|
+
viewBox={`0 0 ${SVGW} ${SVGH}`}
|
|
204
239
|
preserveAspectRatio="none"
|
|
205
|
-
style={{ overflow: "visible" }}
|
|
206
240
|
className="touch-none select-none"
|
|
207
241
|
onPointerMove={handlePointerMove}
|
|
208
242
|
onPointerUp={handlePointerUp}
|
|
209
243
|
onPointerCancel={handlePointerUp}
|
|
210
244
|
>
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
245
|
+
{/* Grid — quarter lines inside the unit square */}
|
|
246
|
+
{[0.25, 0.5, 0.75].map((q) => (
|
|
247
|
+
<line
|
|
248
|
+
key={`v${q}`}
|
|
249
|
+
x1={xToSvg(q)}
|
|
250
|
+
y1={top}
|
|
251
|
+
x2={xToSvg(q)}
|
|
252
|
+
y2={bottom}
|
|
253
|
+
stroke="white"
|
|
254
|
+
strokeOpacity="0.05"
|
|
255
|
+
strokeWidth="1"
|
|
256
|
+
/>
|
|
257
|
+
))}
|
|
258
|
+
{[0.25, 0.5, 0.75].map((q) => (
|
|
259
|
+
<line
|
|
260
|
+
key={`h${q}`}
|
|
261
|
+
x1={left}
|
|
262
|
+
y1={yToSvg(q)}
|
|
263
|
+
x2={right}
|
|
264
|
+
y2={yToSvg(q)}
|
|
265
|
+
stroke="white"
|
|
266
|
+
strokeOpacity="0.05"
|
|
267
|
+
strokeWidth="1"
|
|
268
|
+
/>
|
|
269
|
+
))}
|
|
270
|
+
{/* Unit-square frame (progress 0 → 1) */}
|
|
271
|
+
<rect
|
|
272
|
+
x={left}
|
|
273
|
+
y={top}
|
|
274
|
+
width={S}
|
|
275
|
+
height={bottom - top}
|
|
276
|
+
fill="none"
|
|
216
277
|
stroke="white"
|
|
217
|
-
strokeOpacity="0.
|
|
218
|
-
strokeWidth="
|
|
278
|
+
strokeOpacity="0.1"
|
|
279
|
+
strokeWidth="1"
|
|
219
280
|
/>
|
|
281
|
+
{/* Linear reference diagonal */}
|
|
220
282
|
<line
|
|
221
|
-
x1={
|
|
222
|
-
y1={
|
|
223
|
-
x2={
|
|
224
|
-
y2={
|
|
283
|
+
x1={a0.x}
|
|
284
|
+
y1={a0.y}
|
|
285
|
+
x2={a1.x}
|
|
286
|
+
y2={a1.y}
|
|
225
287
|
stroke="white"
|
|
226
|
-
strokeOpacity="0.
|
|
227
|
-
strokeWidth="
|
|
288
|
+
strokeOpacity="0.08"
|
|
289
|
+
strokeWidth="1"
|
|
290
|
+
strokeDasharray="3 4"
|
|
228
291
|
/>
|
|
292
|
+
{/* Tangent handle lines */}
|
|
229
293
|
<line
|
|
230
|
-
x1={
|
|
231
|
-
y1={
|
|
294
|
+
x1={a0.x}
|
|
295
|
+
y1={a0.y}
|
|
232
296
|
x2={p1.x}
|
|
233
297
|
y2={p1.y}
|
|
234
|
-
stroke=
|
|
235
|
-
|
|
298
|
+
stroke={ACCENT}
|
|
299
|
+
strokeOpacity="0.5"
|
|
300
|
+
strokeWidth="1.5"
|
|
236
301
|
/>
|
|
237
302
|
<line
|
|
238
|
-
x1={
|
|
239
|
-
y1={
|
|
303
|
+
x1={a1.x}
|
|
304
|
+
y1={a1.y}
|
|
240
305
|
x2={p2.x}
|
|
241
306
|
y2={p2.y}
|
|
242
|
-
stroke=
|
|
243
|
-
|
|
307
|
+
stroke={ACCENT}
|
|
308
|
+
strokeOpacity="0.5"
|
|
309
|
+
strokeWidth="1.5"
|
|
244
310
|
/>
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
stroke="#3CE6AC"
|
|
253
|
-
strokeWidth="2"
|
|
254
|
-
className="cursor-grab active:cursor-grabbing"
|
|
255
|
-
onPointerDown={(e) => handlePointerDown("p1", e)}
|
|
256
|
-
/>
|
|
257
|
-
<circle
|
|
258
|
-
cx={p2.x}
|
|
259
|
-
cy={p2.y}
|
|
260
|
-
r="5"
|
|
261
|
-
fill="#0a0a1a"
|
|
262
|
-
stroke="#3CE6AC"
|
|
263
|
-
strokeWidth="2"
|
|
264
|
-
className="cursor-grab active:cursor-grabbing"
|
|
265
|
-
onPointerDown={(e) => handlePointerDown("p2", e)}
|
|
266
|
-
/>
|
|
267
|
-
{duration != null && duration > 0 && (
|
|
311
|
+
{/* The curve */}
|
|
312
|
+
<path d={curvePath} fill="none" stroke={ACCENT} strokeWidth="2.5" strokeLinecap="round" />
|
|
313
|
+
{/* Anchors at (0,0) and (1,1) */}
|
|
314
|
+
<circle cx={a0.x} cy={a0.y} r="3" fill={ACCENT} />
|
|
315
|
+
<circle cx={a1.x} cy={a1.y} r="3" fill={ACCENT} />
|
|
316
|
+
{/* Animated preview dot */}
|
|
317
|
+
{dot && (
|
|
268
318
|
<>
|
|
269
|
-
<
|
|
270
|
-
|
|
271
|
-
</text>
|
|
272
|
-
<text
|
|
273
|
-
x={pad + gw / 2}
|
|
274
|
-
y={h - 1}
|
|
275
|
-
textAnchor="middle"
|
|
276
|
-
className="fill-neutral-600 text-[8px]"
|
|
277
|
-
>
|
|
278
|
-
{(duration / 2).toFixed(1)}s
|
|
279
|
-
</text>
|
|
280
|
-
<text x={w - pad} y={h - 1} textAnchor="end" className="fill-neutral-600 text-[8px]">
|
|
281
|
-
{duration}s
|
|
282
|
-
</text>
|
|
319
|
+
<circle cx={dot.x} cy={dot.y} r="9" fill={ACCENT} fillOpacity="0.18" />
|
|
320
|
+
<circle cx={dot.x} cy={dot.y} r="4.5" fill={ACCENT} />
|
|
283
321
|
</>
|
|
284
322
|
)}
|
|
323
|
+
{/* Draggable control handles (large transparent hit area + visible dot) */}
|
|
324
|
+
{[["p1", p1] as const, ["p2", p2] as const].map(([key, pt]) => (
|
|
325
|
+
<g key={key}>
|
|
326
|
+
<circle
|
|
327
|
+
cx={pt.x}
|
|
328
|
+
cy={pt.y}
|
|
329
|
+
r="14"
|
|
330
|
+
fill="transparent"
|
|
331
|
+
className="cursor-grab active:cursor-grabbing"
|
|
332
|
+
onPointerDown={(e) => handlePointerDown(key, e)}
|
|
333
|
+
onPointerEnter={() => setHover(key)}
|
|
334
|
+
onPointerLeave={() => setHover((h) => (h === key ? null : h))}
|
|
335
|
+
/>
|
|
336
|
+
<circle
|
|
337
|
+
cx={pt.x}
|
|
338
|
+
cy={pt.y}
|
|
339
|
+
r={hover === key || draggingRef.current === key ? 7 : 5.5}
|
|
340
|
+
fill="#0a0a1a"
|
|
341
|
+
stroke={ACCENT}
|
|
342
|
+
strokeWidth="2.5"
|
|
343
|
+
className="pointer-events-none transition-[r]"
|
|
344
|
+
/>
|
|
345
|
+
</g>
|
|
346
|
+
))}
|
|
285
347
|
</svg>
|
|
286
348
|
</div>
|
|
287
|
-
|
|
349
|
+
{/* Axis + value readout */}
|
|
350
|
+
<div className="mt-1.5 flex items-center justify-between px-0.5 text-[9px] text-neutral-600">
|
|
351
|
+
<span>{duration != null && duration > 0 ? "0s" : "start"}</span>
|
|
352
|
+
<span className="tracking-wide text-neutral-500">time →</span>
|
|
353
|
+
<span>{duration != null && duration > 0 ? `${duration}s` : "end"}</span>
|
|
354
|
+
</div>
|
|
355
|
+
<div className="mt-1 flex items-center justify-between px-0.5">
|
|
356
|
+
<span className="text-[10px] text-neutral-400">{label}</span>
|
|
357
|
+
<span
|
|
358
|
+
className="font-mono text-[9px] tracking-tight text-neutral-600"
|
|
359
|
+
title="cubic-bezier control points"
|
|
360
|
+
>
|
|
361
|
+
{bezierText}
|
|
362
|
+
</span>
|
|
363
|
+
</div>
|
|
288
364
|
</div>
|
|
289
365
|
);
|
|
290
366
|
}
|
|
@@ -31,6 +31,7 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
|
|
|
31
31
|
onSetArcPath,
|
|
32
32
|
onUpdateArcSegment,
|
|
33
33
|
onUpdateKeyframeEase,
|
|
34
|
+
onSetAllKeyframeEases,
|
|
34
35
|
onUnroll,
|
|
35
36
|
}: GsapAnimationSectionProps) {
|
|
36
37
|
const [addMenuOpen, setAddMenuOpen] = useState(false);
|
|
@@ -70,6 +71,7 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
|
|
|
70
71
|
onSetArcPath={onSetArcPath}
|
|
71
72
|
onUpdateArcSegment={onUpdateArcSegment}
|
|
72
73
|
onUpdateKeyframeEase={onUpdateKeyframeEase}
|
|
74
|
+
onSetAllKeyframeEases={onSetAllKeyframeEases}
|
|
73
75
|
onUnroll={onUnroll}
|
|
74
76
|
/>
|
|
75
77
|
))}
|
|
@@ -2,24 +2,88 @@ import type { GsapPercentageKeyframe } from "@hyperframes/core/gsap-parser";
|
|
|
2
2
|
import { EASE_LABELS } from "./gsapAnimationConstants";
|
|
3
3
|
import { EaseCurveSection } from "./EaseCurveSection";
|
|
4
4
|
|
|
5
|
+
// The full GSAP easing vocabulary offered by the "Set all…" bulk control —
|
|
6
|
+
// every standard family in in/out/inOut, so authors aren't limited to a curated
|
|
7
|
+
// few. All are valid GSAP runtime eases; the non-cubic families (sine/circ/
|
|
8
|
+
// elastic/bounce) approximate in the per-segment curve preview.
|
|
9
|
+
const APPLY_ALL_EASES = [
|
|
10
|
+
"none",
|
|
11
|
+
"power1.in",
|
|
12
|
+
"power1.out",
|
|
13
|
+
"power1.inOut",
|
|
14
|
+
"power2.in",
|
|
15
|
+
"power2.out",
|
|
16
|
+
"power2.inOut",
|
|
17
|
+
"power3.in",
|
|
18
|
+
"power3.out",
|
|
19
|
+
"power3.inOut",
|
|
20
|
+
"power4.in",
|
|
21
|
+
"power4.out",
|
|
22
|
+
"power4.inOut",
|
|
23
|
+
"sine.in",
|
|
24
|
+
"sine.out",
|
|
25
|
+
"sine.inOut",
|
|
26
|
+
"expo.in",
|
|
27
|
+
"expo.out",
|
|
28
|
+
"expo.inOut",
|
|
29
|
+
"circ.in",
|
|
30
|
+
"circ.out",
|
|
31
|
+
"circ.inOut",
|
|
32
|
+
"back.in",
|
|
33
|
+
"back.out",
|
|
34
|
+
"back.inOut",
|
|
35
|
+
"elastic.in",
|
|
36
|
+
"elastic.out",
|
|
37
|
+
"elastic.inOut",
|
|
38
|
+
"bounce.in",
|
|
39
|
+
"bounce.out",
|
|
40
|
+
"bounce.inOut",
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
5
43
|
export function KeyframeEaseList({
|
|
6
44
|
keyframes,
|
|
7
45
|
globalEase,
|
|
8
46
|
expandedPct,
|
|
9
47
|
onToggle,
|
|
10
48
|
onEaseCommit,
|
|
49
|
+
onApplyAll,
|
|
11
50
|
}: {
|
|
12
51
|
keyframes: GsapPercentageKeyframe[];
|
|
13
52
|
globalEase: string;
|
|
14
53
|
expandedPct: number | null;
|
|
15
54
|
onToggle: (pct: number | null) => void;
|
|
16
55
|
onEaseCommit: (pct: number, ease: string) => void;
|
|
56
|
+
/** Apply one ease to every segment at once (clears per-segment overrides). */
|
|
57
|
+
onApplyAll?: (ease: string) => void;
|
|
17
58
|
}) {
|
|
18
59
|
return (
|
|
19
60
|
<div className="space-y-1">
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
61
|
+
<div className="flex items-center gap-2">
|
|
62
|
+
<p className="text-[9px] font-semibold uppercase tracking-wider text-neutral-500">
|
|
63
|
+
Per-keyframe easing
|
|
64
|
+
</p>
|
|
65
|
+
{onApplyAll && (
|
|
66
|
+
<select
|
|
67
|
+
aria-label="Apply one ease to all segments"
|
|
68
|
+
title="Apply one ease to every segment (clears per-segment overrides)"
|
|
69
|
+
value=""
|
|
70
|
+
onChange={(e) => {
|
|
71
|
+
const next = e.target.value;
|
|
72
|
+
if (next) onApplyAll(next);
|
|
73
|
+
}}
|
|
74
|
+
className="ml-auto cursor-pointer rounded bg-neutral-800 px-1.5 py-0.5 text-[9px] text-neutral-300 outline-none hover:bg-neutral-700 focus:ring-1 focus:ring-panel-accent/40"
|
|
75
|
+
>
|
|
76
|
+
<option value="" disabled>
|
|
77
|
+
Set all…
|
|
78
|
+
</option>
|
|
79
|
+
{APPLY_ALL_EASES.map((name) => (
|
|
80
|
+
<option key={name} value={name}>
|
|
81
|
+
{EASE_LABELS[name] ?? name}
|
|
82
|
+
</option>
|
|
83
|
+
))}
|
|
84
|
+
</select>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
23
87
|
{keyframes.map((kf, i) => {
|
|
24
88
|
if (i === 0) return null;
|
|
25
89
|
const segEase = kf.ease ?? globalEase;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Rect } from "../../utils/marqueeGeometry";
|
|
2
|
+
|
|
3
|
+
interface MarqueeOverlayProps {
|
|
4
|
+
/** Elements the marquee currently intersects — outlined live before mouse-up. */
|
|
5
|
+
candidateRects: Rect[];
|
|
6
|
+
/** The marquee drag rectangle itself, or null when not marquee-selecting. */
|
|
7
|
+
marqueeRect: Rect | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The marquee selection visuals: a live "candidate" outline on each element the
|
|
12
|
+
* box currently touches, plus the dashed drag rectangle. Extracted from
|
|
13
|
+
* DomEditOverlay to keep that file under the 600-line cap.
|
|
14
|
+
*/
|
|
15
|
+
export function MarqueeOverlay({ candidateRects, marqueeRect }: MarqueeOverlayProps) {
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
{candidateRects.map((r, i) => (
|
|
19
|
+
<div
|
|
20
|
+
key={i}
|
|
21
|
+
aria-hidden="true"
|
|
22
|
+
className="pointer-events-none absolute rounded-sm border border-studio-accent bg-studio-accent/5"
|
|
23
|
+
style={{ left: r.left, top: r.top, width: r.width, height: r.height }}
|
|
24
|
+
/>
|
|
25
|
+
))}
|
|
26
|
+
{marqueeRect && (
|
|
27
|
+
<div
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
className="pointer-events-none absolute border border-dashed border-studio-accent bg-studio-accent/10"
|
|
30
|
+
style={{
|
|
31
|
+
left: marqueeRect.left,
|
|
32
|
+
top: marqueeRect.top,
|
|
33
|
+
width: marqueeRect.width,
|
|
34
|
+
height: marqueeRect.height,
|
|
35
|
+
}}
|
|
36
|
+
/>
|
|
37
|
+
)}
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
}
|