@hyperframes/studio 0.6.53 → 0.6.55

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.
@@ -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&apos;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 currentRaw = readStudioPathOffset(element.element);
208
- const currentGsap = readGsapTranslateFromTransform(element.element);
219
+ const current = readStudioPathOffset(element.element);
209
220
  onSetManualOffset(element, {
210
- x: axis === "x" ? parsed - currentGsap.x : currentRaw.x,
211
- y: axis === "y" ? parsed - currentGsap.y : currentRaw.y,
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(visualOffset.x)}
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(visualOffset.y)}
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";