diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index 2f944b228..f100423b2 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -389,9 +389,7 @@ export function StudioApp() { ); const { - selectedStudioMotion, designPanelActive, - motionPanelActive, inspectorPanelActive, inspectorButtonActive, shouldShowSelectedDomBounds, @@ -399,7 +397,6 @@ export function StudioApp() { panelLayout.rightPanelTab, panelLayout.rightCollapsed, isPlaying, - domEditSession.domEditSelection, gestureState === "recording", ); @@ -530,9 +527,7 @@ export function StudioApp() { {!panelLayout.rightCollapsed && ( { setActiveBlockParams(null); diff --git a/packages/studio/src/components/StudioRightPanel.tsx b/packages/studio/src/components/StudioRightPanel.tsx index 9d0bd4916..1cb60fc78 100644 --- a/packages/studio/src/components/StudioRightPanel.tsx +++ b/packages/studio/src/components/StudioRightPanel.tsx @@ -1,20 +1,12 @@ import { Tooltip } from "./ui"; import { PropertyPanel } from "./editor/PropertyPanel"; -import { MotionPanel } from "./editor/MotionPanel"; import { LayersPanel } from "./editor/LayersPanel"; import { CaptionPropertyPanel } from "../captions/components/CaptionPropertyPanel"; import { BlockParamsPanel } from "./editor/BlockParamsPanel"; import { RenderQueue } from "./renders/RenderQueue"; import type { RenderJob } from "./renders/useRenderQueue"; -import type { StudioGsapMotion } from "./editor/studioMotion"; import type { BlockParam } from "@hyperframes/core/registry"; -import { - STUDIO_INSPECTOR_PANELS_ENABLED, - STUDIO_MOTION_PANEL_ENABLED, -} from "./editor/manualEditingAvailability"; - -/** Motion data without targeting metadata. */ -type StudioMotionData = Omit; +import { STUDIO_INSPECTOR_PANELS_ENABLED } from "./editor/manualEditingAvailability"; import { useStudioContext } from "../contexts/StudioContext"; import { usePanelLayoutContext } from "../contexts/PanelLayoutContext"; @@ -23,9 +15,7 @@ import { useDomEditContext } from "../contexts/DomEditContext"; import { usePlayerStore } from "../player"; export interface StudioRightPanelProps { - selectedStudioMotion: StudioMotionData | null; designPanelActive: boolean; - motionPanelActive: boolean; activeBlockParams?: { blockName: string; blockTitle: string; @@ -40,9 +30,7 @@ export interface StudioRightPanelProps { // fallow-ignore-next-line complexity export function StudioRightPanel({ - selectedStudioMotion, designPanelActive, - motionPanelActive, activeBlockParams, onCloseBlockParams, recordingState, @@ -84,8 +72,6 @@ export function StudioRightPanel({ handleDomAddTextField, handleDomRemoveTextField, handleAskAgent, - handleDomMotionCommit, - handleDomMotionClear, selectedGsapAnimations, gsapMultipleTimelines, gsapUnsupportedTimelinePattern, @@ -159,21 +145,6 @@ export function StudioRightPanel({ Layers - {STUDIO_MOTION_PANEL_ENABLED && ( - - - - )} )} @@ -248,14 +219,6 @@ export function StudioRightPanel({ recordingDuration={recordingDuration} onToggleRecording={onToggleRecording} /> - ) : motionPanelActive ? ( - 1 ? null : domEditSelection} - motion={selectedStudioMotion} - onClearSelection={clearDomSelection} - onSetMotion={handleDomMotionCommit} - onClearMotion={handleDomMotionClear} - /> ) : ( { x: number; y: number }, -): string { - const commands: string[] = []; - for (let index = 0; index <= 48; index += 1) { - const point = map(cubicBezierPoint(index / 48, points)); - commands.push(`${index === 0 ? "M" : "L"}${point.x.toFixed(2)},${point.y.toFixed(2)}`); - } - return commands.join(" "); -} - -export function EaseCurveEditor({ - points, - onCommit, -}: { - points: StudioCustomEaseControlPoints; - onCommit: (points: StudioCustomEaseControlPoints) => void; -}) { - const svgRef = useRef(null); - const [draft, setDraft] = useState(points); - const draggingRef = useRef<"p1" | "p2" | null>(null); - - useEffect(() => { - setDraft(points); - }, [points]); - - const width = 324; - const height = 214; - const plot = { left: 46, top: 24, width: 242, height: 146 }; - const yMin = -0.4; - const yMax = 1.4; - - const mapPoint = (point: { x: number; y: number }) => ({ - x: plot.left + point.x * plot.width, - y: plot.top + ((yMax - point.y) / (yMax - yMin)) * plot.height, - }); - - const unmapPointer = (event: PointerEvent) => { - const rect = svgRef.current?.getBoundingClientRect(); - if (!rect) return null; - const x = ((event.clientX - rect.left) / rect.width) * width; - const y = ((event.clientY - rect.top) / rect.height) * height; - return clampStudioCustomEasePoints({ - x1: draggingRef.current === "p1" ? (x - plot.left) / plot.width : draft.x1, - y1: - draggingRef.current === "p1" - ? yMax - ((y - plot.top) / plot.height) * (yMax - yMin) - : draft.y1, - x2: draggingRef.current === "p2" ? (x - plot.left) / plot.width : draft.x2, - y2: - draggingRef.current === "p2" - ? yMax - ((y - plot.top) / plot.height) * (yMax - yMin) - : draft.y2, - }); - }; - - const start = mapPoint({ x: 0, y: 0 }); - const end = mapPoint({ x: 1, y: 1 }); - const p1 = mapPoint({ x: draft.x1, y: draft.y1 }); - const p2 = mapPoint({ x: draft.x2, y: draft.y2 }); - const curvePath = buildCurvePath(draft, mapPoint); - - const handlePointerMove = (event: PointerEvent) => { - if (!draggingRef.current) return; - event.preventDefault(); - const next = unmapPointer(event); - if (next) setDraft(next); - }; - - const endDrag = () => { - if (!draggingRef.current) return; - draggingRef.current = null; - onCommit(draft); - }; - - const startDrag = (handle: "p1" | "p2", event: PointerEvent) => { - event.preventDefault(); - event.stopPropagation(); - draggingRef.current = handle; - event.currentTarget.setPointerCapture(event.pointerId); - }; - - return ( -
-
-
-
CustomEase
-
- {serializeStudioCustomEaseData(draft)} -
-
- -
- - - {[0, 0.5, 1].map((value) => { - const mapped = mapPoint({ x: 0, y: value }); - return ( - - - - {value} - - - ); - })} - - - - - - - - startDrag("p1", event)} - /> - startDrag("p2", event)} - /> - - P1 - - - P2 - - -
-
- P1 {formatNumericValue(draft.x1)}, {formatNumericValue(draft.y1)} -
-
- P2 {formatNumericValue(draft.x2)}, {formatNumericValue(draft.y2)} -
-
-
- ); -} diff --git a/packages/studio/src/components/editor/MotionPanel.tsx b/packages/studio/src/components/editor/MotionPanel.tsx deleted file mode 100644 index 2887c0a76..000000000 --- a/packages/studio/src/components/editor/MotionPanel.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import { memo, useMemo } from "react"; -import { X, Zap } from "../../icons/SystemIcons"; -import type { DomEditSelection } from "./domEditing"; -import { - STUDIO_GSAP_EASE_OPTIONS, - buildStudioGsapPresetMotion, - controlPointsForGsapEase, - parseStudioCustomEaseData, - serializeStudioCustomEaseData, - type StudioCustomEaseControlPoints, - type StudioGsapMotion, - type StudioGsapMotionDirection, - type StudioGsapMotionPreset, -} from "./studioMotion"; -import { - formatNumericValue, - clampMotionNumber, - parsePlainNumber, - DetailField, - SegmentedControl, - SelectField, - MotionSection, - RESPONSIVE_GRID, -} from "./MotionPanelFields"; -import { EaseCurveEditor } from "./EaseCurveEditor"; - -/** Motion data without targeting metadata (kind/target/updatedAt are derived from context). */ -type StudioMotionData = Omit; - -interface MotionPanelProps { - element: DomEditSelection | null; - motion: StudioMotionData | null; - onClearSelection: () => void; - onSetMotion: (element: DomEditSelection, motion: StudioMotionData) => void; - onClearMotion: (element: DomEditSelection) => void; -} - -const MOTION_PRESET_OPTIONS: Array<{ label: string; value: StudioGsapMotionPreset }> = [ - { label: "Fade Up", value: "fade-up" }, - { label: "Slide", value: "slide" }, - { label: "Pop", value: "pop" }, -]; - -const MOTION_DIRECTION_OPTIONS: StudioGsapMotionDirection[] = ["up", "down", "left", "right"]; - -function motionValueDistance(motion: StudioMotionData | null): number { - if (!motion) return 32; - return Math.max(Math.abs(motion.from.x ?? 0), Math.abs(motion.from.y ?? 0), 1); -} - -function inferMotionPreset(motion: StudioMotionData | null): StudioGsapMotionPreset { - if (!motion) return "fade-up"; - if (motion.from.scale != null || motion.to.scale != null) return "pop"; - if (motion.from.x != null || motion.to.x != null) return "slide"; - return "fade-up"; -} - -function inferMotionDirection(motion: StudioMotionData | null): StudioGsapMotionDirection { - if (!motion) return "up"; - const x = motion.from.x ?? 0; - const y = motion.from.y ?? 0; - if (Math.abs(x) > Math.abs(y)) return x < 0 ? "right" : "left"; - return y < 0 ? "down" : "up"; -} - -function buildStudioCustomEaseId(element: DomEditSelection): string { - const source = element.id || element.selector || element.label || "layer"; - const normalized = source - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-|-$/g, ""); - return `studio-${normalized || "layer"}-ease`; -} - -export const MotionPanel = memo(function MotionPanel({ - element, - motion, - onClearSelection, - onSetMotion, - onClearMotion, -}: MotionPanelProps) { - const activeMotionPreset = inferMotionPreset(motion); - const activeMotionDirection = inferMotionDirection(motion); - const activeMotionStart = motion?.start ?? 0; - const activeMotionDuration = motion?.duration ?? 0.6; - const activeMotionDistance = motionValueDistance(motion); - const activeMotionEase = motion?.ease ?? "power3.out"; - const customEaseData = motion?.customEase?.data ?? ""; - const customEaseActive = customEaseData.trim().length > 0; - const activeCustomEasePoints = useMemo( - () => - parseStudioCustomEaseData(customEaseData) ?? - controlPointsForGsapEase( - STUDIO_GSAP_EASE_OPTIONS.includes( - activeMotionEase as (typeof STUDIO_GSAP_EASE_OPTIONS)[number], - ) - ? activeMotionEase - : "power3.out", - ), - [activeMotionEase, customEaseData], - ); - - if (!element) { - return ( -
- -

Select an element for motion.

-

- Timeline layers and inspector selections can receive Studio-authored GSAP motion. -

-
- ); - } - - const sourceLabel = element.id ? `#${element.id}` : element.selector; - const easeSelectValue = customEaseActive - ? "CustomEase" - : STUDIO_GSAP_EASE_OPTIONS.includes( - activeMotionEase as (typeof STUDIO_GSAP_EASE_OPTIONS)[number], - ) - ? activeMotionEase - : "power3.out"; - const easeSelectOptions = customEaseActive - ? ["CustomEase", ...STUDIO_GSAP_EASE_OPTIONS] - : STUDIO_GSAP_EASE_OPTIONS; - - const commitMotion = ( - overrides: Partial<{ - preset: StudioGsapMotionPreset; - direction: StudioGsapMotionDirection; - start: number; - duration: number; - distance: number; - ease: string; - customEaseData: string; - }>, - ) => { - const customEaseText = overrides.customEaseData ?? customEaseData; - const customEase = customEaseText.trim() - ? { - id: motion?.customEase?.id ?? buildStudioCustomEaseId(element), - data: customEaseText.trim(), - } - : undefined; - const nextEase = customEase - ? customEase.id - : (overrides.ease ?? activeMotionEase).trim() || "none"; - onSetMotion( - element, - buildStudioGsapPresetMotion(overrides.preset ?? activeMotionPreset, { - start: clampMotionNumber(overrides.start ?? activeMotionStart, 0, 3600, 0), - duration: clampMotionNumber(overrides.duration ?? activeMotionDuration, 0.01, 3600, 0.6), - distance: clampMotionNumber(overrides.distance ?? activeMotionDistance, 1, 2000, 32), - direction: overrides.direction ?? activeMotionDirection, - ease: nextEase, - customEase, - }), - ); - }; - - const commitCustomEase = (points: StudioCustomEaseControlPoints) => { - commitMotion({ - ease: buildStudioCustomEaseId(element), - customEaseData: serializeStudioCustomEaseData(points), - }); - }; - - return ( -
-
-
-
-
- Motion Target -
-
- {element.label} -
-
{sourceLabel}
-
- -
-
- -
- - GSAP -
- } - > -
- commitMotion({ preset: next as StudioGsapMotionPreset })} - options={MOTION_PRESET_OPTIONS} - /> -
- commitMotion({ direction: next as StudioGsapMotionDirection })} - options={MOTION_DIRECTION_OPTIONS} - /> - { - if (next === "CustomEase") return; - commitMotion({ ease: next, customEaseData: "" }); - }} - options={easeSelectOptions} - /> -
-
- commitMotion({ start: parsePlainNumber(next) ?? 0 })} - /> - commitMotion({ duration: parsePlainNumber(next) ?? 0.6 })} - /> - commitMotion({ distance: parsePlainNumber(next) ?? 32 })} - /> -
-
- - - - CustomEase -
- } - > -
- - { - const parsed = parseStudioCustomEaseData(next); - if (parsed) commitCustomEase(parsed); - }} - /> -
- -
-
- - - - ); -}); diff --git a/packages/studio/src/components/editor/MotionPanelFields.tsx b/packages/studio/src/components/editor/MotionPanelFields.tsx deleted file mode 100644 index 15b64d290..000000000 --- a/packages/studio/src/components/editor/MotionPanelFields.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { useState, useRef, useEffect, type ReactNode } from "react"; -import { Zap } from "../../icons/SystemIcons"; - -const FIELD = - "min-w-0 rounded-xl border border-neutral-800 bg-neutral-900/95 px-3 py-2 text-neutral-100 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)] transition-colors focus-within:border-neutral-600"; -export const LABEL = "text-[11px] font-medium uppercase tracking-[0.18em] text-neutral-500"; -export const RESPONSIVE_GRID = "grid grid-cols-[repeat(auto-fit,minmax(118px,1fr))] gap-3"; - -export function formatNumericValue(value: number): string { - const rounded = Math.round(value * 100) / 100; - return Number.isInteger(rounded) - ? `${rounded}` - : rounded.toFixed(2).replace(/0+$/, "").replace(/\.$/, ""); -} - -export function clampMotionNumber( - value: number | null, - min: number, - max: number, - fallback: number, -): number { - if (value == null || !Number.isFinite(value)) return fallback; - return Math.min(max, Math.max(min, value)); -} - -export function parsePlainNumber(value: string): number | null { - const parsed = Number.parseFloat(value.trim()); - return Number.isFinite(parsed) ? parsed : null; -} - -// ── CommitField ── - -function CommitField({ - value, - disabled, - onCommit, -}: { - value: string; - disabled?: boolean; - onCommit: (nextValue: string) => void; -}) { - const [draft, setDraft] = useState(value); - const focusedRef = useRef(false); - - useEffect(() => { - if (!focusedRef.current) setDraft(value); - }, [value]); - - const commitDraft = () => { - focusedRef.current = false; - const next = draft.trim(); - if (next !== value) onCommit(next); - }; - - return ( - { - focusedRef.current = true; - }} - onChange={(event) => setDraft(event.target.value)} - onBlur={commitDraft} - onKeyDown={(event) => { - if (event.key === "Enter") (event.target as HTMLInputElement).blur(); - }} - className="w-full min-w-0 bg-transparent text-[11px] font-medium text-neutral-100 outline-none disabled:cursor-not-allowed disabled:text-neutral-600" - /> - ); -} - -// ── DetailField ── - -export function DetailField({ - label, - value, - disabled, - onCommit, -}: { - label: string; - value: string; - disabled?: boolean; - onCommit: (nextValue: string) => void; -}) { - return ( - - ); -} - -// ── SegmentedControl ── - -export function SegmentedControl({ - value, - options, - onChange, -}: { - value: string; - options: Array<{ label: string; value: string }>; - onChange: (value: string) => void; -}) { - return ( -
- {options.map((option) => ( - - ))} -
- ); -} - -// ── SelectField ── - -export function SelectField({ - label, - value, - options, - onChange, -}: { - label: string; - value: string; - options: readonly string[]; - onChange: (value: string) => void; -}) { - return ( - - ); -} - -// ── MotionSection ── - -export function MotionSection({ - title, - children, - accessory, -}: { - title: string; - children: ReactNode; - accessory?: ReactNode; -}) { - return ( -
-
-
- -

