mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
fix(studio): property panel group-aware keyframe routing (#1358)
* fix(core): per-property-group keyframe foundations
Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.
* fix(core): add split-into-property-groups and replace-with-keyframes mutations
Server-side mutations for atomic property-group splitting and keyframe
replacement. Client commitMutation returns early on changed:false instead
of throwing.
* fix(studio): per-property-group intercept routing + drag/resize fixes
Rewire GSAP runtime bridge for property-group routing: drag sends only {x,y}
to position group, resize routes to scale group via data-hf-studio-original-width,
rotation routes to rotation group. Add resolveGroupTween helper, from-extend
with split-first-then-position-only pattern, autoKeyframeEnabled guards,
GSAP base + delta fix in drag draft, cancel-restores-GSAP-x/y from data attrs.
* fix(studio): keyframe cache propertyGroup tagging + timeline UI fixes
Tag cached keyframes with propertyGroup for group-aware operations.
Add tweenPercentage for accurate keyframe matching, activeKeyframePct
for diamond-click targeting, context menu offset, selected diamond z-index,
clearProps after kill in soft reload.
* fix(studio): property panel group-aware keyframe routing
Add animIdForProp helper routing keyframe diamonds to correct property-group
animation. Wire StudioPreviewArea delete/move/toggle handlers to use
propertyGroup for routing. Fix per-property epsilon in rdpSimplify.
This commit is contained in:
@@ -154,25 +154,40 @@ export function StudioPreviewArea({
|
|||||||
onRazorSplitAll={handleRazorSplitAll}
|
onRazorSplitAll={handleRazorSplitAll}
|
||||||
onSelectTimelineElement={handleTimelineElementSelect}
|
onSelectTimelineElement={handleTimelineElementSelect}
|
||||||
onDeleteAllKeyframes={(_elId) => {
|
onDeleteAllKeyframes={(_elId) => {
|
||||||
const anim =
|
for (const anim of selectedGsapAnimations) {
|
||||||
selectedGsapAnimations.find((a) => a.keyframes) ?? selectedGsapAnimations[0];
|
handleGsapDeleteAnimation(anim.id);
|
||||||
if (anim) handleGsapDeleteAnimation(anim.id);
|
}
|
||||||
}}
|
}}
|
||||||
onDeleteKeyframe={(_elId, pct) => {
|
onDeleteKeyframe={(_elId, pct) => {
|
||||||
const anim = selectedGsapAnimations.find((a) => a.keyframes);
|
const cacheKey = domEditSelection?.id ?? "";
|
||||||
if (anim) handleGsapRemoveKeyframe(anim.id, pct);
|
const cached = usePlayerStore.getState().keyframeCache.get(cacheKey);
|
||||||
|
const kf = cached?.keyframes.find((k) => Math.abs(k.percentage - pct) < 0.2);
|
||||||
|
const group = kf?.propertyGroup;
|
||||||
|
const anim =
|
||||||
|
(group ? selectedGsapAnimations.find((a) => a.propertyGroup === group) : undefined) ??
|
||||||
|
selectedGsapAnimations.find((a) => a.keyframes);
|
||||||
|
if (!anim) return;
|
||||||
|
handleGsapRemoveKeyframe(anim.id, kf?.tweenPercentage ?? pct);
|
||||||
}}
|
}}
|
||||||
onChangeKeyframeEase={(_elId, _pct, ease) => {
|
onChangeKeyframeEase={(_elId, _pct, ease) => {
|
||||||
const anim = selectedGsapAnimations.find((a) => a.keyframes);
|
for (const anim of selectedGsapAnimations) {
|
||||||
if (anim) handleGsapUpdateMeta(anim.id, { ease });
|
if (anim.keyframes) handleGsapUpdateMeta(anim.id, { ease });
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
// fallow-ignore-next-line complexity
|
// fallow-ignore-next-line complexity
|
||||||
onMoveKeyframe={(_el, oldPct, newPct) => {
|
onMoveKeyframe={(_el, oldPct, newPct) => {
|
||||||
const anim = selectedGsapAnimations.find((a) => a.keyframes);
|
const cacheKey = domEditSelection?.id ?? "";
|
||||||
|
const cached = usePlayerStore.getState().keyframeCache.get(cacheKey);
|
||||||
|
const cachedKf = cached?.keyframes.find((k) => Math.abs(k.percentage - oldPct) < 0.2);
|
||||||
|
const group = cachedKf?.propertyGroup;
|
||||||
|
const anim =
|
||||||
|
(group ? selectedGsapAnimations.find((a) => a.propertyGroup === group) : undefined) ??
|
||||||
|
selectedGsapAnimations.find((a) => a.keyframes);
|
||||||
if (!anim?.keyframes) return;
|
if (!anim?.keyframes) return;
|
||||||
|
const tweenOldPct = cachedKf?.tweenPercentage ?? oldPct;
|
||||||
const kf = anim.keyframes.keyframes.find((k) => k.percentage === oldPct);
|
const kf = anim.keyframes.keyframes.find((k) => k.percentage === oldPct);
|
||||||
if (!kf) return;
|
if (!kf) return;
|
||||||
handleGsapRemoveKeyframe(anim.id, oldPct);
|
handleGsapRemoveKeyframe(anim.id, tweenOldPct);
|
||||||
for (const [prop, val] of Object.entries(kf.properties)) {
|
for (const [prop, val] of Object.entries(kf.properties)) {
|
||||||
handleGsapAddKeyframe(anim.id, newPct, prop, val);
|
handleGsapAddKeyframe(anim.id, newPct, prop, val);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
readGsapBorderRadiusForPanel,
|
readGsapBorderRadiusForPanel,
|
||||||
} from "./propertyPanelHelpers";
|
} from "./propertyPanelHelpers";
|
||||||
import { MetricField, Section } from "./propertyPanelPrimitives";
|
import { MetricField, Section } from "./propertyPanelPrimitives";
|
||||||
|
import { classifyPropertyGroup } from "@hyperframes/core/gsap-parser";
|
||||||
import { isMediaElement, MediaSection } from "./propertyPanelMediaSection";
|
import { isMediaElement, MediaSection } from "./propertyPanelMediaSection";
|
||||||
import { TextSection, StyleSections } from "./propertyPanelSections";
|
import { TextSection, StyleSections } from "./propertyPanelSections";
|
||||||
import { GsapAnimationSection } from "./GsapAnimationSection";
|
import { GsapAnimationSection } from "./GsapAnimationSection";
|
||||||
@@ -227,6 +228,13 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|||||||
const navKeyframes = cacheEntry?.keyframes ?? gsapKeyframes;
|
const navKeyframes = cacheEntry?.keyframes ?? gsapKeyframes;
|
||||||
const seekFromKfPct = (pct: number) => onSeekToTime?.(elStart + (pct / 100) * elDuration);
|
const seekFromKfPct = (pct: number) => onSeekToTime?.(elStart + (pct / 100) * elDuration);
|
||||||
|
|
||||||
|
const animIdForProp = (prop: string): string => {
|
||||||
|
const group = classifyPropertyGroup(prop);
|
||||||
|
const groupAnim = gsapAnimations?.find((a) => a.propertyGroup === group);
|
||||||
|
if (groupAnim) return groupAnim.id;
|
||||||
|
return gsapAnimId ?? "";
|
||||||
|
};
|
||||||
|
|
||||||
// Read ALL GSAP-interpolated values at the current seek time.
|
// Read ALL GSAP-interpolated values at the current seek time.
|
||||||
const gsapRuntimeValues = readGsapRuntimeValuesForPanel(
|
const gsapRuntimeValues = readGsapRuntimeValuesForPanel(
|
||||||
gsapAnimId,
|
gsapAnimId,
|
||||||
@@ -395,8 +403,8 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|||||||
onCommitAnimatedProperty &&
|
onCommitAnimatedProperty &&
|
||||||
void onCommitAnimatedProperty(element, "x", displayX)
|
void onCommitAnimatedProperty(element, "x", displayX)
|
||||||
}
|
}
|
||||||
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(gsapAnimId, pct)}
|
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("x"), pct)}
|
||||||
onConvertToKeyframes={() => onConvertToKeyframes?.(gsapAnimId)}
|
onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("x"))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -420,8 +428,8 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|||||||
onCommitAnimatedProperty &&
|
onCommitAnimatedProperty &&
|
||||||
void onCommitAnimatedProperty(element, "y", displayY)
|
void onCommitAnimatedProperty(element, "y", displayY)
|
||||||
}
|
}
|
||||||
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(gsapAnimId, pct)}
|
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("y"), pct)}
|
||||||
onConvertToKeyframes={() => onConvertToKeyframes?.(gsapAnimId)}
|
onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("y"))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -445,8 +453,8 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|||||||
onCommitAnimatedProperty &&
|
onCommitAnimatedProperty &&
|
||||||
void onCommitAnimatedProperty(element, "width", displayW)
|
void onCommitAnimatedProperty(element, "width", displayW)
|
||||||
}
|
}
|
||||||
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(gsapAnimId, pct)}
|
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("width"), pct)}
|
||||||
onConvertToKeyframes={() => onConvertToKeyframes?.(gsapAnimId)}
|
onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("width"))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -470,8 +478,8 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|||||||
onCommitAnimatedProperty &&
|
onCommitAnimatedProperty &&
|
||||||
void onCommitAnimatedProperty(element, "height", displayH)
|
void onCommitAnimatedProperty(element, "height", displayH)
|
||||||
}
|
}
|
||||||
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(gsapAnimId, pct)}
|
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("height"), pct)}
|
||||||
onConvertToKeyframes={() => onConvertToKeyframes?.(gsapAnimId)}
|
onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("height"))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -493,8 +501,8 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|||||||
onCommitAnimatedProperty &&
|
onCommitAnimatedProperty &&
|
||||||
void onCommitAnimatedProperty(element, "rotation", displayR)
|
void onCommitAnimatedProperty(element, "rotation", displayR)
|
||||||
}
|
}
|
||||||
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(gsapAnimId, pct)}
|
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("rotation"), pct)}
|
||||||
onConvertToKeyframes={() => onConvertToKeyframes?.(gsapAnimId)}
|
onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("rotation"))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -503,6 +511,7 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|||||||
<PropertyPanel3dTransform
|
<PropertyPanel3dTransform
|
||||||
gsapRuntimeValues={gsapRuntimeValues}
|
gsapRuntimeValues={gsapRuntimeValues}
|
||||||
gsapAnimId={gsapAnimId}
|
gsapAnimId={gsapAnimId}
|
||||||
|
resolveAnimIdForProp={animIdForProp}
|
||||||
gsapKeyframes={navKeyframes}
|
gsapKeyframes={navKeyframes}
|
||||||
currentPct={currentPct}
|
currentPct={currentPct}
|
||||||
elStart={elStart}
|
elStart={elStart}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type KeyframeEntry = Array<{
|
|||||||
interface PropertyPanel3dTransformProps {
|
interface PropertyPanel3dTransformProps {
|
||||||
gsapRuntimeValues: Record<string, number>;
|
gsapRuntimeValues: Record<string, number>;
|
||||||
gsapAnimId: string | null;
|
gsapAnimId: string | null;
|
||||||
|
resolveAnimIdForProp?: (prop: string) => string | null;
|
||||||
gsapKeyframes: KeyframeEntry;
|
gsapKeyframes: KeyframeEntry;
|
||||||
currentPct: number;
|
currentPct: number;
|
||||||
elStart: number;
|
elStart: number;
|
||||||
@@ -31,6 +32,7 @@ interface PropertyPanel3dTransformProps {
|
|||||||
export function PropertyPanel3dTransform({
|
export function PropertyPanel3dTransform({
|
||||||
gsapRuntimeValues,
|
gsapRuntimeValues,
|
||||||
gsapAnimId,
|
gsapAnimId,
|
||||||
|
resolveAnimIdForProp,
|
||||||
gsapKeyframes,
|
gsapKeyframes,
|
||||||
currentPct,
|
currentPct,
|
||||||
elStart,
|
elStart,
|
||||||
@@ -41,6 +43,7 @@ export function PropertyPanel3dTransform({
|
|||||||
onRemoveKeyframe,
|
onRemoveKeyframe,
|
||||||
onConvertToKeyframes,
|
onConvertToKeyframes,
|
||||||
}: PropertyPanel3dTransformProps) {
|
}: PropertyPanel3dTransformProps) {
|
||||||
|
const idFor = (prop: string) => resolveAnimIdForProp?.(prop) ?? gsapAnimId;
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 border-t border-neutral-800/40 pt-3">
|
<div className="mt-3 border-t border-neutral-800/40 pt-3">
|
||||||
<div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-neutral-600">
|
<div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-neutral-600">
|
||||||
@@ -72,8 +75,14 @@ export function PropertyPanel3dTransform({
|
|||||||
void onCommitAnimatedProperty(element, "z", gsapRuntimeValues?.z ?? 0);
|
void onCommitAnimatedProperty(element, "z", gsapRuntimeValues?.z ?? 0);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onRemoveKeyframe={(pct) => gsapAnimId && onRemoveKeyframe?.(gsapAnimId, pct)}
|
onRemoveKeyframe={(pct) => {
|
||||||
onConvertToKeyframes={() => gsapAnimId && onConvertToKeyframes?.(gsapAnimId)}
|
const id = idFor("z");
|
||||||
|
if (id) onRemoveKeyframe?.(id, pct);
|
||||||
|
}}
|
||||||
|
onConvertToKeyframes={() => {
|
||||||
|
const id = idFor("z");
|
||||||
|
if (id) onConvertToKeyframes?.(id);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -102,8 +111,14 @@ export function PropertyPanel3dTransform({
|
|||||||
void onCommitAnimatedProperty(element, "scale", gsapRuntimeValues?.scale ?? 1);
|
void onCommitAnimatedProperty(element, "scale", gsapRuntimeValues?.scale ?? 1);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onRemoveKeyframe={(pct) => gsapAnimId && onRemoveKeyframe?.(gsapAnimId, pct)}
|
onRemoveKeyframe={(pct) => {
|
||||||
onConvertToKeyframes={() => gsapAnimId && onConvertToKeyframes?.(gsapAnimId)}
|
const id = idFor("scale");
|
||||||
|
if (id) onRemoveKeyframe?.(id, pct);
|
||||||
|
}}
|
||||||
|
onConvertToKeyframes={() => {
|
||||||
|
const id = idFor("scale");
|
||||||
|
if (id) onConvertToKeyframes?.(id);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ function simplifyTimeSeries(
|
|||||||
export function simplifyGestureSamples(
|
export function simplifyGestureSamples(
|
||||||
samples: Array<{ time: number; properties: Record<string, number> }>,
|
samples: Array<{ time: number; properties: Record<string, number> }>,
|
||||||
totalDuration: number,
|
totalDuration: number,
|
||||||
epsilon: number,
|
epsilon: number | ((key: string) => number),
|
||||||
): Map<number, Record<string, number>> {
|
): Map<number, Record<string, number>> {
|
||||||
if (samples.length === 0) return new Map();
|
if (samples.length === 0) return new Map();
|
||||||
if (totalDuration <= 0) return new Map();
|
if (totalDuration <= 0) return new Map();
|
||||||
@@ -120,7 +120,8 @@ export function simplifyGestureSamples(
|
|||||||
series.push({ time: s.time, value: s.properties[key] });
|
series.push({ time: s.time, value: s.properties[key] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const simplified = simplifyTimeSeries(series, epsilon);
|
const keyEpsilon = typeof epsilon === "function" ? epsilon(key) : epsilon;
|
||||||
|
const simplified = simplifyTimeSeries(series, keyEpsilon);
|
||||||
for (const pt of simplified) {
|
for (const pt of simplified) {
|
||||||
survivingTimes.add(pt.time);
|
survivingTimes.add(pt.time);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user