@hyperframes/studio 0.6.86 → 0.6.87
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-BA19FAPN.js +143 -0
- package/dist/assets/index-CGlIm_-E.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +159 -6
- package/src/components/StudioHeader.tsx +20 -7
- package/src/components/StudioPreviewArea.tsx +6 -1
- package/src/components/StudioRightPanel.tsx +13 -0
- package/src/components/StudioToast.tsx +47 -7
- package/src/components/TimelineToolbar.tsx +12 -122
- package/src/components/editor/AnimationCard.tsx +64 -10
- package/src/components/editor/ArcPathControls.tsx +131 -0
- package/src/components/editor/BorderRadiusEditor.tsx +209 -0
- package/src/components/editor/DomEditOverlay.tsx +70 -11
- package/src/components/editor/DopesheetStrip.tsx +141 -0
- package/src/components/editor/EaseCurveSection.tsx +82 -7
- package/src/components/editor/GestureTrailOverlay.tsx +132 -0
- package/src/components/editor/GsapAnimationSection.tsx +14 -1
- package/src/components/editor/KeyframeDiamond.tsx +27 -12
- package/src/components/editor/LayersPanel.tsx +14 -12
- package/src/components/editor/MotionPathOverlay.tsx +146 -0
- package/src/components/editor/PropertyPanel.tsx +196 -66
- package/src/components/editor/SourceEditor.tsx +0 -1
- package/src/components/editor/StaggerControls.tsx +61 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +13 -0
- package/src/components/editor/domEditOverlayGeometry.ts +2 -1
- package/src/components/editor/domEditing.test.ts +43 -0
- package/src/components/editor/domEditing.ts +2 -0
- package/src/components/editor/domEditingElement.ts +25 -2
- package/src/components/editor/domEditingLayers.test.ts +78 -0
- package/src/components/editor/domEditingLayers.ts +33 -13
- package/src/components/editor/domEditingTypes.ts +1 -0
- package/src/components/editor/manualEditingAvailability.ts +1 -1
- package/src/components/editor/manualEdits.ts +3 -0
- package/src/components/editor/manualEditsDom.ts +23 -5
- package/src/components/editor/manualOffsetDrag.ts +59 -0
- package/src/components/editor/panelTokens.ts +10 -0
- package/src/components/editor/propertyPanelColor.tsx +2 -2
- package/src/components/editor/propertyPanelFill.tsx +1 -1
- package/src/components/editor/propertyPanelHelpers.ts +18 -2
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +38 -25
- package/src/components/editor/propertyPanelSections.tsx +4 -6
- package/src/components/editor/propertyPanelStyleSections.tsx +30 -8
- package/src/components/editor/useDomEditOverlayRects.ts +46 -2
- package/src/components/renders/RenderQueue.tsx +121 -100
- package/src/components/renders/RenderQueueItem.tsx +13 -13
- package/src/contexts/DomEditContext.tsx +12 -0
- package/src/contexts/FileManagerContext.tsx +3 -0
- package/src/contexts/StudioContext.tsx +0 -4
- package/src/hooks/gsapKeyframeCommit.ts +92 -0
- package/src/hooks/gsapRuntimeBridge.ts +147 -85
- package/src/hooks/gsapRuntimeKeyframes.ts +75 -24
- package/src/hooks/gsapRuntimePreview.ts +19 -0
- package/src/hooks/useAppHotkeys.ts +18 -0
- package/src/hooks/useAskAgentModal.ts +2 -4
- package/src/hooks/useDomEditCommits.ts +11 -17
- package/src/hooks/useDomEditSession.ts +47 -4
- package/src/hooks/useEnableKeyframes.ts +171 -0
- package/src/hooks/useFileManager.ts +7 -0
- package/src/hooks/useGestureRecording.ts +340 -0
- package/src/hooks/useGsapScriptCommits.ts +171 -35
- package/src/hooks/useGsapSelectionHandlers.ts +27 -8
- package/src/hooks/useGsapTweenCache.ts +169 -11
- package/src/hooks/useKeyframeKeyboard.ts +103 -0
- package/src/hooks/useStudioContextValue.ts +5 -4
- package/src/hooks/useStudioUrlState.ts +1 -2
- package/src/hooks/useTimelineEditing.ts +50 -3
- package/src/hooks/useToast.ts +6 -1
- package/src/player/components/ShortcutsPanel.tsx +40 -0
- package/src/player/components/TimelineClipDiamonds.tsx +3 -3
- package/src/player/components/TimelinePropertyRows.tsx +120 -0
- package/src/player/lib/timelineDOM.test.ts +55 -0
- package/src/player/lib/timelineDOM.ts +13 -0
- package/src/player/lib/timelineIframeHelpers.test.ts +51 -0
- package/src/player/lib/timelineIframeHelpers.ts +1 -0
- package/src/player/store/playerStore.ts +43 -0
- package/src/utils/audioBeatDetection.ts +58 -0
- package/src/utils/globalTimeCompiler.test.ts +169 -0
- package/src/utils/globalTimeCompiler.ts +77 -0
- package/src/utils/gsapSoftReload.ts +30 -10
- package/src/utils/keyframeSnapping.test.ts +74 -0
- package/src/utils/keyframeSnapping.ts +63 -0
- package/src/utils/rdpSimplify.ts +183 -0
- package/src/utils/sourcePatcher.ts +2 -0
- package/dist/assets/index-BT9VHgSy.js +0 -140
- package/dist/assets/index-DHcptK1_.css +0 -1
|
@@ -90,6 +90,29 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
90
90
|
}: DomEditOverlayProps) {
|
|
91
91
|
const overlayRef = useRef<HTMLDivElement | null>(null);
|
|
92
92
|
const boxRef = useRef<HTMLDivElement | null>(null);
|
|
93
|
+
|
|
94
|
+
const selectionShapeStyles = (() => {
|
|
95
|
+
const fallback = {
|
|
96
|
+
borderRadius: 4 as string | number,
|
|
97
|
+
clipPath: undefined as string | undefined,
|
|
98
|
+
};
|
|
99
|
+
if (!selection?.element) return fallback;
|
|
100
|
+
try {
|
|
101
|
+
const tag = selection.element.tagName.toLowerCase();
|
|
102
|
+
if (tag === "svg" || tag === "img" || tag === "video" || tag === "canvas") return fallback;
|
|
103
|
+
const win = selection.element.ownerDocument.defaultView;
|
|
104
|
+
if (!win) return fallback;
|
|
105
|
+
const cs = win.getComputedStyle(selection.element);
|
|
106
|
+
const br = cs.borderRadius;
|
|
107
|
+
const cp = cs.clipPath;
|
|
108
|
+
return {
|
|
109
|
+
borderRadius: br && br !== "0px" ? br : 4,
|
|
110
|
+
clipPath: cp && cp !== "none" ? cp : undefined,
|
|
111
|
+
};
|
|
112
|
+
} catch {
|
|
113
|
+
return fallback;
|
|
114
|
+
}
|
|
115
|
+
})();
|
|
93
116
|
const gestureRef = useRef<GestureState | null>(null);
|
|
94
117
|
const groupGestureRef = useRef<GroupGestureState | null>(null);
|
|
95
118
|
const blockedMoveRef = useRef<BlockedMoveState | null>(null);
|
|
@@ -134,6 +157,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
134
157
|
groupOverlayItems,
|
|
135
158
|
groupOverlayItemsRef,
|
|
136
159
|
setGroupOverlayItems,
|
|
160
|
+
childRects,
|
|
137
161
|
} = useDomEditOverlayRects({
|
|
138
162
|
iframeRef,
|
|
139
163
|
overlayRef,
|
|
@@ -228,6 +252,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
228
252
|
groupOverlayItems.every((item) => item.selection.capabilities.canApplyManualOffset);
|
|
229
253
|
|
|
230
254
|
const handleOverlayMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
255
|
+
if (!allowCanvasMovement) return;
|
|
231
256
|
if (suppressNextOverlayMouseDownRef.current) {
|
|
232
257
|
suppressNextOverlayMouseDownRef.current = false;
|
|
233
258
|
suppressNextBoxMouseDownRef.current = false;
|
|
@@ -288,6 +313,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
288
313
|
};
|
|
289
314
|
|
|
290
315
|
const handleBoxClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
316
|
+
if (!allowCanvasMovement) return;
|
|
291
317
|
if (gestureRef.current || groupGestureRef.current) return;
|
|
292
318
|
if (suppressNextBoxClickRef.current) {
|
|
293
319
|
suppressNextBoxClickRef.current = false;
|
|
@@ -320,20 +346,37 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
320
346
|
onPointerUp={gestures.onPointerUp}
|
|
321
347
|
onPointerCancel={() => gestures.clearPointerState(selectionRef)}
|
|
322
348
|
>
|
|
323
|
-
{hoverSelection && hoverRect && (
|
|
349
|
+
{hoverSelection && hoverRect && compRect.width > 0 && (
|
|
324
350
|
<div
|
|
325
351
|
aria-hidden="true"
|
|
326
352
|
data-dom-edit-hover-box="true"
|
|
327
|
-
className="pointer-events-none absolute
|
|
328
|
-
style={{
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
353
|
+
className="pointer-events-none absolute border border-studio-accent/80 bg-studio-accent/5 shadow-[0_0_0_1px_rgba(60,230,172,0.25)]"
|
|
354
|
+
style={(() => {
|
|
355
|
+
let br: string | number = 4;
|
|
356
|
+
let cp: string | undefined;
|
|
357
|
+
try {
|
|
358
|
+
const el = hoverSelection.element;
|
|
359
|
+
const tag = el.tagName.toLowerCase();
|
|
360
|
+
if (tag !== "svg" && tag !== "img" && tag !== "video" && tag !== "canvas") {
|
|
361
|
+
const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
|
|
362
|
+
if (cs?.borderRadius && cs.borderRadius !== "0px") br = cs.borderRadius;
|
|
363
|
+
if (cs?.clipPath && cs.clipPath !== "none") cp = cs.clipPath;
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
/* cross-origin guard */
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
left: hoverRect.left,
|
|
370
|
+
top: hoverRect.top,
|
|
371
|
+
width: hoverRect.width,
|
|
372
|
+
height: hoverRect.height,
|
|
373
|
+
borderRadius: br,
|
|
374
|
+
clipPath: cp,
|
|
375
|
+
};
|
|
376
|
+
})()}
|
|
334
377
|
/>
|
|
335
378
|
)}
|
|
336
|
-
{hasGroupSelection && groupOverlayItems.length > 1 && groupBounds && (
|
|
379
|
+
{hasGroupSelection && groupOverlayItems.length > 1 && groupBounds && compRect.width > 0 && (
|
|
337
380
|
<>
|
|
338
381
|
{groupOverlayItems.map((item) => (
|
|
339
382
|
<div
|
|
@@ -367,7 +410,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
367
410
|
/>
|
|
368
411
|
</>
|
|
369
412
|
)}
|
|
370
|
-
{!hasGroupSelection && selection && overlayRect && (
|
|
413
|
+
{!hasGroupSelection && selection && overlayRect && compRect.width > 0 && (
|
|
371
414
|
<>
|
|
372
415
|
{allowCanvasMovement && selection.capabilities.canApplyManualRotation && (
|
|
373
416
|
<div
|
|
@@ -398,12 +441,14 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
398
441
|
key={selectionKey}
|
|
399
442
|
ref={boxRef}
|
|
400
443
|
data-dom-edit-selection-box="true"
|
|
401
|
-
className=
|
|
444
|
+
className={`pointer-events-auto absolute ${selectionShapeStyles.clipPath ? "shadow-[inset_0_0_0_2px_rgba(60,230,172,0.6)]" : "border border-studio-accent/80 shadow-[0_0_0_1px_rgba(60,230,172,0.25)]"} bg-studio-accent/5`}
|
|
402
445
|
style={{
|
|
403
446
|
left: overlayRect.left,
|
|
404
447
|
top: overlayRect.top,
|
|
405
448
|
width: overlayRect.width,
|
|
406
449
|
height: overlayRect.height,
|
|
450
|
+
borderRadius: selectionShapeStyles.borderRadius,
|
|
451
|
+
clipPath: selectionShapeStyles.clipPath,
|
|
407
452
|
cursor:
|
|
408
453
|
allowCanvasMovement && selection.capabilities.canApplyManualOffset
|
|
409
454
|
? "move"
|
|
@@ -441,6 +486,20 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
441
486
|
</div>
|
|
442
487
|
</>
|
|
443
488
|
)}
|
|
489
|
+
{childRects.length > 0 &&
|
|
490
|
+
compRect.width > 0 &&
|
|
491
|
+
childRects.map((cr, i) => (
|
|
492
|
+
<div
|
|
493
|
+
key={i}
|
|
494
|
+
className="pointer-events-none absolute border border-dashed border-white/20 rounded-sm"
|
|
495
|
+
style={{
|
|
496
|
+
left: cr.left,
|
|
497
|
+
top: cr.top,
|
|
498
|
+
width: cr.width,
|
|
499
|
+
height: cr.height,
|
|
500
|
+
}}
|
|
501
|
+
/>
|
|
502
|
+
))}
|
|
444
503
|
<GridOverlay
|
|
445
504
|
visible={gridVisible}
|
|
446
505
|
spacing={gridSpacing}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { memo, useCallback, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
interface DopesheetKeyframe {
|
|
4
|
+
percentage: number;
|
|
5
|
+
properties: Record<string, number | string>;
|
|
6
|
+
ease?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface DopesheetStripProps {
|
|
10
|
+
keyframes: DopesheetKeyframe[];
|
|
11
|
+
selectedPercentage: number | null;
|
|
12
|
+
currentPercentage: number;
|
|
13
|
+
accentColor?: string;
|
|
14
|
+
onSelectKeyframe: (percentage: number) => void;
|
|
15
|
+
onDragKeyframe?: (fromPct: number, toPct: number) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DIAMOND_SIZE = 8;
|
|
19
|
+
const HALF = DIAMOND_SIZE / 2;
|
|
20
|
+
const STRIP_HEIGHT = 20;
|
|
21
|
+
const PADDING_X = 8;
|
|
22
|
+
|
|
23
|
+
export const DopesheetStrip = memo(function DopesheetStrip({
|
|
24
|
+
keyframes,
|
|
25
|
+
selectedPercentage,
|
|
26
|
+
currentPercentage,
|
|
27
|
+
accentColor = "#3CE6AC",
|
|
28
|
+
onSelectKeyframe,
|
|
29
|
+
onDragKeyframe,
|
|
30
|
+
}: DopesheetStripProps) {
|
|
31
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
32
|
+
const dragRef = useRef<{ startX: number; startPct: number } | null>(null);
|
|
33
|
+
|
|
34
|
+
const sorted = keyframes.slice().sort((a, b) => a.percentage - b.percentage);
|
|
35
|
+
|
|
36
|
+
const handlePointerDown = useCallback(
|
|
37
|
+
(e: React.PointerEvent, pct: number) => {
|
|
38
|
+
if (e.button !== 0) return;
|
|
39
|
+
e.stopPropagation();
|
|
40
|
+
const startX = e.clientX;
|
|
41
|
+
|
|
42
|
+
const handleMove = (me: PointerEvent) => {
|
|
43
|
+
if (Math.abs(me.clientX - startX) > 4) {
|
|
44
|
+
dragRef.current = { startX, startPct: pct };
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleUp = (ue: PointerEvent) => {
|
|
49
|
+
document.removeEventListener("pointermove", handleMove);
|
|
50
|
+
document.removeEventListener("pointerup", handleUp);
|
|
51
|
+
if (dragRef.current && containerRef.current && onDragKeyframe) {
|
|
52
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
53
|
+
const usableWidth = rect.width - PADDING_X * 2;
|
|
54
|
+
const dx = ue.clientX - dragRef.current.startX;
|
|
55
|
+
const dpct = (dx / usableWidth) * 100;
|
|
56
|
+
const newPct = Math.max(0, Math.min(100, Math.round((pct + dpct) * 10) / 10));
|
|
57
|
+
if (newPct !== pct) onDragKeyframe(pct, newPct);
|
|
58
|
+
} else {
|
|
59
|
+
onSelectKeyframe(pct);
|
|
60
|
+
}
|
|
61
|
+
dragRef.current = null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
document.addEventListener("pointermove", handleMove);
|
|
65
|
+
document.addEventListener("pointerup", handleUp);
|
|
66
|
+
},
|
|
67
|
+
[onSelectKeyframe, onDragKeyframe],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
ref={containerRef}
|
|
73
|
+
className="relative w-full rounded-md bg-neutral-900/60 border border-neutral-800/50"
|
|
74
|
+
style={{ height: STRIP_HEIGHT }}
|
|
75
|
+
>
|
|
76
|
+
{/* Playhead indicator */}
|
|
77
|
+
<div
|
|
78
|
+
className="absolute top-0 bottom-0 w-px bg-white/30"
|
|
79
|
+
style={{
|
|
80
|
+
left: `${PADDING_X + (currentPercentage / 100) * (100 - PADDING_X * 2)}%`,
|
|
81
|
+
marginLeft: -0.5,
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
{/* Diamond markers */}
|
|
86
|
+
<svg
|
|
87
|
+
className="absolute inset-0 w-full"
|
|
88
|
+
style={{ height: STRIP_HEIGHT }}
|
|
89
|
+
viewBox={`0 0 100 ${STRIP_HEIGHT}`}
|
|
90
|
+
preserveAspectRatio="none"
|
|
91
|
+
>
|
|
92
|
+
{sorted.map((kf) => {
|
|
93
|
+
const x = PADDING_X + (kf.percentage / 100) * (100 - PADDING_X * 2);
|
|
94
|
+
const y = STRIP_HEIGHT / 2;
|
|
95
|
+
const isSelected =
|
|
96
|
+
selectedPercentage !== null && Math.abs(kf.percentage - selectedPercentage) < 0.5;
|
|
97
|
+
const isHold = kf.ease === "steps(1)";
|
|
98
|
+
const fillColor = isSelected ? accentColor : "#737373";
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<g
|
|
102
|
+
key={kf.percentage}
|
|
103
|
+
onPointerDown={(e) => handlePointerDown(e, kf.percentage)}
|
|
104
|
+
style={{ cursor: "pointer" }}
|
|
105
|
+
>
|
|
106
|
+
{isHold ? (
|
|
107
|
+
<rect
|
|
108
|
+
x={x - HALF}
|
|
109
|
+
y={y - HALF}
|
|
110
|
+
width={DIAMOND_SIZE}
|
|
111
|
+
height={DIAMOND_SIZE}
|
|
112
|
+
fill={fillColor}
|
|
113
|
+
/>
|
|
114
|
+
) : (
|
|
115
|
+
<rect
|
|
116
|
+
x={x - HALF}
|
|
117
|
+
y={y - HALF}
|
|
118
|
+
width={DIAMOND_SIZE}
|
|
119
|
+
height={DIAMOND_SIZE}
|
|
120
|
+
fill={fillColor}
|
|
121
|
+
transform={`rotate(45, ${x}, ${y})`}
|
|
122
|
+
/>
|
|
123
|
+
)}
|
|
124
|
+
</g>
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
127
|
+
</svg>
|
|
128
|
+
|
|
129
|
+
{/* Time labels */}
|
|
130
|
+
{sorted.length > 0 && (
|
|
131
|
+
<div
|
|
132
|
+
className="absolute bottom-0 left-0 right-0 flex justify-between px-2 text-[8px] text-neutral-600 pointer-events-none"
|
|
133
|
+
style={{ lineHeight: "10px" }}
|
|
134
|
+
>
|
|
135
|
+
<span>{sorted[0].percentage}%</span>
|
|
136
|
+
{sorted.length > 1 && <span>{sorted[sorted.length - 1].percentage}%</span>}
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
});
|
|
@@ -1,6 +1,80 @@
|
|
|
1
|
-
import { useCallback, useRef, useState } from "react";
|
|
1
|
+
import { memo, useCallback, useRef, useState } from "react";
|
|
2
2
|
import { EASE_CURVES, EASE_LABELS, parseCustomEaseFromString } from "./gsapAnimationConstants";
|
|
3
3
|
|
|
4
|
+
const PRESET_GRID_EASES = [
|
|
5
|
+
"none",
|
|
6
|
+
"power2.out",
|
|
7
|
+
"power2.in",
|
|
8
|
+
"power2.inOut",
|
|
9
|
+
"power3.out",
|
|
10
|
+
"back.out",
|
|
11
|
+
"expo.out",
|
|
12
|
+
"elastic.out",
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
function MiniCurveSvg({
|
|
16
|
+
curve,
|
|
17
|
+
active,
|
|
18
|
+
}: {
|
|
19
|
+
curve: [number, number, number, number];
|
|
20
|
+
active: boolean;
|
|
21
|
+
}) {
|
|
22
|
+
const [x1, y1, x2, y2] = curve;
|
|
23
|
+
const s = 24;
|
|
24
|
+
const p = 3;
|
|
25
|
+
const g = s - p * 2;
|
|
26
|
+
const sx = (px: number) => p + g * px;
|
|
27
|
+
const sy = (py: number) => s - p - g * py;
|
|
28
|
+
const d = `M${p},${s - p} C${sx(x1)},${sy(y1)} ${sx(x2)},${sy(y2)} ${s - p},${p}`;
|
|
29
|
+
return (
|
|
30
|
+
<svg width={s} height={s} viewBox={`0 0 ${s} ${s}`}>
|
|
31
|
+
<path
|
|
32
|
+
d={d}
|
|
33
|
+
fill="none"
|
|
34
|
+
stroke={active ? "#3CE6AC" : "#737373"}
|
|
35
|
+
strokeWidth="1.5"
|
|
36
|
+
strokeLinecap="round"
|
|
37
|
+
/>
|
|
38
|
+
</svg>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const EasePresetGrid = memo(function EasePresetGrid({
|
|
43
|
+
currentEase,
|
|
44
|
+
onSelect,
|
|
45
|
+
}: {
|
|
46
|
+
currentEase: string;
|
|
47
|
+
onSelect: (ease: string) => void;
|
|
48
|
+
}) {
|
|
49
|
+
return (
|
|
50
|
+
<div className="grid grid-cols-4 gap-1 mb-2">
|
|
51
|
+
{PRESET_GRID_EASES.map((name) => {
|
|
52
|
+
const curve = EASE_CURVES[name];
|
|
53
|
+
if (!curve) return null;
|
|
54
|
+
const isActive = currentEase === name;
|
|
55
|
+
return (
|
|
56
|
+
<button
|
|
57
|
+
key={name}
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={() => onSelect(name)}
|
|
60
|
+
className={`flex flex-col items-center gap-0.5 rounded-md p-1 transition-colors ${
|
|
61
|
+
isActive ? "bg-panel-accent/10 ring-1 ring-panel-accent/30" : "hover:bg-neutral-800"
|
|
62
|
+
}`}
|
|
63
|
+
title={EASE_LABELS[name] ?? name}
|
|
64
|
+
>
|
|
65
|
+
<MiniCurveSvg curve={curve} active={isActive} />
|
|
66
|
+
<span
|
|
67
|
+
className={`text-[8px] leading-none ${isActive ? "text-panel-accent" : "text-neutral-500"}`}
|
|
68
|
+
>
|
|
69
|
+
{(EASE_LABELS[name] ?? name).split(" ").slice(0, 2).join(" ")}
|
|
70
|
+
</span>
|
|
71
|
+
</button>
|
|
72
|
+
);
|
|
73
|
+
})}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
4
78
|
function round2(n: number): number {
|
|
5
79
|
return Math.round(n * 100) / 100;
|
|
6
80
|
}
|
|
@@ -108,12 +182,13 @@ export function EaseCurveSection({
|
|
|
108
182
|
|
|
109
183
|
return (
|
|
110
184
|
<div className="rounded-lg bg-neutral-900/50 p-2">
|
|
185
|
+
<EasePresetGrid currentEase={ease} onSelect={(name) => onCustomEaseCommit(name)} />
|
|
111
186
|
<div className="mb-1.5 flex items-center justify-between">
|
|
112
187
|
<span className="text-[10px] font-medium text-neutral-500">Speed curve</span>
|
|
113
188
|
<button
|
|
114
189
|
type="button"
|
|
115
190
|
onClick={play}
|
|
116
|
-
className="rounded px-1.5 py-0.5 text-[10px] font-medium text-
|
|
191
|
+
className="rounded px-1.5 py-0.5 text-[10px] font-medium text-panel-accent transition-colors hover:bg-panel-accent/10"
|
|
117
192
|
>
|
|
118
193
|
{progress !== null ? "Playing…" : "Preview"}
|
|
119
194
|
</button>
|
|
@@ -165,17 +240,17 @@ export function EaseCurveSection({
|
|
|
165
240
|
y1={end.y}
|
|
166
241
|
x2={p2.x}
|
|
167
242
|
y2={p2.y}
|
|
168
|
-
stroke="rgba(
|
|
243
|
+
stroke="rgba(45,212,191,0.25)"
|
|
169
244
|
strokeWidth="1"
|
|
170
245
|
/>
|
|
171
|
-
<path d={curvePath} fill="none" stroke="#
|
|
172
|
-
{progress !== null && <circle cx={dotX} cy={dotY} r="4" fill="#
|
|
246
|
+
<path d={curvePath} fill="none" stroke="#3CE6AC" strokeWidth="2" strokeLinecap="round" />
|
|
247
|
+
{progress !== null && <circle cx={dotX} cy={dotY} r="4" fill="#3CE6AC" />}
|
|
173
248
|
<circle
|
|
174
249
|
cx={p1.x}
|
|
175
250
|
cy={p1.y}
|
|
176
251
|
r="5"
|
|
177
252
|
fill="#0a0a1a"
|
|
178
|
-
stroke="#
|
|
253
|
+
stroke="#3CE6AC"
|
|
179
254
|
strokeWidth="2"
|
|
180
255
|
className="cursor-grab active:cursor-grabbing"
|
|
181
256
|
onPointerDown={(e) => handlePointerDown("p1", e)}
|
|
@@ -185,7 +260,7 @@ export function EaseCurveSection({
|
|
|
185
260
|
cy={p2.y}
|
|
186
261
|
r="5"
|
|
187
262
|
fill="#0a0a1a"
|
|
188
|
-
stroke="#
|
|
263
|
+
stroke="#3CE6AC"
|
|
189
264
|
strokeWidth="2"
|
|
190
265
|
className="cursor-grab active:cursor-grabbing"
|
|
191
266
|
onPointerDown={(e) => handlePointerDown("p2", e)}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { memo, useMemo } from "react";
|
|
2
|
+
import type { GestureSample } from "../../hooks/useGestureRecording";
|
|
3
|
+
|
|
4
|
+
interface GestureTrailOverlayProps {
|
|
5
|
+
samples: GestureSample[];
|
|
6
|
+
sampleCount?: number;
|
|
7
|
+
trail?: Array<{ x: number; y: number }>;
|
|
8
|
+
simplifiedPoints?: Map<number, Record<string, number>>;
|
|
9
|
+
canvasRect: { left: number; top: number; width: number; height: number };
|
|
10
|
+
compositionSize?: { width: number; height: number };
|
|
11
|
+
mode: "recording" | "preview";
|
|
12
|
+
accentColor?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const GestureTrailOverlay = memo(function GestureTrailOverlay({
|
|
16
|
+
samples,
|
|
17
|
+
sampleCount,
|
|
18
|
+
trail,
|
|
19
|
+
simplifiedPoints,
|
|
20
|
+
canvasRect,
|
|
21
|
+
compositionSize,
|
|
22
|
+
mode,
|
|
23
|
+
accentColor = "#3CE6AC",
|
|
24
|
+
}: GestureTrailOverlayProps) {
|
|
25
|
+
const trailPoints = useMemo(() => {
|
|
26
|
+
if (trail && trail.length > 1) {
|
|
27
|
+
return trail.map((p) => `${p.x - canvasRect.left},${p.y - canvasRect.top}`).join(" ");
|
|
28
|
+
}
|
|
29
|
+
if (samples.length === 0) return "";
|
|
30
|
+
return samples
|
|
31
|
+
.filter((s) => s.properties.x != null && s.properties.y != null)
|
|
32
|
+
.map((s) => `${s.properties.x},${s.properties.y}`)
|
|
33
|
+
.join(" ");
|
|
34
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
35
|
+
}, [samples, trail, sampleCount, canvasRect.left, canvasRect.top]);
|
|
36
|
+
|
|
37
|
+
const simplifiedPath = useMemo(() => {
|
|
38
|
+
if (!simplifiedPoints || simplifiedPoints.size === 0) return "";
|
|
39
|
+
const pts: Array<{ x: number; y: number; pct: number }> = [];
|
|
40
|
+
for (const [pct, props] of simplifiedPoints) {
|
|
41
|
+
if (props.x != null && props.y != null) {
|
|
42
|
+
pts.push({ x: props.x, y: props.y, pct });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
pts.sort((a, b) => a.pct - b.pct);
|
|
46
|
+
if (pts.length === 0) return "";
|
|
47
|
+
return pts.map((p) => `${p.x},${p.y}`).join(" ");
|
|
48
|
+
}, [simplifiedPoints]);
|
|
49
|
+
|
|
50
|
+
const diamondPositions = useMemo(() => {
|
|
51
|
+
if (!simplifiedPoints || simplifiedPoints.size === 0) return [];
|
|
52
|
+
const pts: Array<{ x: number; y: number; pct: number }> = [];
|
|
53
|
+
for (const [pct, props] of simplifiedPoints) {
|
|
54
|
+
if (props.x != null && props.y != null) {
|
|
55
|
+
pts.push({ x: props.x, y: props.y, pct });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return pts.sort((a, b) => a.pct - b.pct);
|
|
59
|
+
}, [simplifiedPoints]);
|
|
60
|
+
|
|
61
|
+
if (samples.length < 2 && !simplifiedPoints) return null;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<svg
|
|
65
|
+
className="pointer-events-none fixed z-50"
|
|
66
|
+
style={{
|
|
67
|
+
left: canvasRect.left,
|
|
68
|
+
top: canvasRect.top,
|
|
69
|
+
width: canvasRect.width,
|
|
70
|
+
height: canvasRect.height,
|
|
71
|
+
}}
|
|
72
|
+
viewBox={
|
|
73
|
+
trail && trail.length > 1
|
|
74
|
+
? `0 0 ${canvasRect.width} ${canvasRect.height}`
|
|
75
|
+
: `0 0 ${compositionSize?.width ?? canvasRect.width} ${compositionSize?.height ?? canvasRect.height}`
|
|
76
|
+
}
|
|
77
|
+
>
|
|
78
|
+
{mode === "recording" && trailPoints && (
|
|
79
|
+
<polyline
|
|
80
|
+
points={trailPoints}
|
|
81
|
+
fill="none"
|
|
82
|
+
stroke={accentColor}
|
|
83
|
+
strokeWidth="2"
|
|
84
|
+
strokeOpacity="0.6"
|
|
85
|
+
strokeLinecap="round"
|
|
86
|
+
strokeLinejoin="round"
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{mode === "preview" && (
|
|
91
|
+
<>
|
|
92
|
+
{trailPoints && (
|
|
93
|
+
<polyline
|
|
94
|
+
points={trailPoints}
|
|
95
|
+
fill="none"
|
|
96
|
+
stroke={accentColor}
|
|
97
|
+
strokeWidth="1"
|
|
98
|
+
strokeOpacity="0.2"
|
|
99
|
+
strokeDasharray="4 3"
|
|
100
|
+
strokeLinecap="round"
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
103
|
+
{simplifiedPath && (
|
|
104
|
+
<polyline
|
|
105
|
+
points={simplifiedPath}
|
|
106
|
+
fill="none"
|
|
107
|
+
stroke={accentColor}
|
|
108
|
+
strokeWidth="2"
|
|
109
|
+
strokeOpacity="0.8"
|
|
110
|
+
strokeLinecap="round"
|
|
111
|
+
strokeLinejoin="round"
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
{diamondPositions.map((pt) => (
|
|
115
|
+
<g key={pt.pct} transform={`translate(${pt.x}, ${pt.y})`}>
|
|
116
|
+
<rect
|
|
117
|
+
x="-4"
|
|
118
|
+
y="-4"
|
|
119
|
+
width="8"
|
|
120
|
+
height="8"
|
|
121
|
+
rx="1"
|
|
122
|
+
transform="rotate(45)"
|
|
123
|
+
fill={accentColor}
|
|
124
|
+
fillOpacity="0.9"
|
|
125
|
+
/>
|
|
126
|
+
</g>
|
|
127
|
+
))}
|
|
128
|
+
</>
|
|
129
|
+
)}
|
|
130
|
+
</svg>
|
|
131
|
+
);
|
|
132
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { memo, useState } from "react";
|
|
2
|
-
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
2
|
+
import type { ArcPathSegment, GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
3
|
import { Film } from "../../icons/SystemIcons";
|
|
4
4
|
import { Section } from "./propertyPanelPrimitives";
|
|
5
5
|
import { ADD_METHODS, ADD_METHOD_LABELS, METHOD_TOOLTIPS } from "./gsapAnimationConstants";
|
|
@@ -23,6 +23,15 @@ interface GsapAnimationSectionProps {
|
|
|
23
23
|
onAddAnimation: (method: "to" | "from" | "set" | "fromTo") => void;
|
|
24
24
|
onLivePreview?: (property: string, value: number | string) => void;
|
|
25
25
|
onLivePreviewEnd?: () => void;
|
|
26
|
+
onSetArcPath?: (
|
|
27
|
+
animationId: string,
|
|
28
|
+
config: { enabled: boolean; autoRotate?: boolean | number; segments?: ArcPathSegment[] },
|
|
29
|
+
) => void;
|
|
30
|
+
onUpdateArcSegment?: (
|
|
31
|
+
animationId: string,
|
|
32
|
+
segmentIndex: number,
|
|
33
|
+
update: Partial<ArcPathSegment>,
|
|
34
|
+
) => void;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
export const GsapAnimationSection = memo(function GsapAnimationSection({
|
|
@@ -40,6 +49,8 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
|
|
|
40
49
|
onAddAnimation,
|
|
41
50
|
onLivePreview,
|
|
42
51
|
onLivePreviewEnd,
|
|
52
|
+
onSetArcPath,
|
|
53
|
+
onUpdateArcSegment,
|
|
43
54
|
}: GsapAnimationSectionProps) {
|
|
44
55
|
const [addMenuOpen, setAddMenuOpen] = useState(false);
|
|
45
56
|
|
|
@@ -75,6 +86,8 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
|
|
|
75
86
|
onRemoveFromProperty={onRemoveFromProperty}
|
|
76
87
|
onLivePreview={onLivePreview}
|
|
77
88
|
onLivePreviewEnd={onLivePreviewEnd}
|
|
89
|
+
onSetArcPath={onSetArcPath}
|
|
90
|
+
onUpdateArcSegment={onUpdateArcSegment}
|
|
78
91
|
/>
|
|
79
92
|
))}
|
|
80
93
|
|
|
@@ -7,6 +7,7 @@ interface KeyframeDiamondProps {
|
|
|
7
7
|
onClick: () => void;
|
|
8
8
|
title?: string;
|
|
9
9
|
size?: number;
|
|
10
|
+
isHold?: boolean;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
// fallow-ignore-next-line complexity
|
|
@@ -15,10 +16,11 @@ export const KeyframeDiamond = memo(function KeyframeDiamond({
|
|
|
15
16
|
onClick,
|
|
16
17
|
title,
|
|
17
18
|
size = 10,
|
|
19
|
+
isHold = false,
|
|
18
20
|
}: KeyframeDiamondProps) {
|
|
19
21
|
const isFilled = state === "active";
|
|
20
22
|
const opacity = state === "ghost" ? 0.25 : state === "inactive" ? 0.6 : 1;
|
|
21
|
-
const color = state === "active" ? "#
|
|
23
|
+
const color = state === "active" ? "#3CE6AC" : "#a3a3a3";
|
|
22
24
|
|
|
23
25
|
return (
|
|
24
26
|
<button
|
|
@@ -32,17 +34,30 @@ export const KeyframeDiamond = memo(function KeyframeDiamond({
|
|
|
32
34
|
title={title}
|
|
33
35
|
>
|
|
34
36
|
<svg width={size} height={size} viewBox="0 0 10 10">
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
{isHold ? (
|
|
38
|
+
<rect
|
|
39
|
+
x="2"
|
|
40
|
+
y="2"
|
|
41
|
+
width="6"
|
|
42
|
+
height="6"
|
|
43
|
+
rx="0.5"
|
|
44
|
+
fill={isFilled ? "currentColor" : "none"}
|
|
45
|
+
stroke="currentColor"
|
|
46
|
+
strokeWidth="1.2"
|
|
47
|
+
/>
|
|
48
|
+
) : (
|
|
49
|
+
<rect
|
|
50
|
+
x="5"
|
|
51
|
+
y="0.7"
|
|
52
|
+
width="6"
|
|
53
|
+
height="6"
|
|
54
|
+
rx="1"
|
|
55
|
+
transform="rotate(45 5 0.7)"
|
|
56
|
+
fill={isFilled ? "currentColor" : "none"}
|
|
57
|
+
stroke="currentColor"
|
|
58
|
+
strokeWidth="1.2"
|
|
59
|
+
/>
|
|
60
|
+
)}
|
|
46
61
|
</svg>
|
|
47
62
|
</button>
|
|
48
63
|
);
|