Skip to content

Commit 4c358e6

Browse files
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
2 parents 4c2a90a + e0c3b28 commit 4c358e6

7 files changed

Lines changed: 233 additions & 48 deletions

File tree

packages/core/src/lint/rules/gsap.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,4 +746,99 @@ describe("GSAP rules", () => {
746746
const finding = result.findings.find((f) => f.code === "gsap_infinite_repeat");
747747
expect(finding).toBeUndefined();
748748
});
749+
750+
it("errors when CSS opacity:0 + gsap.from({opacity:0}) — invisible forever", () => {
751+
const html = `
752+
<html><body>
753+
<div data-composition-id="c1" data-width="1920" data-height="1080">
754+
<div id="title" style="opacity: 0; font-size: 120px;">Hello</div>
755+
</div>
756+
<script>
757+
window.__timelines = window.__timelines || {};
758+
const tl = gsap.timeline({ paused: true });
759+
tl.from("#title", { opacity: 0, y: 30, duration: 0.5 }, 0.2);
760+
window.__timelines["c1"] = tl;
761+
</script>
762+
</body></html>`;
763+
const result = lintHyperframeHtml(html);
764+
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
765+
expect(finding).toBeDefined();
766+
expect(finding!.severity).toBe("error");
767+
expect(finding!.selector).toBe("#title");
768+
});
769+
770+
it("errors when style block has opacity:0 + gsap.from({opacity:0})", () => {
771+
const html = `
772+
<html><body>
773+
<div data-composition-id="c1" data-width="1920" data-height="1080">
774+
<div id="hero">Hello</div>
775+
</div>
776+
<style>
777+
#hero { font-size: 200px; color: #fff; opacity: 0; }
778+
</style>
779+
<script>
780+
window.__timelines = window.__timelines || {};
781+
const tl = gsap.timeline({ paused: true });
782+
tl.from("#hero", { opacity: 0, scale: 3.5, duration: 0.25, ease: "expo.out" }, 0.1);
783+
window.__timelines["c1"] = tl;
784+
</script>
785+
</body></html>`;
786+
const result = lintHyperframeHtml(html);
787+
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
788+
expect(finding).toBeDefined();
789+
});
790+
791+
it("does NOT error when gsap.from({opacity:0}) and CSS has no opacity:0", () => {
792+
const html = `
793+
<html><body>
794+
<div data-composition-id="c1" data-width="1920" data-height="1080">
795+
<div id="title" style="font-size: 120px; color: #fff;">Hello</div>
796+
</div>
797+
<script>
798+
window.__timelines = window.__timelines || {};
799+
const tl = gsap.timeline({ paused: true });
800+
tl.from("#title", { opacity: 0, y: 30, duration: 0.5 }, 0.2);
801+
window.__timelines["c1"] = tl;
802+
</script>
803+
</body></html>`;
804+
const result = lintHyperframeHtml(html);
805+
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
806+
expect(finding).toBeUndefined();
807+
});
808+
809+
it("does NOT error when gsap.fromTo({opacity:0}, {opacity:1}) — destination overrides CSS", () => {
810+
const html = `
811+
<html><body>
812+
<div data-composition-id="c1" data-width="1920" data-height="1080">
813+
<div id="title" style="opacity: 0; font-size: 120px;">Hello</div>
814+
</div>
815+
<script>
816+
window.__timelines = window.__timelines || {};
817+
const tl = gsap.timeline({ paused: true });
818+
tl.fromTo("#title", { opacity: 0, y: 30 }, { opacity: 1, y: 0, duration: 0.5 }, 0.2);
819+
window.__timelines["c1"] = tl;
820+
</script>
821+
</body></html>`;
822+
const result = lintHyperframeHtml(html);
823+
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
824+
expect(finding).toBeUndefined();
825+
});
826+
827+
it("does NOT error when gsap.to() uses opacity:0 (exit animation)", () => {
828+
const html = `
829+
<html><body>
830+
<div data-composition-id="c1" data-width="1920" data-height="1080">
831+
<div id="title" style="opacity: 0;">Hello</div>
832+
</div>
833+
<script>
834+
window.__timelines = window.__timelines || {};
835+
const tl = gsap.timeline({ paused: true });
836+
tl.to("#title", { opacity: 0, duration: 0.5 }, 4.0);
837+
window.__timelines["c1"] = tl;
838+
</script>
839+
</body></html>`;
840+
const result = lintHyperframeHtml(html);
841+
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
842+
expect(finding).toBeUndefined();
843+
});
749844
});

