mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +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:
@@ -2,7 +2,6 @@ import { useEffect } from "react";
|
||||
import type { TimelineElement } from "../player";
|
||||
import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
|
||||
import { findElementForSelection } from "../components/editor/domEditing";
|
||||
import type { StudioMotionManifest } from "../components/editor/studioMotion";
|
||||
import type { ImportedFontAsset } from "../components/editor/fontAssets";
|
||||
import type { EditHistoryKind } from "../utils/editHistory";
|
||||
import type { RightPanelTab } from "../utils/studioHelpers";
|
||||
@@ -36,11 +35,6 @@ export interface UseDomEditSessionParams {
|
||||
showToast: (message: string, tone?: "error" | "info") => void;
|
||||
refreshPreviewDocumentVersion: () => void;
|
||||
queueDomEditSave: (save: () => Promise<void>) => Promise<void>;
|
||||
commitStudioMotionManifestOptimistically: (
|
||||
updateManifest: (manifest: StudioMotionManifest) => StudioMotionManifest,
|
||||
options: { label: string; coalesceKey: string },
|
||||
) => void;
|
||||
applyCurrentStudioMotionToPreview: (iframe: HTMLIFrameElement | null) => void;
|
||||
readProjectFile: (path: string) => Promise<string>;
|
||||
writeProjectFile: (path: string, content: string) => Promise<void>;
|
||||
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
||||
@@ -55,9 +49,6 @@ export interface UseDomEditSessionParams {
|
||||
applyStudioManualEditsToPreviewRef: React.MutableRefObject<
|
||||
(iframe: HTMLIFrameElement) => Promise<void>
|
||||
>;
|
||||
applyStudioMotionToPreviewRef: React.MutableRefObject<
|
||||
(iframe: HTMLIFrameElement) => Promise<void>
|
||||
>;
|
||||
syncPreviewHistoryHotkey: (iframe: HTMLIFrameElement | null) => void;
|
||||
reloadPreview: () => void;
|
||||
setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
|
||||
@@ -81,8 +72,6 @@ export function useDomEditSession({
|
||||
showToast,
|
||||
refreshPreviewDocumentVersion,
|
||||
queueDomEditSave,
|
||||
commitStudioMotionManifestOptimistically,
|
||||
applyCurrentStudioMotionToPreview,
|
||||
readProjectFile: _readProjectFile,
|
||||
writeProjectFile,
|
||||
domEditSaveTimestampRef,
|
||||
@@ -95,7 +84,6 @@ export function useDomEditSession({
|
||||
refreshKey,
|
||||
rightPanelTab,
|
||||
applyStudioManualEditsToPreviewRef,
|
||||
applyStudioMotionToPreviewRef,
|
||||
syncPreviewHistoryHotkey,
|
||||
reloadPreview,
|
||||
setRefreshKey: _setRefreshKey,
|
||||
@@ -200,8 +188,6 @@ export function useDomEditSession({
|
||||
previewIframeRef,
|
||||
showToast,
|
||||
queueDomEditSave,
|
||||
commitStudioMotionManifestOptimistically,
|
||||
applyCurrentStudioMotionToPreview,
|
||||
writeProjectFile,
|
||||
domEditSaveTimestampRef,
|
||||
editHistory,
|
||||
@@ -249,19 +235,13 @@ export function useDomEditSession({
|
||||
};
|
||||
|
||||
syncPreviewHistoryHotkey(previewIframe);
|
||||
void (async () => {
|
||||
await applyStudioManualEditsToPreviewRef.current(previewIframe);
|
||||
await applyStudioMotionToPreviewRef.current(previewIframe);
|
||||
})();
|
||||
void applyStudioManualEditsToPreviewRef.current(previewIframe);
|
||||
syncSelectionFromDocument();
|
||||
refreshPreviewDocumentVersion();
|
||||
|
||||
const handleLoad = () => {
|
||||
syncPreviewHistoryHotkey(previewIframe);
|
||||
void (async () => {
|
||||
await applyStudioManualEditsToPreviewRef.current(previewIframe);
|
||||
await applyStudioMotionToPreviewRef.current(previewIframe);
|
||||
})();
|
||||
void applyStudioManualEditsToPreviewRef.current(previewIframe);
|
||||
syncSelectionFromDocument();
|
||||
refreshPreviewDocumentVersion();
|
||||
};
|
||||
@@ -280,7 +260,6 @@ export function useDomEditSession({
|
||||
refreshPreviewDocumentVersion,
|
||||
syncPreviewHistoryHotkey,
|
||||
applyStudioManualEditsToPreviewRef,
|
||||
applyStudioMotionToPreviewRef,
|
||||
]);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user