- {title} -

-
- {accessory} -
- {children} -
- ); -} diff --git a/packages/studio/src/components/editor/MotionPathOverlay.tsx b/packages/studio/src/components/editor/MotionPathOverlay.tsx deleted file mode 100644 index 49ea39dd0..000000000 --- a/packages/studio/src/components/editor/MotionPathOverlay.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { memo, useMemo, type RefObject } from "react"; -import type { ArcPathConfig } from "@hyperframes/core/gsap-parser"; - -interface MotionPathOverlayProps { - iframeRef: RefObject; - arcPath: ArcPathConfig | null; - waypoints: Array<{ x: number; y: number }> | null; - elementBaseRect: { left: number; top: number; scaleX: number; scaleY: number } | null; -} - -function buildSvgPath( - waypoints: Array<{ x: number; y: number }>, - segments: ArcPathConfig["segments"], - base: { left: number; top: number; scaleX: number; scaleY: number }, -): string { - if (waypoints.length < 2) return ""; - - const toPixel = (wp: { x: number; y: number }) => ({ - x: base.left + wp.x * base.scaleX, - y: base.top + wp.y * base.scaleY, - }); - - const first = toPixel(waypoints[0]!); - const parts = [`M ${first.x} ${first.y}`]; - - for (let i = 0; i < segments.length && i < waypoints.length - 1; i++) { - const seg = segments[i]!; - const end = toPixel(waypoints[i + 1]!); - - if (seg.cp1 && seg.cp2) { - const c1 = toPixel(seg.cp1); - const c2 = toPixel(seg.cp2); - parts.push(`C ${c1.x} ${c1.y} ${c2.x} ${c2.y} ${end.x} ${end.y}`); - } else { - const start = toPixel(waypoints[i]!); - const dx = end.x - start.x; - const dy = end.y - start.y; - const c = seg.curviness ?? 1; - const offset = c * Math.abs(dx) * 0.25; - const c1x = start.x + dx * 0.33; - const c1y = start.y + dy * 0.33 - offset; - const c2x = start.x + dx * 0.66; - const c2y = start.y + dy * 0.66 - offset; - parts.push(`C ${c1x} ${c1y} ${c2x} ${c2y} ${end.x} ${end.y}`); - } - } - - return parts.join(" "); -} - -export const MotionPathOverlay = memo(function MotionPathOverlay({ - arcPath, - waypoints, - elementBaseRect, -}: MotionPathOverlayProps) { - const pathD = useMemo(() => { - if (!arcPath?.enabled || !waypoints || waypoints.length < 2 || !elementBaseRect) return ""; - return buildSvgPath(waypoints, arcPath.segments, elementBaseRect); - }, [arcPath, waypoints, elementBaseRect]); - - const anchorPoints = useMemo(() => { - if (!waypoints || !elementBaseRect) return []; - return waypoints.map((wp) => ({ - x: elementBaseRect.left + wp.x * elementBaseRect.scaleX, - y: elementBaseRect.top + wp.y * elementBaseRect.scaleY, - })); - }, [waypoints, elementBaseRect]); - - const controlPoints = useMemo(() => { - if (!arcPath?.enabled || !elementBaseRect) return []; - const points: Array<{ - segIndex: number; - type: "cp1" | "cp2"; - x: number; - y: number; - anchorX: number; - anchorY: number; - }> = []; - for (let i = 0; i < arcPath.segments.length; i++) { - const seg = arcPath.segments[i]!; - if (seg.cp1 && seg.cp2 && waypoints) { - const anchor1 = waypoints[i]!; - const anchor2 = waypoints[i + 1]!; - points.push({ - segIndex: i, - type: "cp1", - x: elementBaseRect.left + seg.cp1.x * elementBaseRect.scaleX, - y: elementBaseRect.top + seg.cp1.y * elementBaseRect.scaleY, - anchorX: elementBaseRect.left + anchor1.x * elementBaseRect.scaleX, - anchorY: elementBaseRect.top + anchor1.y * elementBaseRect.scaleY, - }); - points.push({ - segIndex: i, - type: "cp2", - x: elementBaseRect.left + seg.cp2.x * elementBaseRect.scaleX, - y: elementBaseRect.top + seg.cp2.y * elementBaseRect.scaleY, - anchorX: elementBaseRect.left + anchor2.x * elementBaseRect.scaleX, - anchorY: elementBaseRect.top + anchor2.y * elementBaseRect.scaleY, - }); - } - } - return points; - }, [arcPath, waypoints, elementBaseRect]); - - if (!pathD) return null; - - return ( - - - - {controlPoints.map((cp) => ( - - - - - ))} - - {anchorPoints.map((pt, i) => ( - - ))} - - ); -}); diff --git a/packages/studio/src/components/editor/SpringEaseEditor.tsx b/packages/studio/src/components/editor/SpringEaseEditor.tsx deleted file mode 100644 index 852f2a32d..000000000 --- a/packages/studio/src/components/editor/SpringEaseEditor.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import { useState, useRef, useEffect, useCallback } from "react"; -import { generateSpringEaseData, SPRING_PRESETS } from "@hyperframes/core/spring-ease"; -import { LABEL } from "./MotionPanelFields"; -import { RotateCcw } from "../../icons/SystemIcons"; - -interface SpringParams { - mass: number; - stiffness: number; - damping: number; -} - -const DEFAULT_SPRING: SpringParams = { mass: 1, stiffness: 180, damping: 12 }; - -const SLIDERS: { - key: keyof SpringParams; - label: string; - min: number; - max: number; - step: number; -}[] = [ - { key: "mass", label: "Mass", min: 0.1, max: 5, step: 0.1 }, - { key: "stiffness", label: "Stiffness", min: 10, max: 500, step: 10 }, - { key: "damping", label: "Damping", min: 1, max: 50, step: 1 }, -]; - -function springValue(mass: number, stiffness: number, damping: number, t: number): number { - const w0 = Math.sqrt(stiffness / mass); - const zeta = damping / (2 * Math.sqrt(stiffness * mass)); - if (zeta < 1) { - const wd = w0 * Math.sqrt(1 - zeta * zeta); - return ( - 1 - Math.exp(-zeta * w0 * t) * (Math.cos(wd * t) + ((zeta * w0) / wd) * Math.sin(wd * t)) - ); - } - if (zeta === 1) { - return 1 - (1 + w0 * t) * Math.exp(-w0 * t); - } - const s1 = -w0 * (zeta - Math.sqrt(zeta * zeta - 1)); - const s2 = -w0 * (zeta + Math.sqrt(zeta * zeta - 1)); - return 1 + (s1 * Math.exp(s2 * t) - s2 * Math.exp(s1 * t)) / (s2 - s1); -} - -function springSimDuration(mass: number, stiffness: number, damping: number): number { - const w0 = Math.sqrt(stiffness / mass); - const zeta = damping / (2 * Math.sqrt(stiffness * mass)); - if (zeta < 1) return Math.min(5 / (zeta * w0), 10); - const decayRate = zeta * w0 - w0 * Math.sqrt(zeta * zeta - 1); - return Math.min(4 / Math.max(decayRate, 0.01), 10); -} - -function buildSpringPath( - params: SpringParams, - mapFn: (point: { x: number; y: number }) => { x: number; y: number }, -): string { - const steps = 64; - const simDur = springSimDuration(params.mass, params.stiffness, params.damping); - const commands: string[] = []; - for (let i = 0; i <= steps; i++) { - const t = i / steps; - const simT = t * simDur; - const y = springValue(params.mass, params.stiffness, params.damping, simT); - const mapped = mapFn({ x: t, y }); - commands.push(`${i === 0 ? "M" : "L"}${mapped.x.toFixed(2)},${mapped.y.toFixed(2)}`); - } - return commands.join(" "); -} - -export function SpringEaseEditor({ - onCommit, -}: { - onCommit: (easeId: string, easeData: string) => void; -}) { - const [params, setParams] = useState(DEFAULT_SPRING); - const commitTimeoutRef = useRef | null>(null); - - const scheduleCommit = useCallback( - (next: SpringParams) => { - if (commitTimeoutRef.current) clearTimeout(commitTimeoutRef.current); - commitTimeoutRef.current = setTimeout(() => { - const data = generateSpringEaseData(next.mass, next.stiffness, next.damping); - const id = `spring-m${next.mass}-k${next.stiffness}-d${next.damping}`; - onCommit(id, data); - }, 120); - }, - [onCommit], - ); - - useEffect(() => { - return () => { - if (commitTimeoutRef.current) clearTimeout(commitTimeoutRef.current); - }; - }, []); - - const updateParam = (key: keyof SpringParams, value: number) => { - const next = { ...params, [key]: value }; - setParams(next); - scheduleCommit(next); - }; - - const applyPreset = (preset: (typeof SPRING_PRESETS)[number]) => { - const next: SpringParams = { - mass: preset.mass, - stiffness: preset.stiffness, - damping: preset.damping, - }; - setParams(next); - const data = generateSpringEaseData(next.mass, next.stiffness, next.damping); - onCommit(preset.name, data); - }; - - const reset = () => { - setParams(DEFAULT_SPRING); - const data = generateSpringEaseData( - DEFAULT_SPRING.mass, - DEFAULT_SPRING.stiffness, - DEFAULT_SPRING.damping, - ); - onCommit("spring-bouncy", data); - }; - - // SVG layout matching EaseCurveEditor proportions - const width = 324; - const height = 214; - const plot = { left: 46, top: 24, width: 242, height: 146 }; - const yMin = -0.2; - const yMax = 1.3; - - const mapPoint = (point: { x: number; y: number }) => ({ - x: plot.left + point.x * plot.width, - y: plot.top + ((yMax - point.y) / (yMax - yMin)) * plot.height, - }); - - const curvePath = buildSpringPath(params, mapPoint); - const start = mapPoint({ x: 0, y: 0 }); - const end = mapPoint({ x: 1, y: 1 }); - - const activePreset = SPRING_PRESETS.find( - (p) => - p.mass === params.mass && p.stiffness === params.stiffness && p.damping === params.damping, - ); - - return ( -
-
-
-
Spring Ease
-
- {activePreset?.label ?? `m${params.mass} k${params.stiffness} d${params.damping}`} -
-
- -
- - {/* Curve preview */} - - - {[0, 0.5, 1].map((value) => { - const mapped = mapPoint({ x: 0, y: value }); - return ( - - - - {value} - - - ); - })} - - - - - - - - {/* Presets */} -
- {SPRING_PRESETS.map((preset) => { - const isActive = - preset.mass === params.mass && - preset.stiffness === params.stiffness && - preset.damping === params.damping; - return ( - - ); - })} -
- - {/* Sliders */} -
- {SLIDERS.map((slider) => ( -
-
- - {slider.label} - - - {params[slider.key]} - -
- updateParam(slider.key, Number(e.target.value))} - className="h-1 w-full cursor-pointer appearance-none rounded-full bg-neutral-800 accent-yellow-400 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-yellow-400" - /> -
- ))} -
-
- ); -} diff --git a/packages/studio/src/components/editor/manualEditingAvailability.test.ts b/packages/studio/src/components/editor/manualEditingAvailability.test.ts index 0abab2d5f..58aea3540 100644 --- a/packages/studio/src/components/editor/manualEditingAvailability.test.ts +++ b/packages/studio/src/components/editor/manualEditingAvailability.test.ts @@ -16,13 +16,12 @@ describe("manual editing availability", () => { vi.resetModules(); }); - it("enables inspector selection and manual dragging by default while motion stays opt-in", async () => { + it("enables inspector selection and manual dragging by default", async () => { const availability = await loadAvailabilityWithEnv({}); expect(availability.STUDIO_PREVIEW_MANUAL_EDITING_ENABLED).toBe(true); expect(availability.STUDIO_PREVIEW_SELECTION_ENABLED).toBe(true); expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(true); - expect(availability.STUDIO_MOTION_PANEL_ENABLED).toBe(false); }); it("enables GSAP drag intercept by default", async () => { diff --git a/packages/studio/src/components/editor/manualEditingAvailability.ts b/packages/studio/src/components/editor/manualEditingAvailability.ts index f7c8de005..7c702ac5f 100644 --- a/packages/studio/src/components/editor/manualEditingAvailability.ts +++ b/packages/studio/src/components/editor/manualEditingAvailability.ts @@ -2,7 +2,6 @@ export type StudioFeatureFlagEnv = Record; const STUDIO_PREVIEW_MANUAL_DRAGGING_ENV = "VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING"; const STUDIO_INSPECTOR_PANELS_ENV = "VITE_STUDIO_ENABLE_INSPECTOR_PANELS"; -const STUDIO_MOTION_PANEL_ENV = "VITE_STUDIO_ENABLE_MOTION_PANEL"; const TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on", "enabled"]); const FALSY_ENV_VALUES = new Set(["0", "false", "no", "off", "disabled"]); @@ -53,12 +52,6 @@ export const STUDIO_INSPECTOR_PANELS_ENABLED = resolveStudioBooleanEnvFlag( true, ); -export const STUDIO_MOTION_PANEL_ENABLED = resolveStudioBooleanEnvFlag( - env, - [STUDIO_MOTION_PANEL_ENV, "VITE_STUDIO_MOTION_PANEL_ENABLED"], - false, -); - export const STUDIO_BLOCKS_PANEL_ENABLED = resolveStudioBooleanEnvFlag( env, ["VITE_STUDIO_ENABLE_BLOCKS_PANEL", "VITE_STUDIO_BLOCKS_PANEL_ENABLED"], diff --git a/packages/studio/src/contexts/DomEditContext.tsx b/packages/studio/src/contexts/DomEditContext.tsx index e75e925de..a0ee1403c 100644 --- a/packages/studio/src/contexts/DomEditContext.tsx +++ b/packages/studio/src/contexts/DomEditContext.tsx @@ -37,8 +37,7 @@ export function DomEditProvider({ handleDomBoxSizeCommit, handleDomRotationCommit, handleDomManualEditsReset, - handleDomMotionCommit, - handleDomMotionClear, + handleDomTextCommit, handleDomTextFieldStyleCommit, handleDomAddTextField, @@ -111,8 +110,6 @@ export function DomEditProvider({ handleDomBoxSizeCommit, handleDomRotationCommit, handleDomManualEditsReset, - handleDomMotionCommit, - handleDomMotionClear, handleDomTextCommit, handleDomTextFieldStyleCommit, handleDomAddTextField, @@ -179,8 +176,6 @@ export function DomEditProvider({ handleDomBoxSizeCommit, handleDomRotationCommit, handleDomManualEditsReset, - handleDomMotionCommit, - handleDomMotionClear, handleDomTextCommit, handleDomTextFieldStyleCommit, handleDomAddTextField, diff --git a/packages/studio/src/hooks/useDomEditCommits.ts b/packages/studio/src/hooks/useDomEditCommits.ts index 8c59b69e5..e70e9f038 100644 --- a/packages/studio/src/hooks/useDomEditCommits.ts +++ b/packages/studio/src/hooks/useDomEditCommits.ts @@ -27,15 +27,7 @@ import { buildClearPathOffsetPatches, buildClearBoxSizePatches, buildClearRotationPatches, - buildMotionPatches, - buildClearMotionPatches, } from "../components/editor/manualEditsDom"; -import { - writeStudioMotionToElement, - clearStudioMotionFromElement, - applyStudioMotionFromDom, - type StudioGsapMotion, -} from "../components/editor/studioMotion"; import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets"; import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay"; import type { EditHistoryKind } from "../utils/editHistory"; @@ -400,64 +392,6 @@ export function useDomEditCommits({ [commitPositionPatchToHtml], ); - // ── Motion commits (HTML-attribute–backed) ── - - // fallow-ignore-next-line complexity - const handleDomMotionCommit = useCallback( - ( - selection: DomEditSelection, - motion: Omit, - ) => { - // 1. Write motion data as JSON attribute on the element - writeStudioMotionToElement(selection.element, motion); - // 2. Apply the GSAP timeline from DOM attributes - let doc: Document | null = null; - try { - doc = previewIframeRef.current?.contentDocument ?? null; - } catch { - // cross-origin guard - } - if (doc) applyStudioMotionFromDom(doc); - // 3. Build patches and persist to HTML - const patches = buildMotionPatches(selection.element); - commitPositionPatchToHtml(selection, patches, { - label: "Set GSAP motion", - coalesceKey: `motion:${getDomEditTargetKey(selection)}`, - }); - refreshDomEditSelectionFromPreview(selection); - }, - [commitPositionPatchToHtml, previewIframeRef, refreshDomEditSelectionFromPreview], - ); - - // fallow-ignore-next-line complexity - const handleDomMotionClear = useCallback( - (selection: DomEditSelection) => { - const clearPatches = buildClearMotionPatches(selection.element); - // Get gsap from the preview window for proper cleanup - let gsap: { set?: (target: HTMLElement, vars: Record) => void } | undefined; - try { - gsap = (previewIframeRef.current?.contentWindow as { gsap?: typeof gsap })?.gsap; - } catch { - // cross-origin guard - } - clearStudioMotionFromElement(selection.element, gsap); - let doc: Document | null = null; - try { - doc = previewIframeRef.current?.contentDocument ?? null; - } catch { - // cross-origin guard - } - if (doc) applyStudioMotionFromDom(doc); - commitPositionPatchToHtml(selection, clearPatches, { - label: "Clear GSAP motion", - coalesceKey: `motion:${getDomEditTargetKey(selection)}`, - skipRefresh: false, - }); - refreshDomEditSelectionFromPreview(selection); - }, - [commitPositionPatchToHtml, previewIframeRef, refreshDomEditSelectionFromPreview], - ); - // fallow-ignore-next-line complexity const handleDomEditElementDelete = useCallback( // fallow-ignore-next-line complexity @@ -592,8 +526,6 @@ export function useDomEditCommits({ handleDomBoxSizeCommit, handleDomRotationCommit, handleDomManualEditsReset, - handleDomMotionCommit, - handleDomMotionClear, handleDomEditElementDelete, handleDomZIndexReorderCommit, }; diff --git a/packages/studio/src/hooks/useDomEditSession.ts b/packages/studio/src/hooks/useDomEditSession.ts index 4bb7a2d08..6636efcaf 100644 --- a/packages/studio/src/hooks/useDomEditSession.ts +++ b/packages/studio/src/hooks/useDomEditSession.ts @@ -303,8 +303,6 @@ export function useDomEditSession({ handleDomBoxSizeCommit, handleDomRotationCommit, handleDomManualEditsReset, - handleDomMotionCommit, - handleDomMotionClear, handleDomEditElementDelete, handleDomZIndexReorderCommit, } = useDomEditCommits({ @@ -536,8 +534,6 @@ export function useDomEditSession({ handleDomBoxSizeCommit: handleGsapAwareBoxSizeCommit, handleDomRotationCommit: handleGsapAwareRotationCommit, handleDomManualEditsReset, - handleDomMotionCommit, - handleDomMotionClear, handleDomTextCommit, handleDomTextFieldStyleCommit, handleDomAddTextField, diff --git a/packages/studio/src/hooks/useStudioContextValue.ts b/packages/studio/src/hooks/useStudioContextValue.ts index ab1d1c8a8..9f56084be 100644 --- a/packages/studio/src/hooks/useStudioContextValue.ts +++ b/packages/studio/src/hooks/useStudioContextValue.ts @@ -1,11 +1,6 @@ import { useCallback, useMemo, useRef, useState, type DragEvent } from "react"; -import { - STUDIO_INSPECTOR_PANELS_ENABLED, - STUDIO_MOTION_PANEL_ENABLED, -} from "../components/editor/manualEditingAvailability"; -import { readStudioMotionFromElement } from "../components/editor/studioMotion"; +import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability"; import type { StudioContextValue } from "../contexts/StudioContext"; -import type { DomEditSelection } from "../components/editor/domEditing"; interface StudioContextInput { projectId: string; @@ -66,10 +61,8 @@ export function buildStudioContextValue(input: StudioContextInput): StudioContex } export interface InspectorState { - selectedStudioMotion: ReturnType | null; layersPanelActive: boolean; designPanelActive: boolean; - motionPanelActive: boolean; inspectorPanelActive: boolean; inspectorButtonActive: boolean; shouldShowSelectedDomBounds: boolean; @@ -79,32 +72,23 @@ export function useInspectorState( rightPanelTab: string, rightCollapsed: boolean, isPlaying: boolean, - domEditSelection: DomEditSelection | null, isGestureRecording?: boolean, ): InspectorState { // fallow-ignore-next-line complexity return useMemo(() => { - const selectedStudioMotion = - STUDIO_INSPECTOR_PANELS_ENABLED && domEditSelection - ? readStudioMotionFromElement(domEditSelection.element) - : null; const layersPanelActive = STUDIO_INSPECTOR_PANELS_ENABLED && rightPanelTab === "layers"; const designPanelActive = STUDIO_INSPECTOR_PANELS_ENABLED && rightPanelTab === "design"; - const motionPanelActive = - STUDIO_INSPECTOR_PANELS_ENABLED && STUDIO_MOTION_PANEL_ENABLED && rightPanelTab === "motion"; - const inspectorPanelActive = layersPanelActive || designPanelActive || motionPanelActive; + const inspectorPanelActive = layersPanelActive || designPanelActive; return { - selectedStudioMotion, layersPanelActive, designPanelActive, - motionPanelActive, inspectorPanelActive, inspectorButtonActive: STUDIO_INSPECTOR_PANELS_ENABLED && !rightCollapsed && inspectorPanelActive, shouldShowSelectedDomBounds: inspectorPanelActive && !rightCollapsed && !isPlaying && !isGestureRecording, }; - }, [rightPanelTab, rightCollapsed, isPlaying, domEditSelection, isGestureRecording]); + }, [rightPanelTab, rightCollapsed, isPlaying, isGestureRecording]); } // fallow-ignore-next-line complexity diff --git a/packages/studio/src/utils/studioHelpers.ts b/packages/studio/src/utils/studioHelpers.ts index b193364c4..91da8232d 100644 --- a/packages/studio/src/utils/studioHelpers.ts +++ b/packages/studio/src/utils/studioHelpers.ts @@ -12,7 +12,7 @@ export interface AppToast { tone: "error" | "info"; } -export type RightPanelTab = "layers" | "design" | "motion" | "renders" | "block-params"; +export type RightPanelTab = "layers" | "design" | "renders" | "block-params"; export interface AgentModalAnchorPoint { x: number; diff --git a/packages/studio/src/utils/studioUrlState.test.ts b/packages/studio/src/utils/studioUrlState.test.ts index 6b9620a8a..be0870c8f 100644 --- a/packages/studio/src/utils/studioUrlState.test.ts +++ b/packages/studio/src/utils/studioUrlState.test.ts @@ -159,7 +159,6 @@ describe("studio url state", () => { it("normalizes url tabs against feature flags", () => { expect(normalizeStudioUrlPanelTab("renders")).toBe("renders"); expect(normalizeStudioUrlPanelTab("layers", { inspectorPanelsEnabled: false })).toBe("renders"); - expect(normalizeStudioUrlPanelTab("motion", { motionPanelEnabled: false })).toBe("design"); }); it("hydrates seek first, preserves the initial url state, then restores selection", async () => { diff --git a/packages/studio/src/utils/studioUrlState.ts b/packages/studio/src/utils/studioUrlState.ts index 57af39b52..816178e0e 100644 --- a/packages/studio/src/utils/studioUrlState.ts +++ b/packages/studio/src/utils/studioUrlState.ts @@ -1,9 +1,6 @@ import type { RightPanelTab } from "./studioHelpers"; import { buildProjectHash, parseProjectHashRoute } from "./projectRouting"; -import { - STUDIO_INSPECTOR_PANELS_ENABLED, - STUDIO_MOTION_PANEL_ENABLED, -} from "../components/editor/manualEditingAvailability"; +import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability"; export interface StudioUrlSelectionState { sourceFile?: string; @@ -21,22 +18,19 @@ export interface StudioUrlState { selection: StudioUrlSelectionState | null; } -const VALID_TABS: RightPanelTab[] = ["layers", "design", "motion", "renders"]; +const VALID_TABS: RightPanelTab[] = ["layers", "design", "renders"]; export function normalizeStudioUrlPanelTab( tab: RightPanelTab | null, options: { inspectorPanelsEnabled?: boolean; - motionPanelEnabled?: boolean; } = {}, ): RightPanelTab | null { if (!tab) return null; if (!VALID_TABS.includes(tab)) return null; const inspectorPanelsEnabled = options.inspectorPanelsEnabled ?? STUDIO_INSPECTOR_PANELS_ENABLED; - const motionPanelEnabled = options.motionPanelEnabled ?? STUDIO_MOTION_PANEL_ENABLED; if (!inspectorPanelsEnabled && tab !== "renders") return "renders"; - if (tab === "motion" && !motionPanelEnabled) return "design"; return tab; }