packages/core/src/lint/rules/gsap.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,4 +843,60 @@ export const gsapRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
843843
}
844844
return findings;
845845
},
846+
847+
// gsap_from_opacity_noop — CSS opacity:0 + gsap.from({opacity:0}) = invisible forever
848+
({ styles, scripts, tags }) => {
849+
const findings: HyperframeLintFinding[] = [];
850+
const cssOpacityZeroSelectors = new Set<string>();
851+
852+
for (const style of styles) {
853+
for (const [, selector, body] of style.content.matchAll(
854+
/([#.][a-zA-Z0-9_-]+)\s*\{([^}]+)\}/g,
855+
)) {
856+
if (body && /opacity\s*:\s*0\s*[;}]/.test(body)) {
857+
cssOpacityZeroSelectors.add((selector ?? "").trim());
858+
}
859+
}
860+
}
861+
862+
for (const tag of tags) {
863+
const inlineStyle = readAttr(tag.raw, "style");
864+
if (!inlineStyle || !/opacity\s*:\s*0/.test(inlineStyle)) continue;
865+
const id = readAttr(tag.raw, "id");
866+
const classes = readAttr(tag.raw, "class")?.split(/\s+/).filter(Boolean) ?? [];
867+
if (id) cssOpacityZeroSelectors.add(`#${id}`);
868+
for (const cls of classes) cssOpacityZeroSelectors.add(`.${cls}`);
869+
}
870+
871+
if (cssOpacityZeroSelectors.size === 0) return findings;
872+
873+
for (const script of scripts) {
874+
if (!/gsap\.timeline/.test(script.content)) continue;
875+
const windows = extractGsapWindows(script.content);
876+
877+
for (const win of windows) {
878+
if (win.method !== "from") continue;
879+
if (!win.properties.includes("opacity")) continue;
880+
const sel = win.targetSelector;
881+
const cssKey = sel.startsWith("#") || sel.startsWith(".") ? sel : `#${sel}`;
882+
if (!cssOpacityZeroSelectors.has(cssKey)) continue;
883+
884+
findings.push({
885+
code: "gsap_from_opacity_noop",
886+
severity: "error",
887+
message:
888+
`"${sel}" has CSS \`opacity: 0\` and a gsap.${win.method}() that also sets opacity to 0. ` +
889+
`gsap.from() animates FROM the specified value TO the current CSS value — ` +
890+
`since CSS is already 0, the element animates from 0→0 and never becomes visible.`,
891+
selector: sel,
892+
fixHint:
893+
`Remove \`opacity: 0\` from the CSS/inline style on "${sel}". ` +
894+
`Let gsap.from({opacity: 0}) handle the initial hidden state — ` +
895+
`it will animate FROM 0 TO the CSS value (1 by default).`,
896+
snippet: truncateSnippet(win.raw),
897+
});
898+
}
899+
}
900+
return findings;
901+
},
846902
];

packages/producer/tests/sub-comp-t0/output/compiled.html

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
font-style: normal;
5555
font-weight: 900;
5656
font-display: block;
57-
}</style>
57+
}</style><style data-hyperframes-text-rendering="true">html,body,*{text-rendering:geometricPrecision}</style>
5858
<meta charset="UTF-8">
5959
<meta name="viewport" content="width=1920, height=1080">
6060
<script>/* inlined: https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js */
@@ -81,14 +81,14 @@
8181
width: 1920px;
8282
height: 1080px;
8383
overflow: hidden;
84-
background: #07110d;
84+
background: #0f172a;
8585
}
8686
#root {
8787
position: relative;
8888
width: 1920px;
8989
height: 1080px;
9090
overflow: hidden;
91-
background: #07110d;
91+
background: #0f172a;
9292
}
9393
.scene {
9494
position: absolute;
@@ -100,56 +100,74 @@
100100
opacity: 1;
101101
}
102102

