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:
Miguel Ángel
2026-05-15 21:45:57 +02:00
committed by GitHub
parent adeb92ecb7
commit 9b23ccf665
15 changed files with 1210 additions and 303 deletions
+2 -23
View File
@@ -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 {