Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions packages/lint/src/rules/gsap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,48 @@ describe("GSAP rules", () => {
expect(finding).toBeUndefined();
});

it("does not error when a full-frame overlay uses a GSAP fromTo entrance at frame zero", async () => {
const html = `
<html><body data-composition-id="c1" data-width="1920" data-height="1080">
<div id="tr-flash-1" style="position:fixed;inset:0;background:#fff;pointer-events:none;z-index:990"></div>
<section class="clip" data-start="0" data-duration="8"><h1>Scene 1</h1></section>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.fromTo("#tr-flash-1", { opacity: 0 }, { opacity: 1, duration: 0.3 }, 0);
window.__timelines["c1"] = tl;
</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find(
(f) => f.code === "gsap_fullscreen_overlay_starts_visible",
);
expect(finding).toBeUndefined();
});

it("does not error when a full-frame overlay's GSAP fromTo entrance at frame zero is later hidden again", async () => {
const html = `
<html><body data-composition-id="c1" data-width="1920" data-height="1080">
<div id="tr-flash-1" style="position:fixed;inset:0;background:#fff;pointer-events:none;z-index:990"></div>
<section class="clip" data-start="0" data-duration="8"><h1>Scene 1</h1></section>
<section class="clip" data-start="8" data-duration="8"><h1>Scene 2</h1></section>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.fromTo("#tr-flash-1", { opacity: 0 }, { opacity: 1, duration: 0.3 }, 0);
tl.to("#tr-flash-1", { opacity: 0, duration: 0.18 }, 8.00);
window.__timelines["c1"] = tl;
</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find(
(f) => f.code === "gsap_fullscreen_overlay_starts_visible",
);
expect(finding).toBeUndefined();
});

it("reports one full-frame transition flash finding when multiple scripts touch it", async () => {
const html = `
<html><body data-composition-id="c1" data-width="1920" data-height="1080">
Expand Down
18 changes: 17 additions & 1 deletion packages/lint/src/rules/gsap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ type GsapWindow = {
end: number;
properties: string[];
propertyValues: Record<string, string | number>;
// fromTo()'s first ("from") vars object — the true state at the tween's start,
// as opposed to propertyValues which is always the destination (toVars). Only
// set for method === "fromTo"; other methods have no separate from-state.
fromPropertyValues?: Record<string, string | number>;
overwriteAuto: boolean;
method: string;
raw: string;
Expand Down Expand Up @@ -141,6 +145,7 @@ async function extractGsapWindows(script: string): Promise<GsapWindow[]> {
end: animation.position + effectiveDuration,
properties: Object.keys(animation.properties),
propertyValues: animation.properties,
fromPropertyValues: animation.fromProperties,
overwriteAuto: unwrapRaw(animation.extras?.overwrite) === "auto",
method: animation.method,
raw: synthesizeWindowRaw(parsed.timelineVar, animation),
Expand Down Expand Up @@ -209,6 +214,16 @@ function isVisibleGsapState(values: Record<string, string | number>): boolean {
return false;
}

// The property values a window's element actually holds at the window's own
// start (position). For fromTo() this is the authored fromVars, not the
// destination toVars captured in propertyValues — a fade-in via
// `fromTo(el, {opacity:0}, {opacity:1}, 0)` genuinely starts hidden even
// though propertyValues (the toVars) says opacity: 1.
function windowStartStateValues(win: GsapWindow): Record<string, string | number> {
if (win.method === "fromTo") return win.fromPropertyValues ?? win.propertyValues;
return win.propertyValues;
}

function makesOverlayVisible(win: GsapWindow): boolean {
if (win.method === "from" && isHiddenGsapState(win.propertyValues)) return true;
return isVisibleGsapState(win.propertyValues);
Expand Down Expand Up @@ -680,7 +695,8 @@ export const gsapRules: LintRule<LintContext>[] = [
.sort((a, b) => a.position - b.position);
const startsHiddenAtZero = visibilityWindows.some(
(win) =>
win.position <= SCENE_BOUNDARY_EPSILON_SECONDS && isHiddenGsapState(win.propertyValues),
win.position <= SCENE_BOUNDARY_EPSILON_SECONDS &&
isHiddenGsapState(windowStartStateValues(win)),
);
if (startsHiddenAtZero) continue;
const firstVisible = visibilityWindows.find((win) => makesOverlayVisible(win));
Expand Down
Loading