mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30:03 +00:00
fix(studio): correct gesture commits for scaled and graded elements
- resize on a scale-driven element commits per-axis scale (scaleX/scaleY for non-uniform drags) with keyframe normalization to the longhands, and clears the width/height draft so size can't double-apply; the intercept moves to gsapResizeIntercept.ts - the drop frame applies the corrected position synchronously in the same microtask chain as the soft reload (no network-window jump), and the draft pins the anchor through accumulated moves on scaled elements - gesture size/position math divides by the element's own content scale - convert-to-keyframes resolves current values through the property-group filter for ALL capture passes (opacity/rotationX from unrelated tweens no longer leak into a rotation commit), and a grading-hidden source's opacity is read from its canvas, not the inline hide - canvas pointer-down confirms the hover target with a synchronous hit-test before starting a marquee (stale-hover race lost selections)
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
||||
import {
|
||||
COLOR_GRADING_SOURCE_HIDDEN_ATTR,
|
||||
HF_COLOR_GRADING_CANVAS_ID_PREFIX,
|
||||
} from "@hyperframes/core/color-grading";
|
||||
import { readAllAnimatedProperties, readGsapProperty } from "./gsapRuntimeReaders";
|
||||
|
||||
/**
|
||||
* Regression: converting a property-group tween to keyframes resolves "current
|
||||
* values" via readAllAnimatedProperties. Two ways that used to bake garbage
|
||||
* into the composition file:
|
||||
*
|
||||
* 1. The group filter only pruned the tween's OWN properties — the baseline
|
||||
* pass still captured every property ANY tween on the element touches, so
|
||||
* a rotation commit carried `opacity` from the intro from() tween.
|
||||
* 2. `gsap.getProperty(el, "opacity")` on a color-grading source reads the
|
||||
* runtime hide (inline `opacity: 0 !important`), not the animated value —
|
||||
* so the captured opacity was the transient 0, which then animated 0 → 0
|
||||
* on the next full load and the element disappeared.
|
||||
*/
|
||||
|
||||
function fakeIframe(
|
||||
el: Element,
|
||||
opts: { gsapValues: Record<string, number>; otherTweenVars?: Record<string, number> },
|
||||
): HTMLIFrameElement {
|
||||
const children = opts.otherTweenVars
|
||||
? [{ targets: () => [el], vars: { duration: 0.8, ...opts.otherTweenVars } }]
|
||||
: [];
|
||||
return {
|
||||
contentWindow: {
|
||||
__timelines: { main: { getChildren: () => children } },
|
||||
gsap: { getProperty: (_el: Element, prop: string) => opts.gsapValues[prop] ?? 0 },
|
||||
},
|
||||
contentDocument: document,
|
||||
} as unknown as HTMLIFrameElement;
|
||||
}
|
||||
|
||||
function rotationSetAnim(): GsapAnimation {
|
||||
return {
|
||||
id: "#clip-set-0-rotation",
|
||||
targetSelector: "#clip",
|
||||
method: "set",
|
||||
properties: { rotation: 0 },
|
||||
} as unknown as GsapAnimation;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
describe("readAllAnimatedProperties group filter", () => {
|
||||
it("keeps other tweens' out-of-group properties out of a grouped resolve", () => {
|
||||
const el = document.createElement("div");
|
||||
el.id = "clip";
|
||||
document.body.appendChild(el);
|
||||
const iframe = fakeIframe(el, {
|
||||
gsapValues: { rotation: -28.1, opacity: 0, rotationX: 52, rotationY: -47 },
|
||||
otherTweenVars: { opacity: 0, rotationX: 52, rotationY: -47 },
|
||||
});
|
||||
|
||||
const result = readAllAnimatedProperties(iframe, "#clip", rotationSetAnim(), "rotation");
|
||||
|
||||
expect(result).toEqual({ rotation: -28.1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("color-grading opacity truth", () => {
|
||||
function gradedElement(): HTMLElement {
|
||||
const el = document.createElement("img");
|
||||
el.id = "clip";
|
||||
el.setAttribute(COLOR_GRADING_SOURCE_HIDDEN_ATTR, "");
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.id = `${HF_COLOR_GRADING_CANVAS_ID_PREFIX}clip`;
|
||||
canvas.style.opacity = "0.98";
|
||||
document.body.append(el, canvas);
|
||||
return el;
|
||||
}
|
||||
|
||||
it("resolves opacity from the grading canvas, not the runtime hide", () => {
|
||||
const el = gradedElement();
|
||||
const iframe = fakeIframe(el, { gsapValues: { opacity: 0 } });
|
||||
const anim = {
|
||||
id: "#clip-from-200-visual",
|
||||
targetSelector: "#clip",
|
||||
method: "from",
|
||||
properties: { opacity: 0.5 },
|
||||
} as unknown as GsapAnimation;
|
||||
|
||||
const result = readAllAnimatedProperties(iframe, "#clip", anim);
|
||||
|
||||
expect(result.opacity).toBe(0.98);
|
||||
});
|
||||
|
||||
it("readGsapProperty takes the same detour", () => {
|
||||
const el = gradedElement();
|
||||
const iframe = fakeIframe(el, { gsapValues: { opacity: 0 } });
|
||||
|
||||
expect(readGsapProperty(iframe, "#clip", "opacity")).toBe(0.98);
|
||||
});
|
||||
|
||||
it("reads GSAP directly when the source is not grading-hidden", () => {
|
||||
const el = document.createElement("div");
|
||||
el.id = "clip";
|
||||
document.body.appendChild(el);
|
||||
const iframe = fakeIframe(el, { gsapValues: { opacity: 0.3 } });
|
||||
|
||||
expect(readGsapProperty(iframe, "#clip", "opacity")).toBe(0.3);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user