mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 17:30:50 +00:00
- 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)
112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
// @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);
|
|
});
|
|
});
|