Merge pull request #1001 from heygen-com/fix/sub-comp-t0-baseline-regen

test(producer): regenerate sub-comp-t0 baseline + add gsap_from_opacity_noop lint rule
This commit is contained in:
Miguel Ángel
2026-05-21 21:42:37 +02:00
committed by GitHub
7 changed files with 233 additions and 48 deletions
+95
View File
@@ -746,4 +746,99 @@ describe("GSAP rules", () => {
const finding = result.findings.find((f) => f.code === "gsap_infinite_repeat"); const finding = result.findings.find((f) => f.code === "gsap_infinite_repeat");
expect(finding).toBeUndefined(); expect(finding).toBeUndefined();
}); });
it("errors when CSS opacity:0 + gsap.from({opacity:0}) — invisible forever", () => {
const html = `
<html><body>
<div data-composition-id="c1" data-width="1920" data-height="1080">
<div id="title" style="opacity: 0; font-size: 120px;">Hello</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.from("#title", { opacity: 0, y: 30, duration: 0.5 }, 0.2);
window.__timelines["c1"] = tl;
</script>
</body></html>`;
const result = lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
expect(finding).toBeDefined();
expect(finding!.severity).toBe("error");
expect(finding!.selector).toBe("#title");
});
it("errors when style block has opacity:0 + gsap.from({opacity:0})", () => {
const html = `
<html><body>
<div data-composition-id="c1" data-width="1920" data-height="1080">
<div id="hero">Hello</div>
</div>
<style>
#hero { font-size: 200px; color: #fff; opacity: 0; }
</style>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.from("#hero", { opacity: 0, scale: 3.5, duration: 0.25, ease: "expo.out" }, 0.1);
window.__timelines["c1"] = tl;
</script>
</body></html>`;
const result = lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
expect(finding).toBeDefined();
});
it("does NOT error when gsap.from({opacity:0}) and CSS has no opacity:0", () => {
const html = `
<html><body>
<div data-composition-id="c1" data-width="1920" data-height="1080">
<div id="title" style="font-size: 120px; color: #fff;">Hello</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.from("#title", { opacity: 0, y: 30, duration: 0.5 }, 0.2);
window.__timelines["c1"] = tl;
</script>
</body></html>`;
const result = lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
expect(finding).toBeUndefined();
});
it("does NOT error when gsap.fromTo({opacity:0}, {opacity:1}) — destination overrides CSS", () => {
const html = `
<html><body>
<div data-composition-id="c1" data-width="1920" data-height="1080">
<div id="title" style="opacity: 0; font-size: 120px;">Hello</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.fromTo("#title", { opacity: 0, y: 30 }, { opacity: 1, y: 0, duration: 0.5 }, 0.2);
window.__timelines["c1"] = tl;
</script>
</body></html>`;
const result = lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
expect(finding).toBeUndefined();
});
it("does NOT error when gsap.to() uses opacity:0 (exit animation)", () => {
const html = `
<html><body>
<div data-composition-id="c1" data-width="1920" data-height="1080">
<div id="title" style="opacity: 0;">Hello</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.to("#title", { opacity: 0, duration: 0.5 }, 4.0);
window.__timelines["c1"] = tl;
</script>
</body></html>`;
const result = lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
expect(finding).toBeUndefined();
});
}); });
+56
View File
@@ -843,4 +843,60 @@ export const gsapRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
} }
return findings; return findings;
}, },
// gsap_from_opacity_noop — CSS opacity:0 + gsap.from({opacity:0}) = invisible forever
({ styles, scripts, tags }) => {
const findings: HyperframeLintFinding[] = [];
const cssOpacityZeroSelectors = new Set<string>();
for (const style of styles) {
for (const [, selector, body] of style.content.matchAll(
/([#.][a-zA-Z0-9_-]+)\s*\{([^}]+)\}/g,
)) {
if (body && /opacity\s*:\s*0\s*[;}]/.test(body)) {
cssOpacityZeroSelectors.add((selector ?? "").trim());
}
}
}
for (const tag of tags) {
const inlineStyle = readAttr(tag.raw, "style");
if (!inlineStyle || !/opacity\s*:\s*0/.test(inlineStyle)) continue;
const id = readAttr(tag.raw, "id");
const classes = readAttr(tag.raw, "class")?.split(/\s+/).filter(Boolean) ?? [];
if (id) cssOpacityZeroSelectors.add(`#${id}`);
for (const cls of classes) cssOpacityZeroSelectors.add(`.${cls}`);
}
if (cssOpacityZeroSelectors.size === 0) return findings;
for (const script of scripts) {
if (!/gsap\.timeline/.test(script.content)) continue;
const windows = extractGsapWindows(script.content);
for (const win of windows) {
if (win.method !== "from") continue;
if (!win.properties.includes("opacity")) continue;
const sel = win.targetSelector;
const cssKey = sel.startsWith("#") || sel.startsWith(".") ? sel : `#${sel}`;
if (!cssOpacityZeroSelectors.has(cssKey)) continue;
findings.push({
code: "gsap_from_opacity_noop",
severity: "error",
message:
`"${sel}" has CSS \`opacity: 0\` and a gsap.${win.method}() that also sets opacity to 0. ` +
`gsap.from() animates FROM the specified value TO the current CSS value — ` +
`since CSS is already 0, the element animates from 0→0 and never becomes visible.`,
selector: sel,
fixHint:
`Remove \`opacity: 0\` from the CSS/inline style on "${sel}". ` +
`Let gsap.from({opacity: 0}) handle the initial hidden state — ` +
`it will animate FROM 0 TO the CSS value (1 by default).`,
snippet: truncateSnippet(win.raw),
});
}
}
return findings;
},
]; ];
@@ -54,7 +54,7 @@
font-style: normal; font-style: normal;
font-weight: 900; font-weight: 900;
font-display: block; font-display: block;
}</style> }</style><style data-hyperframes-text-rendering="true">html,body,*{text-rendering:geometricPrecision}</style>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=1920, height=1080"> <meta name="viewport" content="width=1920, height=1080">
<script>/* inlined: https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js */ <script>/* inlined: https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js */
@@ -81,14 +81,14 @@
width: 1920px; width: 1920px;
height: 1080px; height: 1080px;
overflow: hidden; overflow: hidden;
background: #07110d; background: #0f172a;
} }
#root { #root {
position: relative; position: relative;
width: 1920px; width: 1920px;
height: 1080px; height: 1080px;
overflow: hidden; overflow: hidden;
background: #07110d; background: #0f172a;
} }
.scene { .scene {
position: absolute; position: absolute;
@@ -100,49 +100,66 @@
opacity: 1; opacity: 1;
} }
[data-composition-id="hook"] [data-hf-authored-id="hook"] { [data-composition-id="hook"][data-hf-authored-id="hook"] {
position: relative; position: relative;
width: 1920px; width: 1920px;
height: 1080px; height: 1080px;
overflow: hidden; overflow: hidden;
background: #07110d; background: #ef4444;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 20px;
} }
[data-composition-id="hook"] [data-hf-authored-id="hook"] .hook-title { [data-composition-id="hook"][data-hf-authored-id="hook"] .hook-bg {
position: absolute;
inset: 0;
background: #ef4444;
}
[data-composition-id="hook"][data-hf-authored-id="hook"] .hook-title {
position: absolute;
top: 250px;
left: 0;
width: 1920px;
text-align: center;
font-family: Impact, system-ui, sans-serif; font-family: Impact, system-ui, sans-serif;
font-size: 200px; font-size: 280px;
font-weight: 900; font-weight: 900;
color: #ff3b30; color: #ffffff;
opacity: 0; opacity: 0;
} }
[data-composition-id="hook"] [data-hf-authored-id="hook"] .hook-subtitle { [data-composition-id="hook"][data-hf-authored-id="hook"] .hook-subtitle {
position: absolute;
top: 600px;
left: 0;
width: 1920px;
text-align: center;
font-family: Arial, system-ui, sans-serif; font-family: Arial, system-ui, sans-serif;
font-size: 60px; font-size: 80px;
color: #f7f3e8; color: #ffffff;
opacity: 0; opacity: 0;
} }
[data-composition-id="later"] [data-hf-authored-id="later"] { [data-composition-id="later"][data-hf-authored-id="later"] {
position: relative; position: relative;
width: 1920px; width: 1920px;
height: 1080px; height: 1080px;
overflow: hidden; overflow: hidden;
background: #0e2c1e; background: #3b82f6;
display: flex;
align-items: center;
justify-content: center;
} }
[data-composition-id="later"] [data-hf-authored-id="later"] .later-heading { [data-composition-id="later"][data-hf-authored-id="later"] .later-bg {
position: absolute;
inset: 0;
background: #3b82f6;
}
[data-composition-id="later"][data-hf-authored-id="later"] .later-heading {
position: absolute;
top: 380px;
left: 0;
width: 1920px;
text-align: center;
font-family: Arial, system-ui, sans-serif; font-family: Arial, system-ui, sans-serif;
font-size: 120px; font-size: 160px;
font-weight: 700; font-weight: 700;
color: #f7f3e8; color: #ffffff;
opacity: 0; opacity: 0;
}</style> }</style>
</head> </head>
@@ -150,6 +167,7 @@
<div id="root" data-composition-id="main" data-start="0" data-duration="6" data-width="1920" data-height="1080"> <div id="root" data-composition-id="main" data-start="0" data-duration="6" data-width="1920" data-height="1080">
<!-- First sub-comp at near-zero start: the bug trigger --> <!-- First sub-comp at near-zero start: the bug trigger -->
<div style="width:1920px;height:1080px" data-composition-file="compositions/hook.html" data-hf-authored-id="hook" data-height="1080" data-width="1920" id="scene-hook" class="scene" data-layout-allow-overflow="" data-composition-id="hook" data-start="0.001" data-duration="2.0" data-track-index="0"> <div style="width:1920px;height:1080px" data-composition-file="compositions/hook.html" data-hf-authored-id="hook" data-height="1080" data-width="1920" id="scene-hook" class="scene" data-layout-allow-overflow="" data-composition-id="hook" data-start="0.001" data-duration="2.0" data-track-index="0">
<div class="hook-bg"></div>
<div class="hook-title">BREAKING</div> <div class="hook-title">BREAKING</div>
<div class="hook-subtitle">Transfer confirmed</div> <div class="hook-subtitle">Transfer confirmed</div>
@@ -161,6 +179,7 @@
<!-- Second sub-comp at a later start: works correctly --> <!-- Second sub-comp at a later start: works correctly -->
<div style="width:1920px;height:1080px" data-composition-file="compositions/later.html" data-hf-authored-id="later" data-height="1080" data-width="1920" id="scene-later" class="scene" data-layout-allow-overflow="" data-composition-id="later" data-start="2.5" data-duration="3.5" data-track-index="1"> <div style="width:1920px;height:1080px" data-composition-file="compositions/later.html" data-hf-authored-id="later" data-height="1080" data-width="1920" id="scene-later" class="scene" data-layout-allow-overflow="" data-composition-id="later" data-start="2.5" data-duration="3.5" data-track-index="1">
<div class="later-bg"></div>
<div class="later-heading">Second Scene</div> <div class="later-heading">Second Scene</div>
@@ -462,7 +481,6 @@
var tl = gsap.timeline({ paused: true }); var tl = gsap.timeline({ paused: true });
var scope = "#hook"; var scope = "#hook";
// Title slams in at 0.1s
tl.fromTo( tl.fromTo(
scope + " .hook-title", scope + " .hook-title",
{ scale: 3.5, opacity: 0 }, { scale: 3.5, opacity: 0 },
@@ -470,7 +488,6 @@
0.1, 0.1,
); );
// Subtitle fades in at 0.5s
tl.fromTo( tl.fromTo(
scope + " .hook-subtitle", scope + " .hook-subtitle",
{ y: 30, opacity: 0 }, { y: 30, opacity: 0 },
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3ac5c0ef3b7e6e31a49d361a62ba2ca8ac1532544eca9f6f60e0ede52836693c oid sha256:7acd4e38f70f528105dabd4eb9717e2ab369755817e42141c98401ae5ed6009c
size 20587 size 426899
@@ -1,5 +1,6 @@
<template id="hook-template"> <template id="hook-template">
<div id="hook" data-composition-id="hook" data-width="1920" data-height="1080"> <div id="hook" data-composition-id="hook" data-width="1920" data-height="1080">
<div class="hook-bg"></div>
<div class="hook-title">BREAKING</div> <div class="hook-title">BREAKING</div>
<div class="hook-subtitle">Transfer confirmed</div> <div class="hook-subtitle">Transfer confirmed</div>
@@ -9,24 +10,34 @@
width: 1920px; width: 1920px;
height: 1080px; height: 1080px;
overflow: hidden; overflow: hidden;
background: #07110d; background: #ef4444;
display: flex; }
align-items: center; #hook .hook-bg {
justify-content: center; position: absolute;
flex-direction: column; inset: 0;
gap: 20px; background: #ef4444;
} }
#hook .hook-title { #hook .hook-title {
position: absolute;
top: 250px;
left: 0;
width: 1920px;
text-align: center;
font-family: Impact, system-ui, sans-serif; font-family: Impact, system-ui, sans-serif;
font-size: 200px; font-size: 280px;
font-weight: 900; font-weight: 900;
color: #ff3b30; color: #ffffff;
opacity: 0; opacity: 0;
} }
#hook .hook-subtitle { #hook .hook-subtitle {
position: absolute;
top: 600px;
left: 0;
width: 1920px;
text-align: center;
font-family: Arial, system-ui, sans-serif; font-family: Arial, system-ui, sans-serif;
font-size: 60px; font-size: 80px;
color: #f7f3e8; color: #ffffff;
opacity: 0; opacity: 0;
} }
</style> </style>
@@ -38,7 +49,6 @@
var tl = gsap.timeline({ paused: true }); var tl = gsap.timeline({ paused: true });
var scope = "#hook"; var scope = "#hook";
// Title slams in at 0.1s
tl.fromTo( tl.fromTo(
scope + " .hook-title", scope + " .hook-title",
{ scale: 3.5, opacity: 0 }, { scale: 3.5, opacity: 0 },
@@ -46,7 +56,6 @@
0.1, 0.1,
); );
// Subtitle fades in at 0.5s
tl.fromTo( tl.fromTo(
scope + " .hook-subtitle", scope + " .hook-subtitle",
{ y: 30, opacity: 0 }, { y: 30, opacity: 0 },
@@ -1,5 +1,6 @@
<template id="later-template"> <template id="later-template">
<div id="later" data-composition-id="later" data-width="1920" data-height="1080"> <div id="later" data-composition-id="later" data-width="1920" data-height="1080">
<div class="later-bg"></div>
<div class="later-heading">Second Scene</div> <div class="later-heading">Second Scene</div>
<style> <style>
@@ -8,16 +9,23 @@
width: 1920px; width: 1920px;
height: 1080px; height: 1080px;
overflow: hidden; overflow: hidden;
background: #0e2c1e; background: #3b82f6;
display: flex; }
align-items: center; #later .later-bg {
justify-content: center; position: absolute;
inset: 0;
background: #3b82f6;
} }
#later .later-heading { #later .later-heading {
position: absolute;
top: 380px;
left: 0;
width: 1920px;
text-align: center;
font-family: Arial, system-ui, sans-serif; font-family: Arial, system-ui, sans-serif;
font-size: 120px; font-size: 160px;
font-weight: 700; font-weight: 700;
color: #f7f3e8; color: #ffffff;
opacity: 0; opacity: 0;
} }
</style> </style>
@@ -15,14 +15,14 @@
width: 1920px; width: 1920px;
height: 1080px; height: 1080px;
overflow: hidden; overflow: hidden;
background: #07110d; background: #0f172a;
} }
#root { #root {
position: relative; position: relative;
width: 1920px; width: 1920px;
height: 1080px; height: 1080px;
overflow: hidden; overflow: hidden;
background: #07110d; background: #0f172a;
} }
.scene { .scene {
position: absolute; position: absolute;