mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
feat(studio): GSAP tween editing in Design panel (#1102)
* feat(studio): GSAP tween editing in Design panel
Add a GSAP animation editor to the studio Design panel: select an element,
view and edit its tweens (properties, easing, timing), add/delete animations,
and drag custom bezier speed curves — all persisted back to the composition
HTML. Gated behind VITE_STUDIO_ENABLE_GSAP_PANEL.
Parsing of existing GSAP source now uses a recast + Babel AST parser instead of
regex, giving scope resolution, stable tween IDs, and round-trip preservation of
extras and unresolved raw values.
recast compiles to CommonJS that calls require("fs"), which breaks browser and
Vite SSR bundles. To contain it, @hyperframes/core is split into an isomorphic
layer and a Node-only AST layer:
- gsapSerialize.ts holds the recast-free helpers (serialization, keyframe
conversion, validation, shared types). htmlParser.ts is now fully isomorphic.
- parseGsapScript and the script-mutation helpers live in gsapParser.ts,
reachable only via the @hyperframes/core/gsap-parser subpath, loaded
server-side by the studio-api mutation routes and the linter via dynamic
import (recast stays external under SSR).
- The barrel and the gsap-constants subpath are recast-free, so studio browser
bundles never trace recast.
Adds AST parser unit + stress coverage and e2e helpers for the panel.
* fix(lint): await async lintHyperframeHtml in all callers
lintHyperframeHtml became async (gsap rules use dynamic import)
but lintProject and check-hyperframe-static weren't awaiting it,
causing typecheck failures and runtime crashes in CI.
Also wire LintRule type in gsap rules to fix fallow unused-type
finding, and suppress render.ts exported-for-tests symbols.
This commit is contained in:
@@ -1,12 +1,7 @@
|
||||
import { memo } from "react";
|
||||
import { Clock, Eye, Layers, MessageSquare, Move, X } from "../../icons/SystemIcons";
|
||||
import { type DomEditSelection } from "./domEditing";
|
||||
import {
|
||||
readStudioBoxSize,
|
||||
readStudioPathOffset,
|
||||
readStudioRotation,
|
||||
readGsapTranslateFromTransform,
|
||||
} from "./manualEdits";
|
||||
import { readStudioBoxSize, readStudioPathOffset, readStudioRotation } from "./manualEdits";
|
||||
import type { ImportedFontAsset } from "./fontAssets";
|
||||
import {
|
||||
EMPTY_STYLES,
|
||||
@@ -18,12 +13,13 @@ import {
|
||||
import { MetricField, Section } from "./propertyPanelPrimitives";
|
||||
import { isMediaElement, MediaSection } from "./propertyPanelMediaSection";
|
||||
import { TextSection, StyleSections } from "./propertyPanelSections";
|
||||
import { GsapAnimationSection } from "./GsapAnimationSection";
|
||||
import { STUDIO_GSAP_PANEL_ENABLED } from "./manualEditingAvailability";
|
||||
|
||||
// Re-export helpers that external consumers import from this module
|
||||
export {
|
||||
buildStrokeStyleUpdates,
|
||||
buildStrokeWidthStyleUpdates,
|
||||
clampPanelNumber,
|
||||
getCssFilterFunctionPx,
|
||||
getClipPathInsetPx,
|
||||
inferBoxShadowPreset,
|
||||
@@ -54,6 +50,18 @@ interface PropertyPanelProps {
|
||||
onImportAssets?: (files: FileList) => Promise<string[]>;
|
||||
fontAssets?: ImportedFontAsset[];
|
||||
onImportFonts?: (files: FileList | File[]) => Promise<ImportedFontAsset[]>;
|
||||
gsapAnimations?: import("@hyperframes/core/gsap-parser").GsapAnimation[];
|
||||
gsapMultipleTimelines?: boolean;
|
||||
gsapUnsupportedTimelinePattern?: boolean;
|
||||
onUpdateGsapProperty?: (animId: string, prop: string, value: number | string) => void;
|
||||
onUpdateGsapMeta?: (
|
||||
animId: string,
|
||||
updates: { duration?: number; ease?: string; position?: number },
|
||||
) => void;
|
||||
onDeleteGsapAnimation?: (animId: string) => void;
|
||||
onAddGsapProperty?: (animId: string, prop: string) => void;
|
||||
onRemoveGsapProperty?: (animId: string, prop: string) => void;
|
||||
onAddGsapAnimation?: (method: "to" | "from" | "set") => void;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -146,6 +154,15 @@ export const PropertyPanel = memo(function PropertyPanel({
|
||||
onImportAssets,
|
||||
fontAssets = [],
|
||||
onImportFonts,
|
||||
gsapAnimations = [],
|
||||
gsapMultipleTimelines,
|
||||
gsapUnsupportedTimelinePattern,
|
||||
onUpdateGsapProperty,
|
||||
onUpdateGsapMeta,
|
||||
onDeleteGsapAnimation,
|
||||
onAddGsapProperty,
|
||||
onRemoveGsapProperty,
|
||||
onAddGsapAnimation,
|
||||
}: PropertyPanelProps) {
|
||||
const styles = element?.computedStyles ?? EMPTY_STYLES;
|
||||
|
||||
@@ -186,11 +203,6 @@ export const PropertyPanel = memo(function PropertyPanel({
|
||||
const sourceLabel = element.id ? `#${element.id}` : element.selector;
|
||||
const showEditableSections = element.capabilities.canEditStyles;
|
||||
const manualOffset = readStudioPathOffset(element.element);
|
||||
const gsapTranslate = readGsapTranslateFromTransform(element.element);
|
||||
const visualOffset = {
|
||||
x: manualOffset.x + gsapTranslate.x,
|
||||
y: manualOffset.y + gsapTranslate.y,
|
||||
};
|
||||
const manualSize = readStudioBoxSize(element.element);
|
||||
const resolvedWidth =
|
||||
manualSize.width > 0
|
||||
@@ -204,11 +216,10 @@ export const PropertyPanel = memo(function PropertyPanel({
|
||||
const commitManualOffset = (axis: "x" | "y", nextValue: string) => {
|
||||
const parsed = parsePxMetricValue(nextValue);
|
||||
if (parsed == null) return;
|
||||
const currentRaw = readStudioPathOffset(element.element);
|
||||
const currentGsap = readGsapTranslateFromTransform(element.element);
|
||||
const current = readStudioPathOffset(element.element);
|
||||
onSetManualOffset(element, {
|
||||
x: axis === "x" ? parsed - currentGsap.x : currentRaw.x,
|
||||
y: axis === "y" ? parsed - currentGsap.y : currentRaw.y,
|
||||
x: axis === "x" ? parsed : current.x,
|
||||
y: axis === "y" ? parsed : current.y,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -300,14 +311,14 @@ export const PropertyPanel = memo(function PropertyPanel({
|
||||
<div className={RESPONSIVE_GRID}>
|
||||
<MetricField
|
||||
label="X"
|
||||
value={formatPxMetricValue(visualOffset.x)}
|
||||
value={formatPxMetricValue(manualOffset.x)}
|
||||
disabled={manualOffsetEditingDisabled}
|
||||
scrub
|
||||
onCommit={(next) => commitManualOffset("x", next)}
|
||||
/>
|
||||
<MetricField
|
||||
label="Y"
|
||||
value={formatPxMetricValue(visualOffset.y)}
|
||||
value={formatPxMetricValue(manualOffset.y)}
|
||||
disabled={manualOffsetEditingDisabled}
|
||||
scrub
|
||||
onCommit={(next) => commitManualOffset("y", next)}
|
||||
@@ -342,6 +353,25 @@ export const PropertyPanel = memo(function PropertyPanel({
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{STUDIO_GSAP_PANEL_ENABLED &&
|
||||
onUpdateGsapProperty &&
|
||||
onUpdateGsapMeta &&
|
||||
onDeleteGsapAnimation &&
|
||||
onAddGsapProperty &&
|
||||
onAddGsapAnimation && (
|
||||
<GsapAnimationSection
|
||||
animations={gsapAnimations}
|
||||
multipleTimelines={gsapMultipleTimelines}
|
||||
unsupportedTimelinePattern={gsapUnsupportedTimelinePattern}
|
||||
onUpdateProperty={onUpdateGsapProperty}
|
||||
onUpdateMeta={onUpdateGsapMeta}
|
||||
onDeleteAnimation={onDeleteGsapAnimation}
|
||||
onAddProperty={onAddGsapProperty}
|
||||
onRemoveProperty={onRemoveGsapProperty ?? (() => {})}
|
||||
onAddAnimation={onAddGsapAnimation}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showEditableSections && (
|
||||
<StyleSections
|
||||
projectId={projectId}
|
||||
|
||||
Reference in New Issue
Block a user