mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 07:09:59 +00:00
fix(runtime): immediateRender for set tweens + array timeline normalization (#1692)
* chore(studio): remove all console.* calls from studio package * chore(studio): address review — remove dead stubs, restore consent notice - Delete empty if-blocks left after console removal (snapTargetCollection, Player asset-poll, useTimelineSyncCallbacks 5s probe, useGestureRecording dev guard + now-unused isDevBuild) and the stale "surface in dev" comment. - Drop the dangling no-console pragma + dead duplicate-id branch in sourcePatcher. - Restore the one-time telemetry consent disclosure in showNoticeOnce (kept behind a pragma — it is a user-facing notice, not debug noise). - Remove the missed timelineIcons console.warn while preserving the `tag || "div"` null-safety fallback. - Route caption auto-save failures (a data-loss path) through telemetry instead of swallowing silently. - Restore the accidentally-clobbered css-var-fonts output.mp4 fixture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(runtime): immediateRender for set tweens + array timeline normalization - Set tweens now emit immediateRender:true so they render on page load without requiring the runtime to seek past position 0 - Runtime IIFE normalizes array timelines (window.__timelines = [tl]) to keyed objects, and auto-adds data-start on root elements - Drag teardown clears translate:none to prevent #1673 fly-off - Position-only set tweens hidden from timeline diamonds (3 cache paths) - Parser: ease-only keyframe update preserves existing properties * fix(runtime): address review — restore perf gate, debug surface, scrub restore - Restore the #1651 skipForInjectedVideo gate in media.ts that was dropped on restack — avoids ~2400 wasted per-tick seeks on video-heavy renders. - Restore the console.debug body + docstring bullet of swallow() in diagnostics.ts: the __hfDebug opt-in debug surface had been gutted to an empty if-block. - Rebind: after the progress-cycle set() kick, seek to state.currentTime via totalTime() instead of snapping to 0, so a rebind after scrub / soft-reload restore keeps the playhead. - Array __timelines normalization + data-start default now resolve the root via a shared findRootCompositionEl() that honors data-root="true" first (matches resolveRootCompositionElement, which now delegates to it). - Ease-only keyframe update leaves a primitive (non-object) keyframe value untouched instead of wiping it to {}; add a preservation unit test. - Document the boundDuration<=0 progress(1) kick + restore the STATIC-case comment in gsapRuntimeBridge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
adb40321d6
commit
6987447a75
@@ -1792,6 +1792,19 @@ describe("keyframe mutations", () => {
|
||||
expect(kf100.properties.y).toBe(50);
|
||||
});
|
||||
|
||||
it("updateKeyframeInScript — ease-only update preserves existing properties", () => {
|
||||
// Per-keyframe ease editing passes empty properties + an ease. The existing
|
||||
// property bag must survive (don't wipe x/opacity when only the ease changes).
|
||||
const id = getAnimId(KF_SCRIPT);
|
||||
const updated = updateKeyframeInScript(KF_SCRIPT, id, 100, {}, "power2.inOut");
|
||||
const kf100 = parseGsapScript(updated).animations[0].keyframes!.keyframes.find(
|
||||
(k) => k.percentage === 100,
|
||||
)!;
|
||||
expect(kf100.ease).toBe("power2.inOut");
|
||||
expect(kf100.properties.x).toBe(200);
|
||||
expect(kf100.properties.opacity).toBe(1);
|
||||
});
|
||||
|
||||
// Array-form keyframes (`keyframes: [{x,y}, …]`) carry no percentages — GSAP
|
||||
// distributes them evenly. The motion-path overlay drags/adds by percentage,
|
||||
// which used to no-op on array-authored tweens (#puck-b / #shuttle).
|
||||
|
||||
@@ -1243,13 +1243,17 @@ function applyEaseUpdate(varsArg: AstNode, ease: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function applyUpdatesToCall(call: TweenCallInfo, updates: Partial<GsapAnimation>): void {
|
||||
function applyUpdatesToCall(
|
||||
call: TweenCallInfo,
|
||||
updates: Partial<GsapAnimation> & { easeEach?: string },
|
||||
): void {
|
||||
if (updates.properties) reconcileEditableProperties(call.varsArg, updates.properties);
|
||||
if (updates.fromProperties && call.method === "fromTo" && call.fromArg) {
|
||||
reconcileEditableProperties(call.fromArg, updates.fromProperties);
|
||||
}
|
||||
if (updates.duration !== undefined) setVarsKey(call.varsArg, "duration", updates.duration);
|
||||
if (updates.ease !== undefined) applyEaseUpdate(call.varsArg, updates.ease);
|
||||
if (updates.easeEach !== undefined) applyEaseUpdate(call.varsArg, updates.easeEach);
|
||||
else if (updates.ease !== undefined) applyEaseUpdate(call.varsArg, updates.ease);
|
||||
if (updates.position !== undefined) {
|
||||
const posIdx = call.method === "fromTo" ? 3 : 2;
|
||||
call.node.arguments[posIdx] = parseExpr(valueToCode(updates.position));
|
||||
@@ -1282,10 +1286,13 @@ function insertAfterAnchor(parsed: ParsedGsapAst, newStatement: AstNode): void {
|
||||
function buildTweenStatementCode(timelineVar: string, anim: Omit<GsapAnimation, "id">): string {
|
||||
const selector = JSON.stringify(anim.targetSelector);
|
||||
const props: Record<string, number | string> = { ...anim.properties };
|
||||
// `set` is instantaneous — GSAP ignores duration on it, so don't emit one.
|
||||
if (anim.method !== "set" && anim.duration !== undefined) props.duration = anim.duration;
|
||||
if (anim.ease) props.ease = anim.ease;
|
||||
const entries = Object.entries(props).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
||||
// immediateRender forces GSAP to apply the set when added to the timeline,
|
||||
// not on the first seek — without it, tl.set at position 0 on a paused
|
||||
// timeline is invisible until the playhead moves past 0.
|
||||
if (anim.method === "set") entries.push("immediateRender: true");
|
||||
if (anim.extras) {
|
||||
for (const [k, v] of Object.entries(anim.extras)) {
|
||||
entries.push(`${safeKey(k)}: ${valueToCode(v as number | string)}`);
|
||||
@@ -1308,7 +1315,7 @@ function buildTweenStatementCode(timelineVar: string, anim: Omit<GsapAnimation,
|
||||
export function updateAnimationInScript(
|
||||
script: string,
|
||||
animationId: string,
|
||||
updates: Partial<GsapAnimation>,
|
||||
updates: Partial<GsapAnimation> & { easeEach?: string },
|
||||
): string {
|
||||
let parsed: ParsedGsapAst;
|
||||
try {
|
||||
@@ -1437,6 +1444,7 @@ export function addAnimationWithKeyframesToScript(
|
||||
auto?: boolean;
|
||||
}>,
|
||||
ease?: string,
|
||||
easeEach?: string,
|
||||
): { script: string; id: string } {
|
||||
let parsed: ParsedGsapAst;
|
||||
try {
|
||||
@@ -1450,7 +1458,7 @@ export function addAnimationWithKeyframesToScript(
|
||||
}
|
||||
|
||||
const selector = JSON.stringify(targetSelector);
|
||||
const kfCode = buildKeyframeObjectCode(keyframes);
|
||||
const kfCode = buildKeyframeObjectCode(keyframes, easeEach ? { easeEach } : undefined);
|
||||
const varEntries = [`keyframes: ${kfCode}`, `duration: ${valueToCode(duration)}`];
|
||||
if (ease) varEntries.push(`ease: ${JSON.stringify(ease)}`);
|
||||
const posCode = valueToCode(position);
|
||||
@@ -2216,6 +2224,27 @@ export function updateKeyframeInScript(
|
||||
const match = findKeyframePropByPct(kfNode, percentage);
|
||||
if (!match) return script;
|
||||
|
||||
if (Object.keys(properties).length === 0 && ease) {
|
||||
// Ease-only update: preserve existing properties, just add/replace ease
|
||||
const existing = match.prop.value;
|
||||
if (existing?.type === "ObjectExpression") {
|
||||
const props = (existing.properties ?? []) as AstNode[];
|
||||
const easeIdx = props.findIndex(
|
||||
(p: AstNode) => isObjectProperty(p) && propKeyName(p) === "ease",
|
||||
);
|
||||
const easeNode = parseExpr(`({ ease: ${JSON.stringify(ease)} })`).properties[0];
|
||||
if (easeIdx >= 0) {
|
||||
props[easeIdx] = easeNode;
|
||||
} else {
|
||||
props.push(easeNode);
|
||||
}
|
||||
return recast.print(loc.parsed.ast).code;
|
||||
}
|
||||
// Non-object keyframe value (primitive shorthand, e.g. "50%": "0.5"): there
|
||||
// is no property bag to merge the ease into. Rebuilding from empty
|
||||
// `properties` would wipe the primitive — leave the keyframe untouched.
|
||||
return script;
|
||||
}
|
||||
match.prop.value = buildKeyframeValueNode(properties, ease);
|
||||
return recast.print(loc.parsed.ast).code;
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ function findInsertionPoint(parsed: ParsedGsapAcornForWrite): number | null {
|
||||
export function updateAnimationInScript(
|
||||
script: string,
|
||||
animationId: string,
|
||||
updates: Partial<GsapAnimation>,
|
||||
updates: Partial<GsapAnimation> & { easeEach?: string },
|
||||
): string {
|
||||
if (!Object.keys(updates).length) return script;
|
||||
const parsed = parseGsapScriptAcornForWrite(script);
|
||||
@@ -324,13 +324,11 @@ export function updateAnimationInScript(
|
||||
if (updates.duration !== undefined) {
|
||||
upsertProp(ms, call.varsArg, "duration", updates.duration);
|
||||
}
|
||||
if (updates.ease !== undefined) {
|
||||
// For a keyframe tween, easing lives at keyframes.easeEach (per-keyframe),
|
||||
// not a top-level ease. Writing top-level ease would leave the per-keyframe
|
||||
// easing unchanged — the user's edit would silently do nothing.
|
||||
const easeValue = updates.easeEach ?? updates.ease;
|
||||
if (easeValue !== undefined) {
|
||||
const kfNode = keyframesObjectNode(call.varsArg);
|
||||
if (kfNode) upsertProp(ms, kfNode, "easeEach", updates.ease);
|
||||
else upsertProp(ms, call.varsArg, "ease", updates.ease);
|
||||
if (kfNode) upsertProp(ms, kfNode, "easeEach", easeValue);
|
||||
else upsertProp(ms, call.varsArg, "ease", easeValue);
|
||||
}
|
||||
if (updates.extras) {
|
||||
for (const [key, value] of Object.entries(updates.extras)) {
|
||||
@@ -1338,6 +1336,7 @@ export function addAnimationWithKeyframesToScript(
|
||||
auto?: boolean;
|
||||
}>,
|
||||
ease?: string,
|
||||
easeEach?: string,
|
||||
): { script: string; id: string } {
|
||||
const parsed = parseGsapScriptAcornForWrite(script);
|
||||
if (!parsed) return { script, id: "" };
|
||||
@@ -1345,7 +1344,7 @@ export function addAnimationWithKeyframesToScript(
|
||||
if (insertionPoint === null) return { script, id: "" };
|
||||
|
||||
const sorted = [...keyframes].sort((a, b) => a.percentage - b.percentage);
|
||||
const kfObjCode = buildKeyframeObjectCode(sorted);
|
||||
const kfObjCode = buildKeyframeObjectCode(sorted, easeEach);
|
||||
const varParts = [`keyframes: ${kfObjCode}`, `duration: ${valueToCode(duration)}`];
|
||||
if (ease) varParts.push(`ease: ${JSON.stringify(ease)}`);
|
||||
const stmtCode = `${parsed.timelineVar}.to(${JSON.stringify(targetSelector)}, { ${varParts.join(", ")} }, ${valueToCode(position)});`;
|
||||
|
||||
Reference in New Issue
Block a user