mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(preview): rewrite sub-composition asset urls in styles (#174)
## Summary
- rewrite CSS `url(...)` asset paths from sub-compositions before styles are hoisted into bundled/master preview output
- rewrite standalone sub-composition preview HTML so `src`/`href` paths keep resolving correctly under the preview root `<base>`
- add regression tests for bundled CSS asset rewriting and standalone sub-composition preview rewriting
## Root cause
Standalone composition previews reused the project `<head>` with a preview-root `<base href="/api/projects/:id/preview/">`, but the sub-composition body still contained `../...` asset references. Those escaped the preview route and 404ed. Separately, bundled preview already rewrote `<img src="../...">` paths but left hoisted CSS asset references like `@font-face src: url("../font.woff2")` untouched.
## Validation
- `pnpm --filter @hyperframes/core exec vitest run src/compiler/htmlBundler.test.ts src/studio-api/helpers/subComposition.test.ts`
- `pnpm --filter @hyperframes/core exec tsc --noEmit`
- `pnpm --filter @hyperframes/producer exec tsc --noEmit` *(blocked by pre-existing `packages/producer/src/services/deterministicFonts.ts` importing missing generated file `./fontData.generated.js` in the worktree install)*
- browser verification with `agent-browser` against the local preview server using `/Users/miguel07code/dev/test-hyperframes/heygen-promo`
## Browser proof
Verified the fixed preview routes in-browser after seeking to visible frames:
- standalone composition preview
- bundled master preview
This commit is contained in:
@@ -61,6 +61,7 @@
|
||||
"@hyperframes/engine": "workspace:^",
|
||||
"hono": "^4.6.0",
|
||||
"linkedom": "^0.18.12",
|
||||
"postcss": "^8.4.0",
|
||||
"puppeteer": "^24.0.0",
|
||||
"puppeteer-core": "^24.39.1"
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { readFileSync, existsSync, mkdirSync } from "fs";
|
||||
import { join, dirname, resolve } from "path";
|
||||
import { parseHTML } from "linkedom";
|
||||
import postcss from "postcss";
|
||||
import {
|
||||
compileTimingAttrs,
|
||||
injectDurations,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
type ResolvedDuration,
|
||||
type UnresolvedElement,
|
||||
rewriteAssetPaths,
|
||||
rewriteCssAssetUrls,
|
||||
} from "@hyperframes/core";
|
||||
import { extractVideoMetadata, extractAudioMetadata } from "../utils/ffprobe.js";
|
||||
import {
|
||||
@@ -370,36 +372,31 @@ function promoteCssImportsToLinkTags(html: string): string {
|
||||
*/
|
||||
function scopeCssToComposition(css: string, compositionId: string): string {
|
||||
const scope = `[data-composition-id="${compositionId}"]`;
|
||||
// Extract @import rules first — they have no {} block and the selector
|
||||
// regex corrupts them by treating the text after @ as a selector.
|
||||
const importRe = /@import\s+url\([^)]*\)\s*;|@import\s+["'][^"']+["']\s*;/gi;
|
||||
const imports: string[] = [];
|
||||
const cssWithoutImports = css.replace(importRe, (match) => {
|
||||
imports.push(match.trim());
|
||||
return "";
|
||||
const globalAtRules = new Set(["keyframes", "-webkit-keyframes", "font-face"]);
|
||||
const root = postcss.parse(css);
|
||||
|
||||
root.walkRules((rule) => {
|
||||
// Skip rules nested inside @keyframes or @font-face — they're global
|
||||
let node: postcss.Node | undefined = rule.parent;
|
||||
while (node) {
|
||||
if (
|
||||
node.type === "atrule" &&
|
||||
globalAtRules.has((node as postcss.AtRule).name.toLowerCase())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
node = (node as postcss.ChildNode).parent;
|
||||
}
|
||||
|
||||
rule.selectors = rule.selectors.map((sel) => {
|
||||
if (!sel.trim()) return sel;
|
||||
if (/^(html|body|:root|\*)$/i.test(sel.trim())) return sel;
|
||||
if (sel.includes(`data-composition-id="${compositionId}"`)) return sel;
|
||||
return `${scope} ${sel}`;
|
||||
});
|
||||
});
|
||||
// Split on top-level rule boundaries. Simple regex approach:
|
||||
// scope each selector in rule blocks while preserving at-rules.
|
||||
const scoped = cssWithoutImports.replace(/([^{}@]+)\{/g, (match, selectors: string) => {
|
||||
const trimmed = selectors.trim();
|
||||
// Skip @-rule headers (they don't have selectors to scope)
|
||||
if (trimmed.startsWith("@")) return match;
|
||||
// Skip if already scoped to this composition
|
||||
if (trimmed.includes(`data-composition-id="${compositionId}"`)) return match;
|
||||
// Scope each comma-separated selector
|
||||
const scopedSelectors = trimmed
|
||||
.split(",")
|
||||
.map((s: string) => {
|
||||
const sel = s.trim();
|
||||
if (!sel) return sel;
|
||||
// Don't scope :root, html, body, or * alone — they're global
|
||||
if (/^(html|body|:root|\*)$/i.test(sel)) return sel;
|
||||
return `${scope} ${sel}`;
|
||||
})
|
||||
.join(", ");
|
||||
return `${scopedSelectors} {`;
|
||||
});
|
||||
return imports.length > 0 ? imports.join("\n") + "\n\n" + scoped : scoped;
|
||||
|
||||
return root.toResult().css;
|
||||
}
|
||||
|
||||
function coalesceHeadStylesAndBodyScripts(html: string): string {
|
||||
@@ -527,7 +524,7 @@ function inlineSubCompositions(
|
||||
const inferredCompId = innerRoot?.getAttribute("data-composition-id")?.trim() || null;
|
||||
|
||||
for (const styleEl of contentDoc.querySelectorAll("style")) {
|
||||
const css = styleEl.textContent || "";
|
||||
const css = rewriteCssAssetUrls(styleEl.textContent || "", srcPath);
|
||||
const scopeId = compId || inferredCompId;
|
||||
if (scopeId && css.trim()) {
|
||||
// Scope sub-composition styles to their composition ID to prevent
|
||||
@@ -746,6 +743,9 @@ export async function compileForRender(
|
||||
const mainVideos = parseVideoElements(html);
|
||||
const mainAudios = parseAudioElements(html);
|
||||
|
||||
// Keep inlined sub-composition media authoritative on ID collisions.
|
||||
// inlineSubCompositions() hoists those nodes into the final HTML, so the
|
||||
// producer should follow the same precedence the runtime sees in the merged DOM.
|
||||
const videos = dedupeElementsById([...mainVideos, ...subVideos]);
|
||||
const audios = dedupeElementsById([...mainAudios, ...subAudios]);
|
||||
|
||||
@@ -948,6 +948,7 @@ export async function recompileWithResolutions(
|
||||
const mainVideos = parseVideoElements(html);
|
||||
const mainAudios = parseAudioElements(html);
|
||||
|
||||
// Keep inlined sub-composition media authoritative on ID collisions.
|
||||
const videos = dedupeElementsById([...mainVideos, ...subVideos]);
|
||||
const audios = dedupeElementsById([...mainAudios, ...subAudios]);
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "heygen-promo-preview-assets",
|
||||
"description": "Regression fixture using the HeyGen promo composition with sub-composition images and local brand fonts referenced via ../ paths. Guards against broken asset rewriting in compiled output and rendered snapshots.",
|
||||
"tags": ["regression", "sub-composition", "preview-assets", "prod-style", "slow"],
|
||||
"minPsnr": 30,
|
||||
"maxFrameFailures": 0,
|
||||
"minAudioCorrelation": 0,
|
||||
"maxAudioLagWindows": 120,
|
||||
"renderConfig": {
|
||||
"fps": 30
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:18f17bdc8f66b8d695408c5fd19661e63d3e3658cba931a0c69345f6c0173759
|
||||
size 6292940
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+336
@@ -0,0 +1,336 @@
|
||||
<template id="scene1-heygen-hero-template">
|
||||
<div
|
||||
data-composition-id="scene1-heygen-hero"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="5"
|
||||
>
|
||||
<div class="scene">
|
||||
<div class="halo halo-left"></div>
|
||||
<div class="halo halo-right"></div>
|
||||
<div class="mesh"></div>
|
||||
|
||||
<div class="nav-shell">
|
||||
<div class="nav-pill">
|
||||
<img class="nav-logo" src="../heygen-logo.png" alt="HeyGen" />
|
||||
<div class="nav-links">
|
||||
<span>Platform</span>
|
||||
<span>Use cases</span>
|
||||
<span>Pricing</span>
|
||||
<span>Enterprise</span>
|
||||
</div>
|
||||
<div class="nav-action">Sign in</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="copy">
|
||||
<div class="eyebrow">HEYGEN / AI VIDEO GENERATOR</div>
|
||||
<h1>
|
||||
<span class="dark">Create studio-quality</span>
|
||||
<span class="blue">AI videos</span>
|
||||
<span class="dark">in minutes.</span>
|
||||
</h1>
|
||||
<p class="subhead">
|
||||
Turn scripts, avatars, photos, and multilingual dubbing into polished videos with one
|
||||
fast workflow built for teams.
|
||||
</p>
|
||||
<div class="actions">
|
||||
<div class="cta primary">Get Started for Free</div>
|
||||
<div class="cta secondary">heygen.com</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visuals">
|
||||
<div class="browser-frame">
|
||||
<div class="browser-top">
|
||||
<div class="dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="browser-url">www.heygen.com</div>
|
||||
</div>
|
||||
<img class="browser-shot" src="../heygen-homepage.png" alt="HeyGen homepage" />
|
||||
</div>
|
||||
<img class="prism" src="../hero-prism.png" alt="HeyGen avatars" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "TT Norms Pro";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
src: url("../TT_Norms_Pro_Normal.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "TT Norms Pro";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: block;
|
||||
src: url("../TT_Norms_Pro_Medium.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "TT Norms Pro";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: block;
|
||||
src: url("../TT_Norms_Pro_Bold.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "ABC Solar Display";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: block;
|
||||
src: url("../ABCSolarDisplay-Bold.woff2") format("woff2");
|
||||
}
|
||||
|
||||
[data-composition-id="scene1-heygen-hero"] {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #fcfcfd;
|
||||
}
|
||||
|
||||
.scene {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 22% 30%, rgba(0, 195, 255, 0.12), transparent 26%),
|
||||
radial-gradient(circle at 78% 22%, rgba(191, 127, 255, 0.12), transparent 24%),
|
||||
linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
|
||||
font-family: "TT Norms Pro", Arial, sans-serif;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.halo,
|
||||
.mesh {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.halo {
|
||||
filter: blur(38px);
|
||||
}
|
||||
|
||||
.halo-left {
|
||||
background: radial-gradient(circle at 20% 30%, rgba(0, 195, 255, 0.18), transparent 24%);
|
||||
}
|
||||
|
||||
.halo-right {
|
||||
background: radial-gradient(circle at 80% 20%, rgba(191, 127, 255, 0.16), transparent 24%);
|
||||
}
|
||||
|
||||
.mesh {
|
||||
opacity: 0.22;
|
||||
background-image:
|
||||
linear-gradient(rgba(0, 195, 255, 0.08) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(191, 127, 255, 0.06) 1px, transparent 1px);
|
||||
background-size: 96px 96px;
|
||||
mask-image: linear-gradient(180deg, black 20%, rgba(0, 0, 0, 0.25) 100%);
|
||||
}
|
||||
|
||||
.nav-shell,
|
||||
.copy,
|
||||
.visuals {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.nav-shell {
|
||||
left: 42px;
|
||||
top: 24px;
|
||||
width: 1836px;
|
||||
}
|
||||
|
||||
.nav-pill {
|
||||
height: 74px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
border-radius: 28px;
|
||||
background:
|
||||
linear-gradient(#ffffff, #ffffff) padding-box,
|
||||
linear-gradient(90deg, rgba(0, 195, 255, 0.55), rgba(191, 127, 255, 0.45)) border-box;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 10px 30px rgba(51, 51, 51, 0.06);
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
width: 96px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 42px;
|
||||
margin-left: auto;
|
||||
margin-right: 22px;
|
||||
font-size: 25px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-action {
|
||||
padding: 14px 24px;
|
||||
border-radius: 20px;
|
||||
background: #111111;
|
||||
color: #ffffff;
|
||||
font-size: 23px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.copy {
|
||||
left: 86px;
|
||||
top: 182px;
|
||||
width: 820px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
color: rgba(51, 51, 51, 0.56);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 34px;
|
||||
font-size: 108px;
|
||||
line-height: 0.9;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
h1 span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dark {
|
||||
font-family: "ABC Solar Display", "TT Norms Pro", Arial, sans-serif;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.blue {
|
||||
font-family: "ABC Solar Display", "TT Norms Pro", Arial, sans-serif;
|
||||
color: #00c3ff;
|
||||
}
|
||||
|
||||
.subhead {
|
||||
margin-top: 34px;
|
||||
width: 700px;
|
||||
font-size: 34px;
|
||||
line-height: 1.22;
|
||||
color: rgba(51, 51, 51, 0.72);
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 42px;
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.cta {
|
||||
border-radius: 22px;
|
||||
padding: 18px 28px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: #00c3ff;
|
||||
color: #333333;
|
||||
box-shadow: 0 18px 40px rgba(0, 195, 255, 0.28);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: rgba(51, 51, 51, 0.72);
|
||||
border: 1px solid rgba(51, 51, 51, 0.12);
|
||||
}
|
||||
|
||||
.visuals {
|
||||
right: 60px;
|
||||
top: 130px;
|
||||
width: 940px;
|
||||
height: 860px;
|
||||
}
|
||||
|
||||
.browser-frame {
|
||||
position: absolute;
|
||||
left: 22px;
|
||||
top: 188px;
|
||||
width: 680px;
|
||||
height: 456px;
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 28px 80px rgba(35, 43, 62, 0.16);
|
||||
}
|
||||
|
||||
.browser-top {
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 18px;
|
||||
border-bottom: 1px solid rgba(51, 51, 51, 0.08);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.dots {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dots span {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(51, 51, 51, 0.14);
|
||||
}
|
||||
|
||||
.browser-url {
|
||||
font-size: 18px;
|
||||
color: rgba(51, 51, 51, 0.54);
|
||||
}
|
||||
|
||||
.browser-shot {
|
||||
width: 100%;
|
||||
height: calc(100% - 52px);
|
||||
object-fit: cover;
|
||||
object-position: center top;
|
||||
}
|
||||
|
||||
.prism {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 10px;
|
||||
width: 720px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 28px 60px rgba(0, 195, 255, 0.18));
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const root = document.currentScript.parentElement;
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.from(root.querySelector(".nav-pill"), { y: -36, opacity: 0, duration: 0.75, ease: "power3.out" }, 0);
|
||||
tl.from(root.querySelector(".eyebrow"), { y: 20, opacity: 0, duration: 0.5, ease: "power2.out" }, 0.2);
|
||||
tl.from(root.querySelectorAll("h1 span"), { yPercent: 110, opacity: 0, duration: 1, ease: "power4.out", stagger: 0.1 }, 0.28);
|
||||
tl.from(root.querySelector(".subhead"), { y: 28, opacity: 0, duration: 0.8, ease: "power2.out" }, 0.7);
|
||||
tl.from(root.querySelectorAll(".cta"), { y: 22, opacity: 0, scale: 0.96, duration: 0.7, ease: "back.out(1.3)", stagger: 0.08 }, 0.92);
|
||||
tl.from(root.querySelector(".browser-frame"), { x: 140, y: 40, rotate: -5, opacity: 0, duration: 1, ease: "power3.out" }, 0.45);
|
||||
tl.from(root.querySelector(".prism"), { x: 160, y: -40, rotate: 8, opacity: 0, duration: 1.1, ease: "power3.out" }, 0.6);
|
||||
tl.to(root.querySelector(".browser-frame"), { y: -16, duration: 5, ease: "sine.inOut" }, 0);
|
||||
tl.to(root.querySelector(".prism"), { y: 10, rotation: -3, duration: 5, ease: "sine.inOut" }, 0);
|
||||
tl.to(root.querySelectorAll(".halo"), { scale: 1.08, duration: 5, ease: "sine.inOut" }, 0);
|
||||
|
||||
window.__timelines["scene1-heygen-hero"] = tl;
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
<template id="scene2-heygen-showcase-template">
|
||||
<div
|
||||
data-composition-id="scene2-heygen-showcase"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="6"
|
||||
>
|
||||
<div class="scene">
|
||||
<div class="top-pills">
|
||||
<span>Text to Video</span>
|
||||
<span>Photo to Video</span>
|
||||
<span>Product Ads</span>
|
||||
<span>UGC ads</span>
|
||||
<span>AI Models</span>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<div class="eyebrow">ONE PLATFORM / MULTIPLE WAYS TO CREATE</div>
|
||||
<h2>Text, avatars, dubbing, and AI Studio in one flow.</h2>
|
||||
<p>
|
||||
Build product videos, talking photos, translated content, and social-ready promos
|
||||
without leaving the same system.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="stage">
|
||||
<div class="shot large">
|
||||
<img src="../heygen-superpower.png" alt="HeyGen product section" />
|
||||
</div>
|
||||
<div class="shot small">
|
||||
<img src="../feature-strip.png" alt="HeyGen feature strip" />
|
||||
</div>
|
||||
<img class="prism-mini" src="../hero-prism.png" alt="HeyGen prism" />
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="number">117,048,632</div>
|
||||
<div class="label">Videos generated</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="number">90,808,526</div>
|
||||
<div class="label">Avatars generated</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="number">16,125,234</div>
|
||||
<div class="label">Videos translated</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "TT Norms Pro";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
src: url("../TT_Norms_Pro_Normal.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "TT Norms Pro";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: block;
|
||||
src: url("../TT_Norms_Pro_Medium.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "TT Norms Pro";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: block;
|
||||
src: url("../TT_Norms_Pro_Bold.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "ABC Solar Display";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: block;
|
||||
src: url("../ABCSolarDisplay-Bold.woff2") format("woff2");
|
||||
}
|
||||
|
||||
[data-composition-id="scene2-heygen-showcase"] {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f7f9fc 100%);
|
||||
font-family: "TT Norms Pro", Arial, sans-serif;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.scene {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.top-pills,
|
||||
.header,
|
||||
.stage,
|
||||
.stats {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.top-pills {
|
||||
left: 190px;
|
||||
top: 72px;
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.top-pills span {
|
||||
padding: 18px 28px;
|
||||
border-radius: 28px;
|
||||
background: #f1f1f3;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #333333;
|
||||
box-shadow: inset 0 0 0 1px rgba(51, 51, 51, 0.04);
|
||||
}
|
||||
|
||||
.header {
|
||||
left: 96px;
|
||||
top: 208px;
|
||||
width: 660px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-size: 20px;
|
||||
letter-spacing: 0.16em;
|
||||
font-weight: 700;
|
||||
color: rgba(51, 51, 51, 0.54);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 24px;
|
||||
font-family: "ABC Solar Display", "TT Norms Pro", Arial, sans-serif;
|
||||
font-size: 86px;
|
||||
line-height: 0.94;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 28px;
|
||||
font-size: 30px;
|
||||
line-height: 1.24;
|
||||
color: rgba(51, 51, 51, 0.72);
|
||||
}
|
||||
|
||||
.stage {
|
||||
right: 72px;
|
||||
top: 160px;
|
||||
width: 980px;
|
||||
height: 620px;
|
||||
}
|
||||
|
||||
.shot {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
border-radius: 34px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 24px 60px rgba(23, 30, 43, 0.16);
|
||||
}
|
||||
|
||||
.shot img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.large {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 760px;
|
||||
height: 468px;
|
||||
}
|
||||
|
||||
.large img {
|
||||
object-position: center top;
|
||||
}
|
||||
|
||||
.small {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 500px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.small img {
|
||||
object-position: center center;
|
||||
}
|
||||
|
||||
.prism-mini {
|
||||
position: absolute;
|
||||
left: -30px;
|
||||
bottom: -12px;
|
||||
width: 420px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
opacity: 0.92;
|
||||
filter: drop-shadow(0 24px 42px rgba(0, 195, 255, 0.12));
|
||||
}
|
||||
|
||||
.stats {
|
||||
left: 92px;
|
||||
right: 92px;
|
||||
bottom: 74px;
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
border-radius: 28px;
|
||||
background: #171717;
|
||||
color: #ffffff;
|
||||
padding: 26px 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 56px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 10px;
|
||||
font-size: 24px;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const root = document.currentScript.parentElement;
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.from(root.querySelectorAll(".top-pills span"), { y: -26, opacity: 0, duration: 0.7, ease: "power3.out", stagger: 0.06 }, 0);
|
||||
tl.from(root.querySelector(".eyebrow"), { y: 18, opacity: 0, duration: 0.5, ease: "power2.out" }, 0.24);
|
||||
tl.from(root.querySelector("h2"), { y: 38, opacity: 0, duration: 0.8, ease: "power3.out" }, 0.34);
|
||||
tl.from(root.querySelector("p"), { y: 22, opacity: 0, duration: 0.7, ease: "power2.out" }, 0.52);
|
||||
tl.from(root.querySelector(".large"), { x: 120, y: 24, rotate: -4, opacity: 0, duration: 1, ease: "power3.out" }, 0.4);
|
||||
tl.from(root.querySelector(".small"), { x: 140, y: 50, rotate: 5, opacity: 0, duration: 1, ease: "power3.out" }, 0.58);
|
||||
tl.from(root.querySelector(".prism-mini"), { x: -120, y: 40, rotate: -7, opacity: 0, duration: 1, ease: "power3.out" }, 0.66);
|
||||
tl.from(root.querySelectorAll(".stat-card"), { y: 26, opacity: 0, duration: 0.7, ease: "power3.out", stagger: 0.08 }, 1.05);
|
||||
tl.to(root.querySelector(".large"), { y: -16, duration: 6, ease: "sine.inOut" }, 0);
|
||||
tl.to(root.querySelector(".small"), { y: 10, duration: 6, ease: "sine.inOut" }, 0);
|
||||
tl.to(root.querySelector(".prism-mini"), { y: -14, duration: 6, ease: "sine.inOut" }, 0);
|
||||
|
||||
window.__timelines["scene2-heygen-showcase"] = tl;
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
<template id="scene3-heygen-cta-template">
|
||||
<div
|
||||
data-composition-id="scene3-heygen-cta"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="5"
|
||||
>
|
||||
<div class="scene">
|
||||
<div class="bg-orb left"></div>
|
||||
<div class="bg-orb right"></div>
|
||||
<div class="grid"></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="brand-row">
|
||||
<img class="logo" src="../heygen-logo.png" alt="HeyGen" />
|
||||
<div class="tag">Used by 100,000+ teams</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="copy">
|
||||
<div class="eyebrow">HEYGEN / START CREATING</div>
|
||||
<h2>Fast to prompt.<br />Faster to publish.</h2>
|
||||
<p>
|
||||
Create AI videos with avatars, dubbing, and text-first editing in one polished
|
||||
workflow built to move at team speed.
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
<div class="cta primary">Get Started for Free</div>
|
||||
<div class="cta secondary">heygen.com</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stack">
|
||||
<div class="mini browser">
|
||||
<img src="../heygen-homepage.png" alt="HeyGen homepage" />
|
||||
</div>
|
||||
<div class="mini prism-shell">
|
||||
<img src="../hero-prism.png" alt="HeyGen prism" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "TT Norms Pro";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
src: url("../TT_Norms_Pro_Normal.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "TT Norms Pro";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: block;
|
||||
src: url("../TT_Norms_Pro_Medium.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "TT Norms Pro";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: block;
|
||||
src: url("../TT_Norms_Pro_Bold.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "ABC Solar Display";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: block;
|
||||
src: url("../ABCSolarDisplay-Bold.woff2") format("woff2");
|
||||
}
|
||||
|
||||
[data-composition-id="scene3-heygen-cta"] {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #0b1018 0%, #111827 48%, #0a0f16 100%);
|
||||
font-family: "TT Norms Pro", Arial, sans-serif;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.scene {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bg-orb,
|
||||
.grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bg-orb {
|
||||
filter: blur(46px);
|
||||
}
|
||||
|
||||
.left {
|
||||
background: radial-gradient(circle at 16% 24%, rgba(0, 195, 255, 0.26), transparent 26%);
|
||||
}
|
||||
|
||||
.right {
|
||||
background: radial-gradient(circle at 84% 22%, rgba(191, 127, 255, 0.22), transparent 26%);
|
||||
}
|
||||
|
||||
.grid {
|
||||
opacity: 0.14;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.06) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.06) 1px, transparent 1px);
|
||||
background-size: 84px 84px;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: absolute;
|
||||
left: 60px;
|
||||
top: 56px;
|
||||
width: 1800px;
|
||||
height: 968px;
|
||||
padding: 42px 46px;
|
||||
border-radius: 36px;
|
||||
background: linear-gradient(180deg, rgba(17, 24, 39, 0.88), rgba(10, 16, 24, 0.94));
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.brand-row,
|
||||
.content {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.brand-row {
|
||||
left: 46px;
|
||||
right: 46px;
|
||||
top: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 112px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 14px 22px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
font-size: 22px;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.content {
|
||||
left: 46px;
|
||||
right: 46px;
|
||||
top: 144px;
|
||||
bottom: 46px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.copy {
|
||||
width: 920px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-size: 21px;
|
||||
letter-spacing: 0.18em;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.58);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 26px;
|
||||
font-family: "ABC Solar Display", "TT Norms Pro", Arial, sans-serif;
|
||||
font-size: 112px;
|
||||
line-height: 0.9;
|
||||
letter-spacing: -0.06em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 28px;
|
||||
width: 760px;
|
||||
font-size: 32px;
|
||||
line-height: 1.22;
|
||||
color: rgba(255, 255, 255, 0.74);
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 40px;
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.cta {
|
||||
padding: 20px 30px;
|
||||
border-radius: 24px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: #00c3ff;
|
||||
color: #333333;
|
||||
box-shadow: 0 20px 46px rgba(0, 195, 255, 0.28);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: rgba(255, 255, 255, 0.76);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.stack {
|
||||
position: relative;
|
||||
width: 620px;
|
||||
height: 700px;
|
||||
}
|
||||
|
||||
.mini {
|
||||
position: absolute;
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 22px 64px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.browser {
|
||||
right: 0;
|
||||
top: 26px;
|
||||
width: 560px;
|
||||
height: 360px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.browser img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center top;
|
||||
}
|
||||
|
||||
.prism-shell {
|
||||
left: 0;
|
||||
bottom: 20px;
|
||||
width: 500px;
|
||||
height: 340px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.prism-shell img {
|
||||
width: 520px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const root = document.currentScript.parentElement;
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.from(root.querySelector(".card"), { opacity: 0, y: 28, scale: 0.985, duration: 0.8, ease: "power3.out" }, 0);
|
||||
tl.from(root.querySelector(".brand-row"), { y: -20, opacity: 0, duration: 0.55, ease: "power2.out" }, 0.16);
|
||||
tl.from(root.querySelector(".eyebrow"), { y: 16, opacity: 0, duration: 0.45, ease: "power2.out" }, 0.28);
|
||||
tl.from(root.querySelector("h2"), { y: 34, opacity: 0, duration: 0.8, ease: "power3.out" }, 0.38);
|
||||
tl.from(root.querySelector("p"), { y: 22, opacity: 0, duration: 0.7, ease: "power2.out" }, 0.58);
|
||||
tl.from(root.querySelectorAll(".cta"), { y: 24, opacity: 0, scale: 0.96, duration: 0.65, ease: "back.out(1.25)", stagger: 0.08 }, 0.82);
|
||||
tl.from(root.querySelector(".browser"), { x: 120, y: -18, rotate: 4, opacity: 0, duration: 1, ease: "power3.out" }, 0.48);
|
||||
tl.from(root.querySelector(".prism-shell"), { x: 90, y: 42, rotate: -4, opacity: 0, duration: 1, ease: "power3.out" }, 0.64);
|
||||
tl.to(root.querySelector(".browser"), { y: -14, duration: 5, ease: "sine.inOut" }, 0);
|
||||
tl.to(root.querySelector(".prism-shell"), { y: 10, duration: 5, ease: "sine.inOut" }, 0);
|
||||
tl.to(root.querySelectorAll(".bg-orb"), { scale: 1.08, duration: 5, ease: "sine.inOut" }, 0);
|
||||
|
||||
window.__timelines["scene3-heygen-cta"] = tl;
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 599 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 274 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 445 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 265 KiB |
@@ -0,0 +1,70 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>HeyGen Promo</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="16"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div
|
||||
id="scene-1"
|
||||
data-composition-id="scene1-heygen-hero"
|
||||
data-composition-src="compositions/scene1-heygen-hero.html"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-track-index="0"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
<div
|
||||
id="scene-2"
|
||||
data-composition-id="scene2-heygen-showcase"
|
||||
data-composition-src="compositions/scene2-heygen-showcase.html"
|
||||
data-start="5"
|
||||
data-duration="6"
|
||||
data-track-index="0"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
<div
|
||||
id="scene-3"
|
||||
data-composition-id="scene3-heygen-cta"
|
||||
data-composition-src="compositions/scene3-heygen-cta.html"
|
||||
data-start="11"
|
||||
data-duration="5"
|
||||
data-track-index="0"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["main"] = gsap.timeline({ paused: true });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user