mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(studio): html-backed motion panel (#873)
## Summary
Re-architects the studio motion panel to persist GSAP motion data directly in HTML element attributes instead of a `.hyperframes/studio-motion.json` JSON sidecar file. Same pattern as position/resize/rotation edits.
### Before
```
MotionPanel → commitStudioMotionManifestOptimistically()
→ writes .hyperframes/studio-motion.json
→ applyStudioMotionManifest(doc, manifest)
```
### After
```html
<div id="hero" data-hf-studio-motion='{"start":0.5,"duration":1,"ease":"power3.out","from":{"opacity":0,"y":40},"to":{"opacity":1,"y":0}}'>
```
```
MotionPanel → writeStudioMotionToElement(element, motion)
→ buildMotionPatches(element)
→ commitPositionPatchToHtml(selection, patches)
```
## What changed
- **studioMotionOps.ts** — Added `readStudioMotionFromElement()`, `writeStudioMotionToElement()`, `clearStudioMotionFromElement()` for attribute-based CRUD
- **studioMotion.ts** — Added `applyStudioMotionFromDom()` that reads motion from DOM attributes and builds GSAP timeline (kept `applyStudioMotionManifest` for render script compat)
- **manualEditsDom.ts** — Added `buildMotionPatches()` / `buildClearMotionPatches()`, integrated motion into `reapplyPositionEditsAfterSeek()`
- **useDomEditCommits.ts** — Rewrote `handleDomMotionCommit` / `handleDomMotionClear` to use HTML patching instead of manifest persistence
- **useManifestPersistence.ts** — Removed all motion manifest state (~200 lines): `studioMotionManifestRef`, `commitStudioMotionManifestOptimistically`, `applyStudioMotionToPreview`, motion SSE handler
- **App.tsx** — Reads motion from element attribute (`readStudioMotionFromElement`) instead of manifest ref
- **manualEditsRenderScript.ts** — Extended `studioPositionSeekReapplyRuntime` to rebuild GSAP motion timeline from `data-hf-studio-motion` attributes after each seek, including CustomEase support
- **htmlCompiler.ts** — Trigger seek-reapply script injection on `data-hf-studio-motion=` attributes
## Benefits
- No sidecar file — motion survives git, copy-paste, and manual HTML editing
- Undo/redo works via HTML source history (same as position edits)
- Renders correctly via CLI — seek-reapply script handles motion timeline rebuild
- Simpler architecture — one persistence path for all studio edits
## Test plan
- [x] `bun run build` passes
- [x] Pre-commit hooks pass (lint, format, typecheck)
- [ ] Set motion on element in Studio → `data-hf-studio-motion` attribute appears in HTML source
- [ ] Reload page → motion persists and plays correctly
- [ ] Clear motion → attribute removed, element returns to original state
- [ ] Undo/redo motion changes
- [ ] Render via CLI → motion visible in rendered video
- [ ] Seek animation → motion timeline re-syncs correctly
This commit is contained in:
@@ -24,14 +24,14 @@ import {
|
||||
} from "./MotionPanelFields";
|
||||
import { EaseCurveEditor } from "./EaseCurveEditor";
|
||||
|
||||
/** Motion data without targeting metadata (kind/target/updatedAt are derived from context). */
|
||||
type StudioMotionData = Omit<StudioGsapMotion, "kind" | "target" | "updatedAt">;
|
||||
|
||||
interface MotionPanelProps {
|
||||
element: DomEditSelection | null;
|
||||
motion: StudioGsapMotion | null;
|
||||
motion: StudioMotionData | null;
|
||||
onClearSelection: () => void;
|
||||
onSetMotion: (
|
||||
element: DomEditSelection,
|
||||
motion: Omit<StudioGsapMotion, "kind" | "target" | "updatedAt">,
|
||||
) => void;
|
||||
onSetMotion: (element: DomEditSelection, motion: StudioMotionData) => void;
|
||||
onClearMotion: (element: DomEditSelection) => void;
|
||||
}
|
||||
|
||||
@@ -43,19 +43,19 @@ const MOTION_PRESET_OPTIONS: Array<{ label: string; value: StudioGsapMotionPrese
|
||||
|
||||
const MOTION_DIRECTION_OPTIONS: StudioGsapMotionDirection[] = ["up", "down", "left", "right"];
|
||||
|
||||
function motionValueDistance(motion: StudioGsapMotion | null): number {
|
||||
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: StudioGsapMotion | null): StudioGsapMotionPreset {
|
||||
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: StudioGsapMotion | null): StudioGsapMotionDirection {
|
||||
function inferMotionDirection(motion: StudioMotionData | null): StudioGsapMotionDirection {
|
||||
if (!motion) return "up";
|
||||
const x = motion.from.x ?? 0;
|
||||
const y = motion.from.y ?? 0;
|
||||
|
||||
Reference in New Issue
Block a user