103-
[data-composition-id="hook"] [data-hf-authored-id="hook"] {
103+
[data-composition-id="hook"][data-hf-authored-id="hook"] {
104104
position: relative;
105105
width: 1920px;
106106
height: 1080px;
107107
overflow: hidden;
108-
background: #07110d;
109-
display: flex;
110-
align-items: center;
111-
justify-content: center;
112-
flex-direction: column;
113-
gap: 20px;
108+
background: #ef4444;
114109
}
115-
[data-composition-id="hook"] [data-hf-authored-id="hook"] .hook-title {
110+
[data-composition-id="hook"][data-hf-authored-id="hook"] .hook-bg {
111+
position: absolute;
112+
inset: 0;
113+
background: #ef4444;
114+
}
115+
[data-composition-id="hook"][data-hf-authored-id="hook"] .hook-title {
116+
position: absolute;
117+
top: 250px;
118+
left: 0;
119+
width: 1920px;
120+
text-align: center;
116121
font-family: Impact, system-ui, sans-serif;
117-
font-size: 200px;
122+
font-size: 280px;
118123
font-weight: 900;
119-
color: #ff3b30;
124+
color: #ffffff;
120125
opacity: 0;
121126
}
122-
[data-composition-id="hook"] [data-hf-authored-id="hook"] .hook-subtitle {
127+
[data-composition-id="hook"][data-hf-authored-id="hook"] .hook-subtitle {
128+
position: absolute;
129+
top: 600px;
130+
left: 0;
131+
width: 1920px;
132+
text-align: center;
123133
font-family: Arial, system-ui, sans-serif;
124-
font-size: 60px;
125-
color: #f7f3e8;
134+
font-size: 80px;
135+
color: #ffffff;
126136
opacity: 0;
127137
}
128138

129139

130140

131-
[data-composition-id="later"] [data-hf-authored-id="later"] {
141+
[data-composition-id="later"][data-hf-authored-id="later"] {
132142
position: relative;
133143
width: 1920px;
134144
height: 1080px;
135145
overflow: hidden;
136-
background: #0e2c1e;
137-
display: flex;
138-
align-items: center;
139-
justify-content: center;
146+
background: #3b82f6;
147+
}
148+
[data-composition-id="later"][data-hf-authored-id="later"] .later-bg {
149+
position: absolute;
150+
inset: 0;
151+
background: #3b82f6;
140152
}
141-
[data-composition-id="later"] [data-hf-authored-id="later"] .later-heading {
153+
[data-composition-id="later"][data-hf-authored-id="later"] .later-heading {
154+
position: absolute;
155+
top: 380px;
156+
left: 0;
157+
width: 1920px;
158+
text-align: center;
142159
font-family: Arial, system-ui, sans-serif;
143-
font-size: 120px;
160+
font-size: 160px;
144161
font-weight: 700;
145-
color: #f7f3e8;
162+
color: #ffffff;
146163
opacity: 0;
147164
}</style>
148165
</head>
149166
<body>
150167
<div id="root" data-composition-id="main" data-start="0" data-duration="6" data-width="1920" data-height="1080">
151168
<!-- First sub-comp at near-zero start: the bug trigger -->
152169
<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">
170+
<div class="hook-bg"></div>
153171
<div class="hook-title">BREAKING</div>
154172
<div class="hook-subtitle">Transfer confirmed</div>
155173

@@ -161,6 +179,7 @@
161179

162180
<!-- Second sub-comp at a later start: works correctly -->
163181
<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">
182+
<div class="later-bg"></div>
164183
<div class="later-heading">Second Scene</div>
165184

166185

@@ -462,15 +481,13 @@
462481
var tl = gsap.timeline({ paused: true });
463482
var scope = "#hook";
464483

465-
// Title slams in at 0.1s
466484
tl.fromTo(
467485
scope + " .hook-title",
468486
{ scale: 3.5, opacity: 0 },
469487
{ scale: 1, opacity: 1, duration: 0.25, ease: "expo.out" },
470488
0.1,
471489
);
472490

473-
// Subtitle fades in at 0.5s
474491
tl.fromTo(
475492
scope + " .hook-subtitle",
476493
{ y: 30, opacity: 0 },
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:3ac5c0ef3b7e6e31a49d361a62ba2ca8ac1532544eca9f6f60e0ede52836693c
3-
size 20587
2+
oid sha256:7acd4e38f70f528105dabd4eb9717e2ab369755817e42141c98401ae5ed6009c
3+
size 426899

packages/producer/tests/sub-comp-t0/src/compositions/hook.html

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<template id="hook-template">
22
<div id="hook" data-composition-id="hook" data-width="1920" data-height="1080">
3+
<div class="hook-bg"></div>
34
<div class="hook-title">BREAKING</div>
45
<div class="hook-subtitle">Transfer confirmed</div>
56

@@ -9,24 +10,34 @@
910
width: 1920px;
1011
height: 1080px;
1112
overflow: hidden;
12-
background: #07110d;
13-
display: flex;
14-
align-items: center;
15-
justify-content: center;
16-
flex-direction: column;
17-
gap: 20px;
13+
background: #ef4444;
14+
}
15+
#hook .hook-bg {
16+
position: absolute;
17+
inset: 0;
18+
background: #ef4444;
1819
}
1920
#hook .hook-title {
21+
position: absolute;
22+
top: 250px;
23+
left: 0;
24+
width: 1920px;
25+
text-align: center;
2026
font-family: Impact, system-ui, sans-serif;
21-
font-size: 200px;
27+
font-size: 280px;
2228
font-weight: 900;
23-
color: #ff3b30;
29+
color: #ffffff;
2430
opacity: 0;
2531
}
2632
#hook .hook-subtitle {
33+
position: absolute;
34+
top: 600px;
35+
left: 0;
36+
width: 1920px;
37+
text-align: center;
2738
font-family: Arial, system-ui, sans-serif;
28-
font-size: 60px;
29-
color: #f7f3e8;
39+
font-size: 80px;
40+
color: #ffffff;
3041
opacity: 0;
3142
}
3243
</style>
@@ -38,15 +49,13 @@
3849
var tl = gsap.timeline({ paused: true });
3950
var scope = "#hook";
4051

41-
// Title slams in at 0.1s
4252
tl.fromTo(
4353
scope + " .hook-title",
4454
{ scale: 3.5, opacity: 0 },
4555
{ scale: 1, opacity: 1, duration: 0.25, ease: "expo.out" },
4656
0.1,
4757
);
4858

49-
// Subtitle fades in at 0.5s
5059
tl.fromTo(
5160
scope + " .hook-subtitle",
5261
{ y: 30, opacity: 0 },

0 commit comments

Comments
 (0)