@hyperframes/studio 0.6.53 → 0.6.54
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-CKJCBFsG.js +138 -0
- package/dist/assets/index-ZdgB8MFr.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/components/StudioRightPanel.tsx +18 -0
- package/src/components/editor/AnimationCard.tsx +325 -0
- package/src/components/editor/EaseCurveSection.tsx +213 -0
- package/src/components/editor/GsapAnimationSection.tsx +112 -0
- package/src/components/editor/PropertyPanel.tsx +48 -18
- package/src/components/editor/domEditingTypes.ts +2 -0
- package/src/components/editor/gsapAnimationConstants.ts +130 -0
- package/src/components/editor/manualEditingAvailability.ts +6 -0
- package/src/components/editor/manualEdits.test.ts +101 -0
- package/src/components/editor/manualEdits.ts +22 -9
- package/src/components/editor/manualEditsDom.ts +22 -21
- package/src/components/editor/manualOffsetDrag.test.ts +35 -22
- package/src/components/editor/manualOffsetDrag.ts +1 -7
- package/src/components/editor/propertyPanelPrimitives.tsx +6 -1
- package/src/contexts/DomEditContext.tsx +27 -0
- package/src/hooks/useDomEditSession.ts +98 -2
- package/src/hooks/useDomSelection.ts +8 -0
- package/src/hooks/useGsapScriptCommits.ts +303 -0
- package/src/hooks/useGsapTweenCache.ts +80 -0
- package/src/hooks/usePreviewPersistence.ts +1 -0
- package/dist/assets/index-B2mn12z0.css +0 -1
- package/dist/assets/index-CZNoIjSE.js +0 -138
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from "react";
|
|
2
|
+
import { EASE_CURVES, EASE_LABELS, parseCustomEaseFromString } from "./gsapAnimationConstants";
|
|
3
|
+
|
|
4
|
+
function round2(n: number): number {
|
|
5
|
+
return Math.round(n * 100) / 100;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function EaseCurveSection({
|
|
9
|
+
ease,
|
|
10
|
+
duration,
|
|
11
|
+
onCustomEaseCommit,
|
|
12
|
+
}: {
|
|
13
|
+
ease: string;
|
|
14
|
+
duration?: number;
|
|
15
|
+
onCustomEaseCommit: (ease: string) => void;
|
|
16
|
+
}) {
|
|
17
|
+
const isCustom = ease.startsWith("custom(");
|
|
18
|
+
const curveFromPreset = EASE_CURVES[ease];
|
|
19
|
+
const customPoints = isCustom ? parseCustomEaseFromString(ease) : null;
|
|
20
|
+
const curve: [number, number, number, number] | null =
|
|
21
|
+
isCustom && customPoints
|
|
22
|
+
? [customPoints.x1, customPoints.y1, customPoints.x2, customPoints.y2]
|
|
23
|
+
: (curveFromPreset ?? null);
|
|
24
|
+
|
|
25
|
+
const [draft, setDraft] = useState<[number, number, number, number] | null>(null);
|
|
26
|
+
const [progress, setProgress] = useState<number | null>(null);
|
|
27
|
+
const draggingRef = useRef<"p1" | "p2" | null>(null);
|
|
28
|
+
const svgRef = useRef<SVGSVGElement | null>(null);
|
|
29
|
+
const rafRef = useRef<number>(0);
|
|
30
|
+
|
|
31
|
+
const play = useCallback(() => {
|
|
32
|
+
const start = performance.now();
|
|
33
|
+
const dur = 1000;
|
|
34
|
+
const tick = (now: number) => {
|
|
35
|
+
const t = Math.min((now - start) / dur, 1);
|
|
36
|
+
setProgress(t);
|
|
37
|
+
if (t < 1) rafRef.current = requestAnimationFrame(tick);
|
|
38
|
+
else setTimeout(() => setProgress(null), 400);
|
|
39
|
+
};
|
|
40
|
+
cancelAnimationFrame(rafRef.current);
|
|
41
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const active = draft ?? curve;
|
|
45
|
+
if (!active) return null;
|
|
46
|
+
const [x1, y1, x2, y2] = active;
|
|
47
|
+
|
|
48
|
+
const w = 200;
|
|
49
|
+
const h = 100;
|
|
50
|
+
const pad = 14;
|
|
51
|
+
const gw = w - pad * 2;
|
|
52
|
+
const gh = h - pad * 2;
|
|
53
|
+
|
|
54
|
+
const toSvg = (px: number, py: number) => ({
|
|
55
|
+
x: pad + gw * px,
|
|
56
|
+
y: h - pad - gh * py,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const curvePath = `M${pad},${h - pad} C${toSvg(x1, y1).x},${toSvg(x1, y1).y} ${toSvg(x2, y2).x},${toSvg(x2, y2).y} ${w - pad},${pad}`;
|
|
60
|
+
|
|
61
|
+
let dotX = pad;
|
|
62
|
+
let dotY = h - pad;
|
|
63
|
+
if (progress !== null) {
|
|
64
|
+
const t = progress;
|
|
65
|
+
const mt = 1 - t;
|
|
66
|
+
dotX = pad + gw * (mt * mt * mt * 0 + 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t * t * t);
|
|
67
|
+
dotY =
|
|
68
|
+
h - pad - gh * (mt * mt * mt * 0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const handlePointerDown = (handle: "p1" | "p2", e: React.PointerEvent) => {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
e.stopPropagation();
|
|
74
|
+
draggingRef.current = handle;
|
|
75
|
+
(e.target as SVGElement).setPointerCapture(e.pointerId);
|
|
76
|
+
if (!draft) setDraft([x1, y1, x2, y2]);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handlePointerMove = (e: React.PointerEvent<SVGSVGElement>) => {
|
|
80
|
+
if (!draggingRef.current || !svgRef.current) return;
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
const rect = svgRef.current.getBoundingClientRect();
|
|
83
|
+
const sx = ((e.clientX - rect.left) / rect.width) * w;
|
|
84
|
+
const sy = ((e.clientY - rect.top) / rect.height) * h;
|
|
85
|
+
const px = Math.max(0, Math.min(1, (sx - pad) / gw));
|
|
86
|
+
const py = Math.max(-1, Math.min(2, (h - pad - sy) / gh));
|
|
87
|
+
const prev = draft ?? [x1, y1, x2, y2];
|
|
88
|
+
const next: [number, number, number, number] =
|
|
89
|
+
draggingRef.current === "p1"
|
|
90
|
+
? [round2(px), round2(py), prev[2], prev[3]]
|
|
91
|
+
: [prev[0], prev[1], round2(px), round2(py)];
|
|
92
|
+
setDraft(next);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const handlePointerUp = () => {
|
|
96
|
+
if (!draggingRef.current || !draft) return;
|
|
97
|
+
draggingRef.current = null;
|
|
98
|
+
const path = `M0,0 C${draft[0]},${draft[1]} ${draft[2]},${draft[3]} 1,1`;
|
|
99
|
+
onCustomEaseCommit(`custom(${path})`);
|
|
100
|
+
setDraft(null);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const p1 = toSvg(x1, y1);
|
|
104
|
+
const p2 = toSvg(x2, y2);
|
|
105
|
+
const start = toSvg(0, 0);
|
|
106
|
+
const end = toSvg(1, 1);
|
|
107
|
+
const label = isCustom ? "Custom curve" : (EASE_LABELS[ease] ?? ease);
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className="rounded-lg bg-neutral-900/50 p-2">
|
|
111
|
+
<div className="mb-1.5 flex items-center justify-between">
|
|
112
|
+
<span className="text-[10px] font-medium text-neutral-500">Speed curve</span>
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={play}
|
|
116
|
+
className="rounded px-1.5 py-0.5 text-[10px] font-medium text-emerald-400 transition-colors hover:bg-emerald-500/10"
|
|
117
|
+
>
|
|
118
|
+
{progress !== null ? "Playing…" : "Preview"}
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
<div className="overflow-hidden rounded pt-[72px] -mt-[72px]">
|
|
122
|
+
<svg
|
|
123
|
+
ref={svgRef}
|
|
124
|
+
width="100%"
|
|
125
|
+
height={h}
|
|
126
|
+
viewBox={`0 0 ${w} ${h}`}
|
|
127
|
+
preserveAspectRatio="none"
|
|
128
|
+
style={{ overflow: "visible" }}
|
|
129
|
+
className="touch-none select-none"
|
|
130
|
+
onPointerMove={handlePointerMove}
|
|
131
|
+
onPointerUp={handlePointerUp}
|
|
132
|
+
onPointerCancel={handlePointerUp}
|
|
133
|
+
>
|
|
134
|
+
<line
|
|
135
|
+
x1={pad}
|
|
136
|
+
y1={h - pad}
|
|
137
|
+
x2={w - pad}
|
|
138
|
+
y2={h - pad}
|
|
139
|
+
stroke="white"
|
|
140
|
+
strokeOpacity="0.06"
|
|
141
|
+
strokeWidth="0.5"
|
|
142
|
+
/>
|
|
143
|
+
<line
|
|
144
|
+
x1={pad}
|
|
145
|
+
y1={pad}
|
|
146
|
+
x2={pad}
|
|
147
|
+
y2={h - pad}
|
|
148
|
+
stroke="white"
|
|
149
|
+
strokeOpacity="0.06"
|
|
150
|
+
strokeWidth="0.5"
|
|
151
|
+
/>
|
|
152
|
+
<line
|
|
153
|
+
x1={start.x}
|
|
154
|
+
y1={start.y}
|
|
155
|
+
x2={p1.x}
|
|
156
|
+
y2={p1.y}
|
|
157
|
+
stroke="rgba(52,211,153,0.25)"
|
|
158
|
+
strokeWidth="1"
|
|
159
|
+
/>
|
|
160
|
+
<line
|
|
161
|
+
x1={end.x}
|
|
162
|
+
y1={end.y}
|
|
163
|
+
x2={p2.x}
|
|
164
|
+
y2={p2.y}
|
|
165
|
+
stroke="rgba(52,211,153,0.25)"
|
|
166
|
+
strokeWidth="1"
|
|
167
|
+
/>
|
|
168
|
+
<path d={curvePath} fill="none" stroke="#34d399" strokeWidth="2" strokeLinecap="round" />
|
|
169
|
+
{progress !== null && <circle cx={dotX} cy={dotY} r="4" fill="#34d399" />}
|
|
170
|
+
<circle
|
|
171
|
+
cx={p1.x}
|
|
172
|
+
cy={p1.y}
|
|
173
|
+
r="5"
|
|
174
|
+
fill="#0a0a1a"
|
|
175
|
+
stroke="#34d399"
|
|
176
|
+
strokeWidth="2"
|
|
177
|
+
className="cursor-grab active:cursor-grabbing"
|
|
178
|
+
onPointerDown={(e) => handlePointerDown("p1", e)}
|
|
179
|
+
/>
|
|
180
|
+
<circle
|
|
181
|
+
cx={p2.x}
|
|
182
|
+
cy={p2.y}
|
|
183
|
+
r="5"
|
|
184
|
+
fill="#0a0a1a"
|
|
185
|
+
stroke="#34d399"
|
|
186
|
+
strokeWidth="2"
|
|
187
|
+
className="cursor-grab active:cursor-grabbing"
|
|
188
|
+
onPointerDown={(e) => handlePointerDown("p2", e)}
|
|
189
|
+
/>
|
|
190
|
+
{duration != null && duration > 0 && (
|
|
191
|
+
<>
|
|
192
|
+
<text x={pad} y={h - 1} textAnchor="start" className="fill-neutral-600 text-[8px]">
|
|
193
|
+
0s
|
|
194
|
+
</text>
|
|
195
|
+
<text
|
|
196
|
+
x={pad + gw / 2}
|
|
197
|
+
y={h - 1}
|
|
198
|
+
textAnchor="middle"
|
|
199
|
+
className="fill-neutral-600 text-[8px]"
|
|
200
|
+
>
|
|
201
|
+
{(duration / 2).toFixed(1)}s
|
|
202
|
+
</text>
|
|
203
|
+
<text x={w - pad} y={h - 1} textAnchor="end" className="fill-neutral-600 text-[8px]">
|
|
204
|
+
{duration}s
|
|
205
|
+
</text>
|
|
206
|
+
</>
|
|
207
|
+
)}
|
|
208
|
+
</svg>
|
|
209
|
+
</div>
|
|
210
|
+
<p className="mt-1 text-center text-[10px] text-neutral-500">{label}</p>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { memo, useState } from "react";
|
|
2
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
|
+
import { Film } from "../../icons/SystemIcons";
|
|
4
|
+
import { Section } from "./propertyPanelPrimitives";
|
|
5
|
+
import { ADD_METHODS, ADD_METHOD_LABELS, METHOD_TOOLTIPS } from "./gsapAnimationConstants";
|
|
6
|
+
import { AnimationCard } from "./AnimationCard";
|
|
7
|
+
|
|
8
|
+
interface GsapAnimationSectionProps {
|
|
9
|
+
animations: GsapAnimation[];
|
|
10
|
+
multipleTimelines?: boolean;
|
|
11
|
+
unsupportedTimelinePattern?: boolean;
|
|
12
|
+
onUpdateProperty: (animationId: string, property: string, value: number | string) => void;
|
|
13
|
+
onUpdateMeta: (
|
|
14
|
+
animationId: string,
|
|
15
|
+
updates: { duration?: number; ease?: string; position?: number },
|
|
16
|
+
) => void;
|
|
17
|
+
onDeleteAnimation: (animationId: string) => void;
|
|
18
|
+
onAddProperty: (animationId: string, property: string) => void;
|
|
19
|
+
onRemoveProperty: (animationId: string, property: string) => void;
|
|
20
|
+
onAddAnimation: (method: "to" | "from" | "set") => void;
|
|
21
|
+
onLivePreview?: (property: string, value: number | string) => void;
|
|
22
|
+
onLivePreviewEnd?: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const GsapAnimationSection = memo(function GsapAnimationSection({
|
|
26
|
+
animations,
|
|
27
|
+
multipleTimelines,
|
|
28
|
+
unsupportedTimelinePattern,
|
|
29
|
+
onUpdateProperty,
|
|
30
|
+
onUpdateMeta,
|
|
31
|
+
onDeleteAnimation,
|
|
32
|
+
onAddProperty,
|
|
33
|
+
onRemoveProperty,
|
|
34
|
+
onAddAnimation,
|
|
35
|
+
onLivePreview,
|
|
36
|
+
onLivePreviewEnd,
|
|
37
|
+
}: GsapAnimationSectionProps) {
|
|
38
|
+
const [addMenuOpen, setAddMenuOpen] = useState(false);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Section title="Animation" icon={<Film size={15} />}>
|
|
42
|
+
{multipleTimelines && (
|
|
43
|
+
<p className="mb-2 rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-amber-400">
|
|
44
|
+
This file has multiple GSAP timelines. Animation editing is disabled to prevent data loss
|
|
45
|
+
— consolidate into a single timeline to enable editing.
|
|
46
|
+
</p>
|
|
47
|
+
)}
|
|
48
|
+
{unsupportedTimelinePattern && (
|
|
49
|
+
<p className="mb-2 rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-amber-400">
|
|
50
|
+
This composition uses a timeline assignment pattern (window.__timelines[...]) that the
|
|
51
|
+
editor doesn't support. Use a variable declaration (const tl = gsap.timeline()) to
|
|
52
|
+
enable editing.
|
|
53
|
+
</p>
|
|
54
|
+
)}
|
|
55
|
+
{multipleTimelines || unsupportedTimelinePattern ? null : (
|
|
56
|
+
<div className="space-y-2">
|
|
57
|
+
{animations.map((anim, index) => (
|
|
58
|
+
<AnimationCard
|
|
59
|
+
key={anim.id}
|
|
60
|
+
animation={anim}
|
|
61
|
+
defaultExpanded={index === 0}
|
|
62
|
+
onUpdateProperty={onUpdateProperty}
|
|
63
|
+
onUpdateMeta={onUpdateMeta}
|
|
64
|
+
onDeleteAnimation={onDeleteAnimation}
|
|
65
|
+
onAddProperty={onAddProperty}
|
|
66
|
+
onRemoveProperty={onRemoveProperty}
|
|
67
|
+
onLivePreview={onLivePreview}
|
|
68
|
+
onLivePreviewEnd={onLivePreviewEnd}
|
|
69
|
+
/>
|
|
70
|
+
))}
|
|
71
|
+
|
|
72
|
+
<div className="relative pt-1">
|
|
73
|
+
{addMenuOpen ? (
|
|
74
|
+
<div className="flex gap-1.5">
|
|
75
|
+
{ADD_METHODS.map((method) => (
|
|
76
|
+
<button
|
|
77
|
+
key={method}
|
|
78
|
+
type="button"
|
|
79
|
+
title={METHOD_TOOLTIPS[method]}
|
|
80
|
+
onClick={() => {
|
|
81
|
+
onAddAnimation(method);
|
|
82
|
+
setAddMenuOpen(false);
|
|
83
|
+
}}
|
|
84
|
+
className="rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 py-1.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white"
|
|
85
|
+
>
|
|
86
|
+
{ADD_METHOD_LABELS[method] ?? method}
|
|
87
|
+
</button>
|
|
88
|
+
))}
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
onClick={() => setAddMenuOpen(false)}
|
|
92
|
+
className="px-1.5 text-[11px] text-neutral-500 hover:text-neutral-300"
|
|
93
|
+
>
|
|
94
|
+
Cancel
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
) : (
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
onClick={() => setAddMenuOpen(true)}
|
|
101
|
+
className="text-[11px] font-medium text-neutral-400 transition-colors hover:text-neutral-200"
|
|
102
|
+
title="Add a new animation effect to this element"
|
|
103
|
+
>
|
|
104
|
+
+ Add effect
|
|
105
|
+
</button>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</Section>
|
|
111
|
+
);
|
|
112
|
+
});
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { memo } from "react";
|
|
2
2
|
import { Clock, Eye, Layers, MessageSquare, Move, X } from "../../icons/SystemIcons";
|
|
3
3
|
import { type DomEditSelection } from "./domEditing";
|
|
4
|
-
import {
|
|
5
|
-
readStudioBoxSize,
|
|
6
|
-
readStudioPathOffset,
|
|
7
|
-
readStudioRotation,
|
|
8
|
-
readGsapTranslateFromTransform,
|
|
9
|
-
} from "./manualEdits";
|
|
4
|
+
import { readStudioBoxSize, readStudioPathOffset, readStudioRotation } from "./manualEdits";
|
|
10
5
|
import type { ImportedFontAsset } from "./fontAssets";
|
|
11
6
|
import {
|
|
12
7
|
EMPTY_STYLES,
|
|
@@ -18,12 +13,13 @@ import {
|
|
|
18
13
|
import { MetricField, Section } from "./propertyPanelPrimitives";
|
|
19
14
|
import { isMediaElement, MediaSection } from "./propertyPanelMediaSection";
|
|
20
15
|
import { TextSection, StyleSections } from "./propertyPanelSections";
|
|
16
|
+
import { GsapAnimationSection } from "./GsapAnimationSection";
|
|
17
|
+
import { STUDIO_GSAP_PANEL_ENABLED } from "./manualEditingAvailability";
|
|
21
18
|
|
|
22
19
|
// Re-export helpers that external consumers import from this module
|
|
23
20
|
export {
|
|
24
21
|
buildStrokeStyleUpdates,
|
|
25
22
|
buildStrokeWidthStyleUpdates,
|
|
26
|
-
clampPanelNumber,
|
|
27
23
|
getCssFilterFunctionPx,
|
|
28
24
|
getClipPathInsetPx,
|
|
29
25
|
inferBoxShadowPreset,
|
|
@@ -54,6 +50,18 @@ interface PropertyPanelProps {
|
|
|
54
50
|
onImportAssets?: (files: FileList) => Promise<string[]>;
|
|
55
51
|
fontAssets?: ImportedFontAsset[];
|
|
56
52
|
onImportFonts?: (files: FileList | File[]) => Promise<ImportedFontAsset[]>;
|
|
53
|
+
gsapAnimations?: import("@hyperframes/core/gsap-parser").GsapAnimation[];
|
|
54
|
+
gsapMultipleTimelines?: boolean;
|
|
55
|
+
gsapUnsupportedTimelinePattern?: boolean;
|
|
56
|
+
onUpdateGsapProperty?: (animId: string, prop: string, value: number | string) => void;
|
|
57
|
+
onUpdateGsapMeta?: (
|
|
58
|
+
animId: string,
|
|
59
|
+
updates: { duration?: number; ease?: string; position?: number },
|
|
60
|
+
) => void;
|
|
61
|
+
onDeleteGsapAnimation?: (animId: string) => void;
|
|
62
|
+
onAddGsapProperty?: (animId: string, prop: string) => void;
|
|
63
|
+
onRemoveGsapProperty?: (animId: string, prop: string) => void;
|
|
64
|
+
onAddGsapAnimation?: (method: "to" | "from" | "set") => void;
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
/* ------------------------------------------------------------------ */
|
|
@@ -146,6 +154,15 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
146
154
|
onImportAssets,
|
|
147
155
|
fontAssets = [],
|
|
148
156
|
onImportFonts,
|
|
157
|
+
gsapAnimations = [],
|
|
158
|
+
gsapMultipleTimelines,
|
|
159
|
+
gsapUnsupportedTimelinePattern,
|
|
160
|
+
onUpdateGsapProperty,
|
|
161
|
+
onUpdateGsapMeta,
|
|
162
|
+
onDeleteGsapAnimation,
|
|
163
|
+
onAddGsapProperty,
|
|
164
|
+
onRemoveGsapProperty,
|
|
165
|
+
onAddGsapAnimation,
|
|
149
166
|
}: PropertyPanelProps) {
|
|
150
167
|
const styles = element?.computedStyles ?? EMPTY_STYLES;
|
|
151
168
|
|
|
@@ -186,11 +203,6 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
186
203
|
const sourceLabel = element.id ? `#${element.id}` : element.selector;
|
|
187
204
|
const showEditableSections = element.capabilities.canEditStyles;
|
|
188
205
|
const manualOffset = readStudioPathOffset(element.element);
|
|
189
|
-
const gsapTranslate = readGsapTranslateFromTransform(element.element);
|
|
190
|
-
const visualOffset = {
|
|
191
|
-
x: manualOffset.x + gsapTranslate.x,
|
|
192
|
-
y: manualOffset.y + gsapTranslate.y,
|
|
193
|
-
};
|
|
194
206
|
const manualSize = readStudioBoxSize(element.element);
|
|
195
207
|
const resolvedWidth =
|
|
196
208
|
manualSize.width > 0
|
|
@@ -204,11 +216,10 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
204
216
|
const commitManualOffset = (axis: "x" | "y", nextValue: string) => {
|
|
205
217
|
const parsed = parsePxMetricValue(nextValue);
|
|
206
218
|
if (parsed == null) return;
|
|
207
|
-
const
|
|
208
|
-
const currentGsap = readGsapTranslateFromTransform(element.element);
|
|
219
|
+
const current = readStudioPathOffset(element.element);
|
|
209
220
|
onSetManualOffset(element, {
|
|
210
|
-
x: axis === "x" ? parsed
|
|
211
|
-
y: axis === "y" ? parsed
|
|
221
|
+
x: axis === "x" ? parsed : current.x,
|
|
222
|
+
y: axis === "y" ? parsed : current.y,
|
|
212
223
|
});
|
|
213
224
|
};
|
|
214
225
|
|
|
@@ -300,14 +311,14 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
300
311
|
<div className={RESPONSIVE_GRID}>
|
|
301
312
|
<MetricField
|
|
302
313
|
label="X"
|
|
303
|
-
value={formatPxMetricValue(
|
|
314
|
+
value={formatPxMetricValue(manualOffset.x)}
|
|
304
315
|
disabled={manualOffsetEditingDisabled}
|
|
305
316
|
scrub
|
|
306
317
|
onCommit={(next) => commitManualOffset("x", next)}
|
|
307
318
|
/>
|
|
308
319
|
<MetricField
|
|
309
320
|
label="Y"
|
|
310
|
-
value={formatPxMetricValue(
|
|
321
|
+
value={formatPxMetricValue(manualOffset.y)}
|
|
311
322
|
disabled={manualOffsetEditingDisabled}
|
|
312
323
|
scrub
|
|
313
324
|
onCommit={(next) => commitManualOffset("y", next)}
|
|
@@ -342,6 +353,25 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
342
353
|
</div>
|
|
343
354
|
</Section>
|
|
344
355
|
|
|
356
|
+
{STUDIO_GSAP_PANEL_ENABLED &&
|
|
357
|
+
onUpdateGsapProperty &&
|
|
358
|
+
onUpdateGsapMeta &&
|
|
359
|
+
onDeleteGsapAnimation &&
|
|
360
|
+
onAddGsapProperty &&
|
|
361
|
+
onAddGsapAnimation && (
|
|
362
|
+
<GsapAnimationSection
|
|
363
|
+
animations={gsapAnimations}
|
|
364
|
+
multipleTimelines={gsapMultipleTimelines}
|
|
365
|
+
unsupportedTimelinePattern={gsapUnsupportedTimelinePattern}
|
|
366
|
+
onUpdateProperty={onUpdateGsapProperty}
|
|
367
|
+
onUpdateMeta={onUpdateGsapMeta}
|
|
368
|
+
onDeleteAnimation={onDeleteGsapAnimation}
|
|
369
|
+
onAddProperty={onAddGsapProperty}
|
|
370
|
+
onRemoveProperty={onRemoveGsapProperty ?? (() => {})}
|
|
371
|
+
onAddAnimation={onAddGsapAnimation}
|
|
372
|
+
/>
|
|
373
|
+
)}
|
|
374
|
+
|
|
345
375
|
{showEditableSections && (
|
|
346
376
|
<StyleSections
|
|
347
377
|
projectId={projectId}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PatchTarget } from "../../utils/sourcePatcher";
|
|
2
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
2
3
|
|
|
3
4
|
export const CURATED_STYLE_PROPERTIES = [
|
|
4
5
|
"position",
|
|
@@ -86,6 +87,7 @@ export interface DomEditSelection extends PatchTarget {
|
|
|
86
87
|
computedStyles: Record<string, string>;
|
|
87
88
|
textFields: DomEditTextField[];
|
|
88
89
|
capabilities: DomEditCapabilities;
|
|
90
|
+
gsapAnimations?: GsapAnimation[];
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
export interface DomEditLayerItem {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { controlPointsForGsapEase } from "./studioMotion";
|
|
2
|
+
|
|
3
|
+
export const METHOD_LABELS: Record<string, string> = {
|
|
4
|
+
set: "Set",
|
|
5
|
+
to: "Animate",
|
|
6
|
+
from: "Animate In",
|
|
7
|
+
fromTo: "Animate",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const METHOD_TOOLTIPS: Record<string, string> = {
|
|
11
|
+
set: "Instantly snap to these values — no transition",
|
|
12
|
+
to: "Smoothly animate the element to these target values",
|
|
13
|
+
from: "Element starts at these values and transitions to its normal state",
|
|
14
|
+
fromTo: "Animate from one state to another",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const PROP_LABELS: Record<string, string> = {
|
|
18
|
+
x: "Move X",
|
|
19
|
+
y: "Move Y",
|
|
20
|
+
width: "Width",
|
|
21
|
+
height: "Height",
|
|
22
|
+
rotation: "Rotate",
|
|
23
|
+
opacity: "Opacity",
|
|
24
|
+
scale: "Scale",
|
|
25
|
+
scaleX: "Scale X",
|
|
26
|
+
scaleY: "Scale Y",
|
|
27
|
+
autoAlpha: "Visibility",
|
|
28
|
+
visibility: "Visible",
|
|
29
|
+
scaleX_alias: "Stretch X",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const PROP_UNITS: Record<string, string> = {
|
|
33
|
+
x: "px",
|
|
34
|
+
y: "px",
|
|
35
|
+
width: "px",
|
|
36
|
+
height: "px",
|
|
37
|
+
rotation: "°",
|
|
38
|
+
opacity: "%",
|
|
39
|
+
scale: "×",
|
|
40
|
+
scaleX: "×",
|
|
41
|
+
scaleY: "×",
|
|
42
|
+
autoAlpha: "%",
|
|
43
|
+
visibility: "",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const PROP_TOOLTIPS: Record<string, string> = {
|
|
47
|
+
x: "Move left/right (negative = left, positive = right)",
|
|
48
|
+
y: "Move up/down (negative = up, positive = down)",
|
|
49
|
+
opacity: "How visible (0 = invisible, 1 = fully visible)",
|
|
50
|
+
scale: "Size multiplier (1 = normal, 2 = double, 0.5 = half)",
|
|
51
|
+
scaleX: "Horizontal stretch (1 = normal)",
|
|
52
|
+
scaleY: "Vertical stretch (1 = normal)",
|
|
53
|
+
rotation: "Spin angle (360 = full rotation)",
|
|
54
|
+
width: "Element width",
|
|
55
|
+
height: "Element height",
|
|
56
|
+
autoAlpha: "Like opacity but hides element completely at 0",
|
|
57
|
+
visibility: "Show or hide the element",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const EASE_LABELS: Record<string, string> = {
|
|
61
|
+
none: "Constant speed",
|
|
62
|
+
"power1.out": "Gentle slowdown",
|
|
63
|
+
"power2.out": "Smooth slowdown",
|
|
64
|
+
"power3.out": "Snappy slowdown",
|
|
65
|
+
"power4.out": "Sharp slowdown",
|
|
66
|
+
"power1.in": "Gentle speedup",
|
|
67
|
+
"power2.in": "Smooth speedup",
|
|
68
|
+
"power3.in": "Strong speedup",
|
|
69
|
+
"power4.in": "Sharp speedup",
|
|
70
|
+
"power1.inOut": "Gentle ease",
|
|
71
|
+
"power2.inOut": "Smooth ease",
|
|
72
|
+
"power3.inOut": "Strong ease",
|
|
73
|
+
"power4.inOut": "Sharp ease",
|
|
74
|
+
"back.out": "Overshoot & settle",
|
|
75
|
+
"back.in": "Pull back & go",
|
|
76
|
+
"back.inOut": "Pull & overshoot",
|
|
77
|
+
"elastic.out": "Springy bounce",
|
|
78
|
+
"elastic.in": "Wind up spring",
|
|
79
|
+
"elastic.inOut": "Full spring",
|
|
80
|
+
"bounce.out": "Drop & bounce",
|
|
81
|
+
"bounce.in": "Reverse bounce",
|
|
82
|
+
"bounce.inOut": "Double bounce",
|
|
83
|
+
"expo.out": "Very snappy stop",
|
|
84
|
+
"expo.in": "Very slow start",
|
|
85
|
+
"expo.inOut": "Dramatic ease",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const EASE_CURVES: Record<string, [number, number, number, number]> = {
|
|
89
|
+
none: [0, 0, 1, 1],
|
|
90
|
+
"power1.out": [0, 0, 0.58, 1],
|
|
91
|
+
"power2.out": [0.16, 1, 0.3, 1],
|
|
92
|
+
"power3.out": [0.08, 0.82, 0.17, 1],
|
|
93
|
+
"power4.out": [0.06, 0.73, 0.09, 1],
|
|
94
|
+
"power1.in": [0.42, 0, 1, 1],
|
|
95
|
+
"power2.in": [0.55, 0.06, 0.68, 0.19],
|
|
96
|
+
"power3.in": [0.6, 0.04, 0.98, 0.34],
|
|
97
|
+
"power4.in": [0.7, 0, 0.84, 0],
|
|
98
|
+
"power1.inOut": [0.42, 0, 0.58, 1],
|
|
99
|
+
"power2.inOut": [0.45, 0.05, 0.55, 0.95],
|
|
100
|
+
"power3.inOut": [0.65, 0.05, 0.35, 1],
|
|
101
|
+
"power4.inOut": [0.76, 0, 0.24, 1],
|
|
102
|
+
"back.out": [0.34, 1.56, 0.64, 1],
|
|
103
|
+
"back.in": [0.36, 0, 0.66, -0.56],
|
|
104
|
+
"back.inOut": [0.68, -0.55, 0.27, 1.55],
|
|
105
|
+
"expo.out": [0.16, 1, 0.3, 1],
|
|
106
|
+
"expo.in": [0.7, 0, 0.84, 0],
|
|
107
|
+
"expo.inOut": [0.87, 0, 0.13, 1],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export function parseCustomEaseFromString(ease: string): {
|
|
111
|
+
x1: number;
|
|
112
|
+
y1: number;
|
|
113
|
+
x2: number;
|
|
114
|
+
y2: number;
|
|
115
|
+
} {
|
|
116
|
+
const match = ease.match(/^custom\((.+)\)$/);
|
|
117
|
+
if (!match) return controlPointsForGsapEase("power2.out");
|
|
118
|
+
const data = match[1];
|
|
119
|
+
const nums = data.match(/[\d.]+/g)?.map(Number);
|
|
120
|
+
if (!nums || nums.length < 6) return controlPointsForGsapEase("power2.out");
|
|
121
|
+
return { x1: nums[2], y1: nums[3], x2: nums[4], y2: nums[5] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const ADD_METHODS = ["to", "from", "set"] as const;
|
|
125
|
+
|
|
126
|
+
export const ADD_METHOD_LABELS: Record<string, string> = {
|
|
127
|
+
to: "Animate",
|
|
128
|
+
from: "Animate In",
|
|
129
|
+
set: "Set Instantly",
|
|
130
|
+
};
|
|
@@ -65,6 +65,12 @@ export const STUDIO_BLOCKS_PANEL_ENABLED = resolveStudioBooleanEnvFlag(
|
|
|
65
65
|
true,
|
|
66
66
|
);
|
|
67
67
|
|
|
68
|
+
export const STUDIO_GSAP_PANEL_ENABLED = resolveStudioBooleanEnvFlag(
|
|
69
|
+
env,
|
|
70
|
+
["VITE_STUDIO_ENABLE_GSAP_PANEL", "VITE_STUDIO_GSAP_PANEL_ENABLED"],
|
|
71
|
+
false,
|
|
72
|
+
);
|
|
73
|
+
|
|
68
74
|
export const STUDIO_PREVIEW_SELECTION_ENABLED = STUDIO_INSPECTOR_PANELS_ENABLED;
|
|
69
75
|
|
|
70
76
|
export const STUDIO_MANUAL_EDITING_DISABLED_TITLE = "Manual editing is temporarily disabled";
|