diff --git a/packages/lint/src/rules/gsap.test.ts b/packages/lint/src/rules/gsap.test.ts
index 2715ff5a9c..ad991aac9c 100644
--- a/packages/lint/src/rules/gsap.test.ts
+++ b/packages/lint/src/rules/gsap.test.ts
@@ -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 = `
+
+
+
+
+
+`;
+ 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 = `
+
+
+
+
+
+
+`;
+ 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 = `
diff --git a/packages/lint/src/rules/gsap.ts b/packages/lint/src/rules/gsap.ts
index b54b7c8732..f5600892a9 100644
--- a/packages/lint/src/rules/gsap.ts
+++ b/packages/lint/src/rules/gsap.ts
@@ -42,6 +42,10 @@ type GsapWindow = {
end: number;
properties: string[];
propertyValues: Record;
+ // 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;
overwriteAuto: boolean;
method: string;
raw: string;
@@ -141,6 +145,7 @@ async function extractGsapWindows(script: string): Promise {
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),
@@ -209,6 +214,16 @@ function isVisibleGsapState(values: Record): 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 {
+ 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);
@@ -680,7 +695,8 @@ export const gsapRules: LintRule[] = [
.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));