From a1fe2b1a6f08e115dac167b1f5d355c14202bbb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Fri, 15 May 2026 10:48:23 -0700 Subject: [PATCH] refactor: remove legacy manual edits JSON manifest render script Remove the old `.hyperframes/studio-manual-edits.json` sidecar approach that injected a manifest-based runtime into rendered HTML to apply position/rotation/size edits. This was replaced by the seek-reapply script that reads positions directly from HTML data attributes and CSS custom properties (shipped in v0.6.7). Removed: `createStudioManualEditsRenderBodyScript`, `studioManualEditsRenderRuntime`, `STUDIO_MANUAL_EDITS_PATH`, and all references in CLI server, studio vite plugins, and thumbnail routes. Kept: `createStudioPositionSeekReapplyScript` (active render script), all motion panel code, all manual edits DOM code. --- packages/cli/src/server/studioServer.ts | 41 -- .../helpers/manualEditsRenderScript.test.ts | 387 +----------------- .../helpers/manualEditsRenderScript.ts | 372 ----------------- packages/core/src/studio-api/index.ts | 7 +- .../src/studio-api/routes/thumbnail.test.ts | 24 -- .../core/src/studio-api/routes/thumbnail.ts | 10 +- packages/studio/vite.browser.ts | 11 +- packages/studio/vite.studioMotion.test.ts | 20 +- packages/studio/vite.studioMotion.ts | 21 +- 9 files changed, 26 insertions(+), 867 deletions(-) diff --git a/packages/cli/src/server/studioServer.ts b/packages/cli/src/server/studioServer.ts index b246d3525..e5779643f 100644 --- a/packages/cli/src/server/studioServer.ts +++ b/packages/cli/src/server/studioServer.ts @@ -13,7 +13,6 @@ import { createProjectWatcher, type ProjectWatcher } from "./fileWatcher.js"; import { loadRuntimeSource } from "./runtimeSource.js"; import { VERSION as version } from "../version.js"; import { - createStudioManualEditsRenderBodyScript, createStudioApi, createProjectSignature, getMimeType, @@ -24,8 +23,6 @@ import { import { getElementScreenshotClip } from "@hyperframes/core/studio-api/screenshot-clip"; import type { ScreenshotClip } from "@hyperframes/core/studio-api/screenshot-clip"; -const STUDIO_MANUAL_EDITS_PATH = ".hyperframes/studio-manual-edits.json"; - // ── Path resolution ───────────────────────────────────────────────────────── function resolveDistDir(): string { @@ -81,38 +78,6 @@ function resolveRuntimePath(): string { return builtPath; } -function readStudioManualEditManifestContent(projectDir: string): string { - const manifestPath = join(projectDir, STUDIO_MANUAL_EDITS_PATH); - if (!existsSync(manifestPath)) return ""; - try { - return readFileSync(manifestPath, "utf-8"); - } catch { - return ""; - } -} - -async function applyStudioManualEditsToThumbnailPage( - page: import("puppeteer-core").Page, - manifestContent: string, - activeCompositionPath: string, -): Promise { - const script = createStudioManualEditsRenderBodyScript(manifestContent, { - activeCompositionPath, - }); - if (!script) return; - await page.addScriptTag({ content: script }); -} - -async function reapplyStudioManualEditsToThumbnailPage( - page: import("puppeteer-core").Page, -): Promise { - await page.evaluate(() => { - const apply = (window as Window & { __hfStudioManualEditsApply?: () => number }) - .__hfStudioManualEditsApply; - if (typeof apply === "function") apply(); - }); -} - // ── Shared thumbnail browser (singleton per process) ──────────────────────── // One browser instance is reused across all composition thumbnail requests. // Spawning a new Puppeteer process per request adds 2-5s overhead and causes @@ -253,8 +218,6 @@ export function createStudioServer(options: StudioServerOptions): StudioServer { // Continue without — acquireBrowser will try its own resolution } - const manifestContent = readStudioManualEditManifestContent(opts.project.dir); - const manualEditsRenderScript = createStudioManualEditsRenderBodyScript(manifestContent); const job = createRenderJob({ // opts.fps is already an Fps rational — see vite-config-studio // adapter for the same convention. @@ -262,7 +225,6 @@ export function createStudioServer(options: StudioServerOptions): StudioServer { quality: opts.quality as "draft" | "standard" | "high", format: opts.format, outputResolution: opts.outputResolution, - ...(manualEditsRenderScript ? { renderBodyScripts: [manualEditsRenderScript] } : {}), }); const startTime = Date.now(); const onProgress = (j: { progress: number; currentStage?: string }) => { @@ -319,11 +281,8 @@ export function createStudioServer(options: StudioServerOptions): StudioServer { win.__timeline.seek(t); } }, opts.seekTime); - const manifestContent = readStudioManualEditManifestContent(opts.project.dir); - await applyStudioManualEditsToThumbnailPage(page, manifestContent, opts.compPath); // Let the seek render settle. await new Promise((r) => setTimeout(r, 200)); - await reapplyStudioManualEditsToThumbnailPage(page); let clip: ScreenshotClip | undefined; if (opts.selector) { clip = await page.evaluate(getElementScreenshotClip, opts.selector, opts.selectorIndex); diff --git a/packages/core/src/studio-api/helpers/manualEditsRenderScript.test.ts b/packages/core/src/studio-api/helpers/manualEditsRenderScript.test.ts index 3e41a15f2..2039da2d9 100644 --- a/packages/core/src/studio-api/helpers/manualEditsRenderScript.test.ts +++ b/packages/core/src/studio-api/helpers/manualEditsRenderScript.test.ts @@ -1,382 +1,17 @@ import { describe, expect, it } from "vitest"; -import { Window } from "happy-dom"; -import { createStudioManualEditsRenderBodyScript } from "./manualEditsRenderScript"; +import { createStudioPositionSeekReapplyScript } from "./manualEditsRenderScript"; -function runScript( - window: Window, - script: string, - getComputedStyle: typeof window.getComputedStyle = window.getComputedStyle.bind(window), - timers: { - setInterval?: typeof globalThis.setInterval; - clearInterval?: typeof globalThis.clearInterval; - } = {}, -): void { - const execute = new Function( - "window", - "document", - "HTMLElement", - "getComputedStyle", - "setInterval", - "clearInterval", - script, - ); - execute( - window, - window.document, - window.HTMLElement, - getComputedStyle, - timers.setInterval ?? - (((callback: TimerHandler) => { - void callback; - return 0 as never; - }) as typeof globalThis.setInterval), - timers.clearInterval ?? globalThis.clearInterval, - ); -} - -describe("createStudioManualEditsRenderBodyScript", () => { - it("returns null for an empty manifest", () => { - expect(createStudioManualEditsRenderBodyScript("")).toBeNull(); - }); - - it("applies manual edits and reapplies them after render seeks", () => { - const window = new Window(); - window.document.body.innerHTML = '
'; - const card = window.document.getElementById("card"); - if (!(card instanceof window.HTMLElement)) { - throw new Error("card fixture missing"); - } - - let seekCalls = 0; - ( - window as unknown as { - __hf: { seek: (time: number) => void }; - } - ).__hf = { - seek: () => { - seekCalls += 1; - card.style.removeProperty("translate"); - }, - }; - - const script = createStudioManualEditsRenderBodyScript( - JSON.stringify({ - version: 1, - edits: [ - { - kind: "path-offset", - target: { sourceFile: "index.html", id: "card" }, - x: 12, - y: 24, - }, - { - kind: "box-size", - target: { sourceFile: "index.html", id: "card" }, - width: 120, - height: 64, - }, - { - kind: "rotation", - target: { sourceFile: "index.html", id: "card" }, - angle: 15, - }, - ], - }), - ); - if (!script) throw new Error("script fixture missing"); - - const computedStyle = (element: Element) => - ({ - display: element === card ? "block" : "block", - flexDirection: "row", - }) as CSSStyleDeclaration; - - const intervalCallbacks: Array<() => void> = []; - runScript(window, script, computedStyle, { - setInterval: ((callback: TimerHandler) => { - if (typeof callback === "function") intervalCallbacks.push(callback as () => void); - return 0 as never; - }) as typeof globalThis.setInterval, - }); - - expect(card.style.getPropertyValue("translate")).toContain("--hf-studio-offset-x"); - expect(card.style.getPropertyValue("width")).toBe("120px"); - expect(card.style.getPropertyValue("height")).toBe("64px"); - expect(card.style.getPropertyValue("rotate")).toContain("--hf-studio-rotation"); - expect(card.style.getPropertyValue("transform-origin")).toBe("center center"); - - ( - window as unknown as { - __hf: { seek: (time: number) => void }; - } - ).__hf.seek(1); - - expect(seekCalls).toBe(1); - expect(card.style.getPropertyValue("translate")).toContain("--hf-studio-offset-x"); - - ( - window as unknown as { - __hf: { seek: (time: number) => void }; - } - ).__hf.seek = () => { - card.style.removeProperty("rotate"); - }; - intervalCallbacks.forEach((callback) => callback()); - ( - window as unknown as { - __hf: { seek: (time: number) => void }; - } - ).__hf.seek(2); - expect(card.style.getPropertyValue("rotate")).toContain("--hf-studio-rotation"); - - ( - window as unknown as { - __player: { renderSeek: (time: number) => void }; - } - ).__player = { - renderSeek: () => { - card.style.removeProperty("rotate"); - }, - }; - intervalCallbacks.forEach((callback) => callback()); - ( - window as unknown as { - __player: { renderSeek: (time: number) => void }; - } - ).__player.renderSeek(3); - expect(card.style.getPropertyValue("rotate")).toContain("--hf-studio-rotation"); - }); - - it("applies render edits to the matching source file target", () => { - const window = new Window(); - window.document.body.innerHTML = ` -
-
-
-
-
-
- `; - const cards = Array.from(window.document.getElementsByTagName("*")).filter( - (element): element is HTMLElement => - element instanceof window.HTMLElement && element.id === "card", - ); - const rootCard = cards[0]; - const nestedCard = cards[1]; - if (!rootCard || !nestedCard) { - throw new Error("source-scoped render fixture missing"); - } - - const script = createStudioManualEditsRenderBodyScript( - JSON.stringify({ - version: 1, - edits: [ - { - kind: "rotation", - target: { sourceFile: "scenes/nested.html", id: "card" }, - angle: 21, - }, - ], - }), - ); - if (!script) throw new Error("script fixture missing"); - - runScript(window, script); - - expect(rootCard.style.getPropertyValue("rotate")).toBe(""); - expect(nestedCard.style.getPropertyValue("rotate")).toContain("--hf-studio-rotation"); - }); - - it("applies render edits inside composition-file hosts without composition ids", () => { - const window = new Window(); - window.document.body.innerHTML = ` -
-
-
-
-
-
- `; - const cards = Array.from(window.document.getElementsByTagName("*")).filter( - (element): element is HTMLElement => - element instanceof window.HTMLElement && element.id === "card", - ); - const rootCard = cards[0]; - const nestedCard = cards[1]; - if (!rootCard || !nestedCard) { - throw new Error("anonymous composition render fixture missing"); - } - - const script = createStudioManualEditsRenderBodyScript( - JSON.stringify({ - version: 1, - edits: [ - { - kind: "path-offset", - target: { sourceFile: "scenes/anonymous.html", id: "card" }, - x: 12, - y: 24, - }, - ], - }), - ); - if (!script) throw new Error("script fixture missing"); - - runScript(window, script); - - expect(rootCard.style.getPropertyValue("translate")).toBe(""); - expect(nestedCard.style.getPropertyValue("translate")).toContain("--hf-studio-offset-x"); - }); - - it("uses the active composition path as the unscoped document fallback", () => { - const window = new Window(); - window.document.body.innerHTML = `
`; - const card = window.document.getElementById("card"); - if (!(card instanceof window.HTMLElement)) { - throw new Error("card fixture missing"); - } - - const script = createStudioManualEditsRenderBodyScript( - JSON.stringify({ - version: 1, - edits: [ - { - kind: "path-offset", - target: { sourceFile: "compositions/scene-2.html", id: "card" }, - x: 12, - y: 24, - }, - ], - }), - { activeCompositionPath: "compositions/scene-2.html" }, - ); - if (!script) throw new Error("script fixture missing"); - - runScript(window, script); - - expect(card.style.getPropertyValue("translate")).toContain("--hf-studio-offset-x"); +describe("createStudioPositionSeekReapplyScript", () => { + it("returns a non-empty IIFE string", () => { + const script = createStudioPositionSeekReapplyScript(); + expect(typeof script).toBe("string"); + expect(script.length).toBeGreaterThan(0); + expect(script).toContain("studioPositionSeekReapplyRuntime"); }); - it("preserves computed transform longhands as render edit bases", () => { - const window = new Window(); - window.document.body.innerHTML = `
`; - const card = window.document.getElementById("card"); - if (!(card instanceof window.HTMLElement)) { - throw new Error("card fixture missing"); - } - - const script = createStudioManualEditsRenderBodyScript( - JSON.stringify({ - version: 1, - edits: [ - { - kind: "path-offset", - target: { sourceFile: "index.html", id: "card" }, - x: 12, - y: 24, - }, - { - kind: "rotation", - target: { sourceFile: "index.html", id: "card" }, - angle: 15, - }, - ], - }), - ); - if (!script) throw new Error("script fixture missing"); - - const computedStyle = (element: Element) => - ({ - getPropertyValue: (property: string) => { - if (element !== card) return ""; - if (property === "translate") return "10px 20px"; - if (property === "rotate") return "8deg"; - return ""; - }, - }) as CSSStyleDeclaration; - - runScript(window, script, computedStyle); - - expect(card.style.getPropertyValue("translate")).toContain("calc(10px +"); - expect(card.style.getPropertyValue("translate")).toContain("calc(20px +"); - expect(card.style.getPropertyValue("rotate")).toContain("8deg"); - expect(card.style.getPropertyValue("rotate")).toContain("--hf-studio-rotation"); - expect(card.style.getPropertyValue("transform-origin")).toBe("center center"); - }); - - it("does not compound stale studio variables during render reapply", () => { - const window = new Window(); - window.document.body.innerHTML = ` -
- `; - const card = window.document.getElementById("card"); - if (!(card instanceof window.HTMLElement)) { - throw new Error("card fixture missing"); - } - - const script = createStudioManualEditsRenderBodyScript( - JSON.stringify({ - version: 1, - edits: [ - { - kind: "path-offset", - target: { sourceFile: "index.html", id: "card" }, - x: 12, - y: 24, - }, - { - kind: "rotation", - target: { sourceFile: "index.html", id: "card" }, - angle: 15, - }, - ], - }), - ); - if (!script) throw new Error("script fixture missing"); - - runScript(window, script); - - expect(card.style.getPropertyValue("translate")).toBe( - "var(--hf-studio-offset-x, 0px) var(--hf-studio-offset-y, 0px)", - ); - expect(card.style.getPropertyValue("rotate")).toBe("var(--hf-studio-rotation, 0deg)"); - }); - - it("exposes a render reapply hook for thumbnails after layout settles", () => { - const window = new Window(); - window.document.body.innerHTML = `
`; - const card = window.document.getElementById("card"); - if (!(card instanceof window.HTMLElement)) { - throw new Error("card fixture missing"); - } - - const script = createStudioManualEditsRenderBodyScript( - JSON.stringify({ - version: 1, - edits: [ - { - kind: "path-offset", - target: { sourceFile: "index.html", id: "card" }, - x: 12, - y: 24, - }, - ], - }), - ); - if (!script) throw new Error("script fixture missing"); - - runScript(window, script); - card.style.removeProperty("translate"); - - ( - window as unknown as { - __hfStudioManualEditsApply?: () => number; - } - ).__hfStudioManualEditsApply?.(); - - expect(card.style.getPropertyValue("translate")).toContain("--hf-studio-offset-x"); + it("contains the expected data attribute selectors", () => { + const script = createStudioPositionSeekReapplyScript(); + expect(script).toContain("data-hf-studio-path-offset"); + expect(script).toContain("data-hf-studio-rotation"); }); }); diff --git a/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts b/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts index 0ca7b4b72..f363b0d69 100644 --- a/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts +++ b/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts @@ -1,17 +1,3 @@ -export interface StudioManualEditsRenderScriptOptions { - activeCompositionPath?: string | null; -} - -export const STUDIO_MANUAL_EDITS_PATH = ".hyperframes/studio-manual-edits.json"; - -export function createStudioManualEditsRenderBodyScript( - manifestContent: string, - options: StudioManualEditsRenderScriptOptions = {}, -): string | null { - if (!manifestContent.trim()) return null; - return `(${studioManualEditsRenderRuntime.toString()})(${JSON.stringify(manifestContent)}, ${JSON.stringify(options.activeCompositionPath ?? null)});`; -} - /** * Returns a self-contained IIFE string that re-applies studio position edits * (translate, rotate) after every GSAP seek by querying data attributes baked @@ -222,361 +208,3 @@ function studioPositionSeekReapplyRuntime(): void { if (remaining <= 0) clearInterval(interval); }, 50); } - -function studioManualEditsRenderRuntime( - manifestContent: string, - activeCompositionPath: string | null, -): void { - const OFFSET_X_PROP = "--hf-studio-offset-x"; - const OFFSET_Y_PROP = "--hf-studio-offset-y"; - const WIDTH_PROP = "--hf-studio-width"; - const HEIGHT_PROP = "--hf-studio-height"; - const ROTATION_PROP = "--hf-studio-rotation"; - const PATH_OFFSET_ATTR = "data-hf-studio-path-offset"; - const BOX_SIZE_ATTR = "data-hf-studio-box-size"; - const ROTATION_ATTR = "data-hf-studio-rotation"; - const ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate"; - const ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate"; - const WRAPPED_SEEK_PROP = "__hfStudioManualEditsWrapped"; - const ROTATION_TRANSFORM_ORIGIN = "center center"; - - const finiteNumber = (value: unknown): number | null => - typeof value === "number" && Number.isFinite(value) ? value : null; - - const objectRecord = (value: unknown): Record | null => - value && typeof value === "object" ? (value as Record) : null; - - const runtimeWindow = window as Window & { - __hf?: { seek?: (time: number) => unknown }; - __hfStudioManualEditsApply?: () => number; - __player?: { renderSeek?: (time: number) => unknown }; - }; - - const parsedManifest = (() => { - try { - return objectRecord(JSON.parse(manifestContent)); - } catch { - return null; - } - })(); - const manifestEdits = Array.isArray(parsedManifest?.edits) ? parsedManifest.edits : []; - if (manifestEdits.length === 0) return; - - const sourceFileForElement = (element: HTMLElement): string => { - let current: HTMLElement | null = element; - while (current) { - const sourceFile = - current.getAttribute("data-composition-file") ?? - current.getAttribute("data-composition-src"); - if (sourceFile) return sourceFile; - current = current.parentElement; - } - return activeCompositionPath ?? "index.html"; - }; - - const elementMatchesSourceFile = (element: HTMLElement, sourceFile: string): boolean => - sourceFileForElement(element) === sourceFile; - - const styleUsesStudioOffset = (value: string): boolean => - value.includes(OFFSET_X_PROP) || value.includes(OFFSET_Y_PROP); - - const styleUsesStudioRotation = (value: string): boolean => value.includes(ROTATION_PROP); - - const splitTopLevelWhitespace = (value: string): string[] => { - const parts: string[] = []; - let depth = 0; - let current = ""; - for (const char of value.trim()) { - if (char === "(") depth += 1; - if (char === ")") depth = Math.max(0, depth - 1); - if (/\s/.test(char) && depth === 0) { - if (current) parts.push(current); - current = ""; - } else { - current += char; - } - } - if (current) parts.push(current); - return parts; - }; - - const composeTranslate = (element: HTMLElement, x: string, y: string): string => { - const original = element.getAttribute(ORIGINAL_TRANSLATE_ATTR)?.trim(); - if (!original || original === "none") return `${x} ${y}`; - - const parts = splitTopLevelWhitespace(original); - if (parts.length === 1) return `calc(${parts[0]} + ${x}) ${y}`; - if (parts.length === 2) return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y})`; - if (parts.length === 3) { - return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y}) ${parts[2]}`; - } - return `${x} ${y}`; - }; - - const readStyleOrComputed = (element: HTMLElement, property: string): string => { - try { - return ( - element.style.getPropertyValue(property) || - getComputedStyle(element).getPropertyValue(property) - ); - } catch { - return element.style.getPropertyValue(property); - } - }; - - const readTransformLonghandBase = ( - element: HTMLElement, - property: "translate" | "rotate", - ): string => { - const value = readStyleOrComputed(element, property).trim(); - return value === "none" ? "" : value; - }; - - const preparePathOffsetBase = (element: HTMLElement): void => { - const currentTranslate = readTransformLonghandBase(element, "translate"); - const hasMarker = element.hasAttribute(PATH_OFFSET_ATTR); - const wasResetByAnimation = !styleUsesStudioOffset(currentTranslate); - if (!hasMarker) { - element.setAttribute(ORIGINAL_TRANSLATE_ATTR, wasResetByAnimation ? currentTranslate : ""); - } else if (wasResetByAnimation) { - element.setAttribute(ORIGINAL_TRANSLATE_ATTR, currentTranslate); - } - }; - - const prepareRotationBase = (element: HTMLElement): void => { - const currentRotate = readTransformLonghandBase(element, "rotate"); - const hasMarker = element.hasAttribute(ROTATION_ATTR); - const wasResetByAnimation = !styleUsesStudioRotation(currentRotate); - if (!hasMarker) { - element.setAttribute(ORIGINAL_ROTATE_ATTR, wasResetByAnimation ? currentRotate : ""); - } else if (wasResetByAnimation) { - element.setAttribute(ORIGINAL_ROTATE_ATTR, currentRotate); - } - }; - - const querySelectorCandidates = (selector: string): HTMLElement[] => { - const isCandidate = (element: Element): element is HTMLElement => - element instanceof HTMLElement; - - const className = selector.match(/^\.([A-Za-z0-9_-]+)$/)?.[1]; - if (className) { - return Array.from(document.getElementsByTagName("*")).filter( - (element): element is HTMLElement => - isCandidate(element) && element.classList.contains(className), - ); - } - - if (/^[A-Za-z][A-Za-z0-9-]*$/.test(selector)) { - return Array.from(document.getElementsByTagName(selector)).filter(isCandidate); - } - - return Array.from(document.querySelectorAll(selector)).filter(isCandidate); - }; - - const resolveTarget = (edit: Record): HTMLElement | null => { - const targetRecord = objectRecord(edit.target); - if (!targetRecord) return null; - - const sourceFile = typeof targetRecord.sourceFile === "string" ? targetRecord.sourceFile : ""; - if (!sourceFile) return null; - - const id = typeof targetRecord.id === "string" ? targetRecord.id : ""; - if (id) { - const byId = document.getElementById(id); - if (byId instanceof HTMLElement && elementMatchesSourceFile(byId, sourceFile)) return byId; - - const matchesById = [ - document.documentElement, - ...Array.from(document.getElementsByTagName("*")), - ].filter( - (element): element is HTMLElement => - element instanceof HTMLElement && - element.id === id && - elementMatchesSourceFile(element, sourceFile), - ); - if (matchesById[0]) return matchesById[0]; - } - - const selector = typeof targetRecord.selector === "string" ? targetRecord.selector : ""; - if (!selector) return null; - - try { - const matches = querySelectorCandidates(selector).filter((element) => - elementMatchesSourceFile(element, sourceFile), - ); - const selectorIndex = finiteNumber(targetRecord.selectorIndex) ?? 0; - return matches[Math.max(0, Math.floor(selectorIndex))] ?? null; - } catch { - return null; - } - }; - - const roundRotationAngle = (angle: number): number => Math.round(angle * 10) / 10; - - const isSimpleRotateAngle = (value: string): boolean => - /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim()); - - const composeRotation = (element: HTMLElement, rotationValue: string): string => { - const original = element.getAttribute(ORIGINAL_ROTATE_ATTR)?.trim(); - if (!original || original === "none" || !isSimpleRotateAngle(original)) { - return rotationValue; - } - return `calc(${original} + ${rotationValue})`; - }; - - const applyPathOffset = (element: HTMLElement, edit: Record): void => { - const x = finiteNumber(edit.x); - const y = finiteNumber(edit.y); - if (x == null || y == null) return; - preparePathOffsetBase(element); - element.setAttribute(PATH_OFFSET_ATTR, "true"); - element.style.setProperty(OFFSET_X_PROP, `${Math.round(x)}px`); - element.style.setProperty(OFFSET_Y_PROP, `${Math.round(y)}px`); - element.style.setProperty( - "translate", - composeTranslate(element, `var(${OFFSET_X_PROP}, 0px)`, `var(${OFFSET_Y_PROP}, 0px)`), - ); - }; - - const readParentFlexBasisPixels = ( - element: HTMLElement, - size: { width: number; height: number }, - ): number | null => { - const parent = element.parentElement; - if (!parent) return null; - const styles = getComputedStyle(parent); - if (styles.display !== "flex" && styles.display !== "inline-flex") return null; - return Math.round( - Math.max(1, styles.flexDirection.startsWith("column") ? size.height : size.width), - ); - }; - - const applyBoxSize = (element: HTMLElement, edit: Record): void => { - const width = finiteNumber(edit.width); - const height = finiteNumber(edit.height); - if (width == null || height == null || width <= 0 || height <= 0) return; - - const rounded = { - width: Math.round(Math.max(1, width)), - height: Math.round(Math.max(1, height)), - }; - element.setAttribute(BOX_SIZE_ATTR, "true"); - element.style.setProperty(WIDTH_PROP, `${rounded.width}px`); - element.style.setProperty(HEIGHT_PROP, `${rounded.height}px`); - element.style.setProperty("box-sizing", "border-box"); - element.style.setProperty("width", `${rounded.width}px`); - element.style.setProperty("height", `${rounded.height}px`); - element.style.setProperty("min-width", "0px"); - element.style.setProperty("min-height", "0px"); - element.style.setProperty("max-width", "none"); - element.style.setProperty("max-height", "none"); - - const flexBasis = readParentFlexBasisPixels(element, rounded); - if (flexBasis != null) { - element.style.setProperty("flex-basis", `${flexBasis}px`); - element.style.setProperty("flex-grow", "0"); - element.style.setProperty("flex-shrink", "0"); - } - if (getComputedStyle(element).display === "inline") { - element.style.setProperty("display", "inline-block"); - } - }; - - const applyRotation = (element: HTMLElement, edit: Record): void => { - const angle = finiteNumber(edit.angle); - if (angle == null) return; - prepareRotationBase(element); - element.setAttribute(ROTATION_ATTR, "true"); - element.style.setProperty(ROTATION_PROP, `${roundRotationAngle(angle)}deg`); - element.style.setProperty("transform-origin", ROTATION_TRANSFORM_ORIGIN); - element.style.setProperty("rotate", composeRotation(element, `var(${ROTATION_PROP}, 0deg)`)); - }; - - const applyManifest = (): number => { - let applied = 0; - for (const edit of manifestEdits) { - const editRecord = objectRecord(edit); - if (!editRecord) continue; - const element = resolveTarget(editRecord); - if (!element) continue; - if (editRecord.kind === "path-offset") applyPathOffset(element, editRecord); - if (editRecord.kind === "box-size") applyBoxSize(element, editRecord); - if (editRecord.kind === "rotation") applyRotation(element, editRecord); - applied += 1; - } - return applied; - }; - runtimeWindow.__hfStudioManualEditsApply = applyManifest; - - const markWrapped = (fn: (time: number) => unknown): void => { - try { - Object.defineProperty(fn, WRAPPED_SEEK_PROP, { - configurable: false, - enumerable: false, - value: true, - }); - } catch { - try { - (fn as unknown as Record)[WRAPPED_SEEK_PROP] = true; - } catch { - // Ignore non-extensible functions. - } - } - }; - - const isWrapped = (fn: (time: number) => unknown): boolean => - Boolean((fn as unknown as Record)[WRAPPED_SEEK_PROP]); - - const wrapFunction = ( - get: () => ((time: number) => unknown) | undefined, - set: (fn: (time: number) => unknown) => void, - ): boolean => { - const fn = get(); - if (!fn) return false; - const seek = fn as (time: number) => unknown; - if (isWrapped(seek)) { - applyManifest(); - return true; - } - - const wrappedSeek = function (this: unknown, time: number): unknown { - const result = seek.call(this, time); - applyManifest(); - return result; - }; - markWrapped(wrappedSeek); - set(wrappedSeek); - applyManifest(); - return true; - }; - - const wrapSeekFunctions = (): boolean => { - const wrappedHfSeek = wrapFunction( - () => runtimeWindow.__hf?.seek, - (fn) => { - if (runtimeWindow.__hf) runtimeWindow.__hf.seek = fn; - }, - ); - const wrappedPlayerRenderSeek = wrapFunction( - () => runtimeWindow.__player?.renderSeek, - (fn) => { - if (runtimeWindow.__player) runtimeWindow.__player.renderSeek = fn; - }, - ); - return wrappedHfSeek || wrappedPlayerRenderSeek; - }; - - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", () => applyManifest(), { once: true }); - } else { - applyManifest(); - } - - wrapSeekFunctions(); - let remainingSeekWrapAttempts = 120; - const seekWrapInterval = setInterval(() => { - wrapSeekFunctions(); - remainingSeekWrapAttempts -= 1; - if (remainingSeekWrapAttempts <= 0) clearInterval(seekWrapInterval); - }, 50); -} diff --git a/packages/core/src/studio-api/index.ts b/packages/core/src/studio-api/index.ts index bbc77f69d..4b19dcbce 100644 --- a/packages/core/src/studio-api/index.ts +++ b/packages/core/src/studio-api/index.ts @@ -5,12 +5,7 @@ export { isSafePath, walkDir } from "./helpers/safePath.js"; export { getMimeType, MIME_TYPES } from "./helpers/mime.js"; export { buildSubCompositionHtml } from "./helpers/subComposition.js"; export { getElementScreenshotClip, type ScreenshotClip } from "./helpers/screenshotClip.js"; -export { - STUDIO_MANUAL_EDITS_PATH, - createStudioManualEditsRenderBodyScript, - createStudioPositionSeekReapplyScript, - type StudioManualEditsRenderScriptOptions, -} from "./helpers/manualEditsRenderScript.js"; +export { createStudioPositionSeekReapplyScript } from "./helpers/manualEditsRenderScript.js"; export { STUDIO_MOTION_PATH, createStudioMotionRenderBodyScript, diff --git a/packages/core/src/studio-api/routes/thumbnail.test.ts b/packages/core/src/studio-api/routes/thumbnail.test.ts index 1516b3950..c3cd959b3 100644 --- a/packages/core/src/studio-api/routes/thumbnail.test.ts +++ b/packages/core/src/studio-api/routes/thumbnail.test.ts @@ -151,30 +151,6 @@ describe("registerThumbnailRoutes", () => { ); }); - it("keeps changed studio manual edits separated in the disk cache", async () => { - const adapter = createAdapter(); - const project = await adapter.resolveProject("demo"); - if (!project) throw new Error("missing project"); - const app = new Hono(); - registerThumbnailRoutes(app, adapter); - - const indexPath = join(project.dir, "index.html"); - writeFileSync(indexPath, `
`); - const manualEditsDir = join(project.dir, ".hyperframes"); - mkdirSync(manualEditsDir, { recursive: true }); - const manualEditsPath = join(manualEditsDir, "studio-manual-edits.json"); - writeFileSync(manualEditsPath, `{"version":1,"edits":[]}`); - - await app.request("http://localhost/projects/demo/thumbnail/index.html?t=2&v=test"); - writeFileSync( - manualEditsPath, - `{"version":1,"edits":[{"kind":"rotation","target":{"sourceFile":"index.html","id":"card"},"angle":30}]}`, - ); - await app.request("http://localhost/projects/demo/thumbnail/index.html?t=2&v=test"); - - expect(adapter.generateThumbnail).toHaveBeenCalledTimes(2); - }); - it("keeps changed studio motion separated in the disk cache", async () => { const adapter = createAdapter(); const project = await adapter.resolveProject("demo"); diff --git a/packages/core/src/studio-api/routes/thumbnail.ts b/packages/core/src/studio-api/routes/thumbnail.ts index 87290fb95..5b9a79a8d 100644 --- a/packages/core/src/studio-api/routes/thumbnail.ts +++ b/packages/core/src/studio-api/routes/thumbnail.ts @@ -3,7 +3,6 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "no import { join } from "node:path"; import { createHash } from "node:crypto"; import type { StudioApiAdapter } from "../types.js"; -import { STUDIO_MANUAL_EDITS_PATH } from "../helpers/manualEditsRenderScript.js"; import { STUDIO_MOTION_PATH } from "../helpers/studioMotionRenderScript.js"; const THUMBNAIL_CACHE_VERSION = "v4"; @@ -50,13 +49,6 @@ export function registerThumbnailRoutes(api: Hono, adapter: StudioApiAdapter): v if (hMatch?.[1]) compH = parseInt(hMatch[1]); } } - const manualEditsFile = join(project.dir, STUDIO_MANUAL_EDITS_PATH); - let manualEditsKey = ""; - if (existsSync(manualEditsFile)) { - const manualEditsContent = readFileSync(manualEditsFile, "utf-8"); - manualEditsKey = `_${createHash("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`; - sourceMtime = Math.max(sourceMtime, Math.round(statSync(manualEditsFile).mtimeMs)); - } const motionFile = join(project.dir, STUDIO_MOTION_PATH); let motionKey = ""; if (existsSync(motionFile)) { @@ -78,7 +70,7 @@ export function registerThumbnailRoutes(api: Hono, adapter: StudioApiAdapter): v const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : ""; - const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${manualEditsKey}${motionKey}_${format}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`; + const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${motionKey}_${format}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`; const cachePath = join(cacheDir, cacheKey); if (existsSync(cachePath)) { return new Response(new Uint8Array(readFileSync(cachePath)), { diff --git a/packages/studio/vite.browser.ts b/packages/studio/vite.browser.ts index 027c2813e..73ecd9f3b 100644 --- a/packages/studio/vite.browser.ts +++ b/packages/studio/vite.browser.ts @@ -4,7 +4,6 @@ import { existsSync } from "node:fs"; import { createHash } from "node:crypto"; import { createStudioDevRenderBodyScripts, - readStudioDevManualEditManifestContent, readStudioDevMotionManifestContent, } from "./vite.studioMotion"; import { seekThumbnailPreview } from "./vite.thumbnail"; @@ -72,12 +71,8 @@ async function reapplyStudioRenderBodyScriptsToThumbnailPage( ): Promise { await page.evaluate(() => { const runtimeWindow = window as Window & { - __hfStudioManualEditsApply?: () => number; __hfStudioMotionApply?: () => number; }; - if (typeof runtimeWindow.__hfStudioManualEditsApply === "function") { - runtimeWindow.__hfStudioManualEditsApply(); - } if (typeof runtimeWindow.__hfStudioMotionApply === "function") { runtimeWindow.__hfStudioMotionApply(); } @@ -100,15 +95,11 @@ export async function generateThumbnail(opts: GenerateThumbnailOptions): Promise const selectorKey = opts.selector ? `_${opts.selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${opts.selectorIndex ?? 0}` : ""; - const manualManifestContent = readStudioDevManualEditManifestContent(opts.project.dir); - const manualManifestKey = manualManifestContent.trim() - ? `_${createHash("sha1").update(manualManifestContent).digest("hex").slice(0, 16)}` - : ""; const motionManifestContent = readStudioDevMotionManifestContent(opts.project.dir); const motionManifestKey = motionManifestContent.trim() ? `_${createHash("sha1").update(motionManifestContent).digest("hex").slice(0, 16)}` : ""; - const cacheKey = `${THUMBNAIL_CACHE_VERSION}${manualManifestKey}${motionManifestKey}_${opts.compPath.replace(/\//g, "_")}_${opts.seekTime.toFixed(2)}${selectorKey}.${opts.format === "png" ? "png" : "jpg"}`; + const cacheKey = `${THUMBNAIL_CACHE_VERSION}${motionManifestKey}_${opts.compPath.replace(/\//g, "_")}_${opts.seekTime.toFixed(2)}${selectorKey}.${opts.format === "png" ? "png" : "jpg"}`; let bufferPromise = _thumbnailInflight.get(cacheKey); if (!bufferPromise) { diff --git a/packages/studio/vite.studioMotion.test.ts b/packages/studio/vite.studioMotion.test.ts index a39dc3ee2..03aa9d743 100644 --- a/packages/studio/vite.studioMotion.test.ts +++ b/packages/studio/vite.studioMotion.test.ts @@ -19,15 +19,8 @@ describe("createStudioDevRenderBodyScripts", () => { return projectDir; } - it("injects both manual edit and Studio GSAP motion render scripts in dev", () => { + it("injects Studio GSAP motion render script in dev", () => { const dir = createProject(); - writeFileSync( - join(dir, ".hyperframes/studio-manual-edits.json"), - JSON.stringify({ - version: 1, - edits: [{ kind: "text", target: { sourceFile: "index.html" } }], - }), - ); writeFileSync( join(dir, ".hyperframes/studio-motion.json"), JSON.stringify({ @@ -50,9 +43,14 @@ describe("createStudioDevRenderBodyScripts", () => { activeCompositionPath: "compositions/scene.html", }); - expect(scripts).toHaveLength(2); - expect(scripts[0]).toContain("__hfStudioManualEditsApply"); - expect(scripts[1]).toContain("__hfStudioMotionApply"); + expect(scripts).toHaveLength(1); + expect(scripts[0]).toContain("__hfStudioMotionApply"); expect(scripts.join("\n")).toContain("compositions/scene.html"); }); + + it("returns empty array when no motion manifest exists", () => { + const dir = createProject(); + const scripts = createStudioDevRenderBodyScripts(dir); + expect(scripts).toHaveLength(0); + }); }); diff --git a/packages/studio/vite.studioMotion.ts b/packages/studio/vite.studioMotion.ts index acf706648..c501be716 100644 --- a/packages/studio/vite.studioMotion.ts +++ b/packages/studio/vite.studioMotion.ts @@ -1,16 +1,11 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; -import { - createStudioManualEditsRenderBodyScript, - type StudioManualEditsRenderScriptOptions, -} from "../core/src/studio-api/helpers/manualEditsRenderScript"; import { createStudioMotionRenderBodyScript, STUDIO_MOTION_PATH, + type StudioMotionRenderScriptOptions, } from "../core/src/studio-api/helpers/studioMotionRenderScript"; -const STUDIO_MANUAL_EDITS_PATH = ".hyperframes/studio-manual-edits.json"; - function readManifestContent(projectDir: string, manifestPath: string): string { const resolvedPath = join(projectDir, manifestPath); if (!existsSync(resolvedPath)) return ""; @@ -21,27 +16,17 @@ function readManifestContent(projectDir: string, manifestPath: string): string { } } -export function readStudioDevManualEditManifestContent(projectDir: string): string { - return readManifestContent(projectDir, STUDIO_MANUAL_EDITS_PATH); -} - export function readStudioDevMotionManifestContent(projectDir: string): string { return readManifestContent(projectDir, STUDIO_MOTION_PATH); } export function createStudioDevRenderBodyScripts( projectDir: string, - options: StudioManualEditsRenderScriptOptions = {}, + options: StudioMotionRenderScriptOptions = {}, ): string[] { - const manualEditsRenderScript = createStudioManualEditsRenderBodyScript( - readStudioDevManualEditManifestContent(projectDir), - options, - ); const motionRenderScript = createStudioMotionRenderBodyScript( readStudioDevMotionManifestContent(projectDir), options, ); - return [manualEditsRenderScript, motionRenderScript].filter( - (script): script is string => typeof script === "string", - ); + return [motionRenderScript].filter((script): script is string => typeof script === "string"); }