mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
Merge pull request #2521 from heygen-com/fix/stale-compositor-layer
fix(renderer): prevent stale SwiftShader layers
This commit is contained in:
@@ -28,6 +28,25 @@ describe("buildChromeArgs browser GPU mode", () => {
|
||||
expect(args).not.toContain("--enable-gpu-rasterization");
|
||||
});
|
||||
|
||||
it("disables GPU compositing only for software BeginFrame capture", () => {
|
||||
const softwareBeginFrame = buildChromeArgs(
|
||||
{ ...base, captureMode: "beginframe" },
|
||||
{ browserGpuMode: "software" },
|
||||
);
|
||||
const softwareScreenshot = buildChromeArgs(
|
||||
{ ...base, captureMode: "screenshot" },
|
||||
{ browserGpuMode: "software" },
|
||||
);
|
||||
const hardwareBeginFrame = buildChromeArgs(
|
||||
{ ...base, captureMode: "beginframe", platform: "linux" },
|
||||
{ browserGpuMode: "hardware" },
|
||||
);
|
||||
|
||||
expect(softwareBeginFrame).toContain("--disable-gpu-compositing");
|
||||
expect(softwareScreenshot).not.toContain("--disable-gpu-compositing");
|
||||
expect(hardwareBeginFrame).not.toContain("--disable-gpu-compositing");
|
||||
});
|
||||
|
||||
it("uses Metal-backed ANGLE for hardware browser GPU mode on macOS", () => {
|
||||
const args = buildChromeArgs({ ...base, platform: "darwin" }, { browserGpuMode: "hardware" });
|
||||
expect(args).toContain("--enable-unsafe-webgpu");
|
||||
|
||||
@@ -686,6 +686,18 @@ export function buildChromeArgs(
|
||||
|
||||
// BeginFrame flags — only when using chrome-headless-shell on Linux
|
||||
if (options.captureMode !== "screenshot") {
|
||||
// SwiftShader's GPU compositor can retain a transformed layer for several
|
||||
// sequential frames after a GSAP yoyo/reversal. The DOM and timeline are
|
||||
// already at the requested time, but both BeginFrame and
|
||||
// Page.captureScreenshot read the stale surface (the duplicate is present
|
||||
// in the raw JPEG before encoding). Keep deterministic BeginFrame capture,
|
||||
// but route compositing through Chrome's software path when the browser is
|
||||
// already in software-GPU mode. Hardware-GPU and screenshot captures keep
|
||||
// their existing compositor paths. Remove this workaround once the pinned
|
||||
// chrome-headless-shell includes https://issues.chromium.org/issues/535256667.
|
||||
if (browserGpuMode === "software") {
|
||||
chromeArgs.push("--disable-gpu-compositing");
|
||||
}
|
||||
chromeArgs.push(
|
||||
"--deterministic-mode",
|
||||
"--enable-begin-frame-control",
|
||||
|
||||
@@ -121,6 +121,12 @@ type TestMetadata = {
|
||||
* guard; omit for the default screenshot/BeginFrame capture.
|
||||
*/
|
||||
experimentalFastCapture?: boolean;
|
||||
/**
|
||||
* Pin the browser capture path for a regression fixture. The producer's
|
||||
* software-GPU default normally prefers screenshots, so BeginFrame-only
|
||||
* compositor regressions must opt out explicitly to exercise that path.
|
||||
*/
|
||||
captureMode?: "screenshot" | "beginframe";
|
||||
/**
|
||||
* Render-time variable overrides, equivalent to `hyperframes render
|
||||
* --variables '<json>'`. Injected as `window.__hfVariables` before any
|
||||
@@ -390,6 +396,15 @@ function validateMetadata(meta: unknown): TestMetadata {
|
||||
"meta.json: 'renderConfig.experimentalFastCapture' must be a boolean (or omit for false)",
|
||||
);
|
||||
}
|
||||
if (
|
||||
rc.captureMode !== undefined &&
|
||||
rc.captureMode !== "screenshot" &&
|
||||
rc.captureMode !== "beginframe"
|
||||
) {
|
||||
throw new Error(
|
||||
"meta.json: 'renderConfig.captureMode' must be 'screenshot' or 'beginframe' (or omitted)",
|
||||
);
|
||||
}
|
||||
if (
|
||||
rc.variables !== undefined &&
|
||||
(rc.variables === null || typeof rc.variables !== "object" || Array.isArray(rc.variables))
|
||||
@@ -1037,7 +1052,12 @@ async function runTestSuite(
|
||||
// var, scoped to this suite's render so it never leaks to other suites.
|
||||
const useFast = suite.meta.renderConfig.experimentalFastCapture === true;
|
||||
const prevFast = process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE;
|
||||
const captureMode = suite.meta.renderConfig.captureMode;
|
||||
const prevForceScreenshot = process.env.PRODUCER_FORCE_SCREENSHOT;
|
||||
if (useFast) process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE = "true";
|
||||
if (captureMode) {
|
||||
process.env.PRODUCER_FORCE_SCREENSHOT = captureMode === "screenshot" ? "true" : "false";
|
||||
}
|
||||
try {
|
||||
const job = createRenderJob({
|
||||
fps: suite.meta.renderConfig.fps,
|
||||
@@ -1056,6 +1076,10 @@ async function runTestSuite(
|
||||
if (prevFast === undefined) delete process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE;
|
||||
else process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE = prevFast;
|
||||
}
|
||||
if (captureMode) {
|
||||
if (prevForceScreenshot === undefined) delete process.env.PRODUCER_FORCE_SCREENSHOT;
|
||||
else process.env.PRODUCER_FORCE_SCREENSHOT = prevForceScreenshot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "software-beginframe-yoyo-compositor",
|
||||
"description": "Regression guard for Chromium issue 535256667: stale SwiftShader compositor layers after a GSAP yoyo reversal. At 25 fps the unfixed BeginFrame path briefly retained the first output badge at its previous transform, painting a third ghost badge around 10.12 seconds.",
|
||||
"tags": ["regression", "beginframe", "gsap"],
|
||||
"minPsnr": 30,
|
||||
"maxFrameFailures": 0,
|
||||
"minAudioCorrelation": 0,
|
||||
"maxAudioLagWindows": 1,
|
||||
"renderConfig": {
|
||||
"fps": 25,
|
||||
"workers": 1,
|
||||
"captureMode": "beginframe"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9763ae3659454f2baa08f403562e334f4b17b1610a54df20203d21b58543fc26
|
||||
size 437140
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:326ced7888acace3f38c020eb7c56a6164940b94d335603153d615f4b66dc408
|
||||
size 1843718
|
||||
@@ -0,0 +1,410 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Layer Reversal Regression</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&family=Space+Grotesk:wght@300;500;700&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/MotionPathPlugin.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-color: #0b0e14;
|
||||
--purple: #8b3a7a;
|
||||
--purple-light: #a56698;
|
||||
--white: #ffffff;
|
||||
--text-secondary: #a0a4b0;
|
||||
--grid-color: rgba(139, 58, 122, 0.1);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--white);
|
||||
font-family: 'Outfit', sans-serif;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Background Texture */
|
||||
.bg-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background-image:
|
||||
radial-gradient(circle at 50% 50%, rgba(139, 58, 122, 0.15) 0%, transparent 70%),
|
||||
linear-gradient(var(--grid-color) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
|
||||
background-size: 100% 100%, 80px 80px, 80px 80px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* AI Engine Core */
|
||||
.engine-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.engine-core {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
background: radial-gradient(circle, var(--purple) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.engine-ring {
|
||||
position: absolute;
|
||||
inset: -20px;
|
||||
border: 2px solid var(--purple);
|
||||
border-radius: 50%;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.engine-inner {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: var(--white);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 15px rgba(139, 58, 122, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.engine-label {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
color: var(--white);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Input Icons (Left) */
|
||||
.inputs-container {
|
||||
position: absolute;
|
||||
left: 150px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 60px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.input-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 15px 25px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.icon-box {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--purple-light);
|
||||
}
|
||||
|
||||
.input-text {
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
/* Output Badges (Right) */
|
||||
.outputs-container {
|
||||
position: absolute;
|
||||
right: 150px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 80px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.output-badge {
|
||||
background: var(--purple);
|
||||
color: var(--white);
|
||||
padding: 30px 50px;
|
||||
border-radius: 100px;
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 36px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 350px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* SVG Connectors Overlay */
|
||||
#connectors-svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.trail-path {
|
||||
fill: none;
|
||||
stroke: var(--purple);
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 10, 10;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* Header/Footer Info */
|
||||
.header-info {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 42px;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 20px;
|
||||
color: var(--purple-light);
|
||||
letter-spacing: 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Caption Safe Zone Marker (Invisible) */
|
||||
.caption-spacer {
|
||||
position: absolute;
|
||||
bottom: 10%;
|
||||
width: 76%;
|
||||
height: 10%;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" data-composition-id="root" data-width="1920" data-height="1080" data-start="0" data-duration="10.6">
|
||||
<div class="bg-layer"></div>
|
||||
|
||||
<div class="header-info">
|
||||
<div class="brand-name" id="brand">LAYER TEST</div>
|
||||
<div class="tagline" id="tagline">COMPOSITOR REVERSAL</div>
|
||||
</div>
|
||||
|
||||
<div class="inputs-container">
|
||||
<div class="input-item" id="input-social">
|
||||
<div class="icon-box">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg>
|
||||
</div>
|
||||
<div class="input-text">Input Alpha</div>
|
||||
</div>
|
||||
<div class="input-item" id="input-ecommerce">
|
||||
<div class="icon-box">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="21" r="1"></circle><circle cx="20" cy="21" r="1"></circle><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path></svg>
|
||||
</div>
|
||||
<div class="input-text">Input Beta</div>
|
||||
</div>
|
||||
<div class="input-item" id="input-reviews">
|
||||
<div class="icon-box">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
|
||||
</div>
|
||||
<div class="input-text">Input Gamma</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="engine-container">
|
||||
<div class="engine-core" id="engine-core">
|
||||
<div class="engine-ring" id="ring-1"></div>
|
||||
<div class="engine-ring" id="ring-2"></div>
|
||||
<div class="engine-inner">
|
||||
<svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="#8b3a7a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="engine-label" id="engine-label">PROCESSOR</div>
|
||||
</div>
|
||||
|
||||
<div class="outputs-container">
|
||||
<div class="output-badge" id="output-persona">First Output</div>
|
||||
<div class="output-badge" id="output-formats">Second Output</div>
|
||||
</div>
|
||||
|
||||
<svg id="connectors-svg" data-layout-allow-overflow></svg>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
gsap.registerPlugin(MotionPathPlugin);
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["root"] = tl;
|
||||
|
||||
const D = 20;
|
||||
|
||||
// Elements
|
||||
const engine = document.querySelector('#engine-core');
|
||||
const engineLabel = document.querySelector('#engine-label');
|
||||
const inputs = document.querySelectorAll('.input-item');
|
||||
const outputs = document.querySelectorAll('.output-badge');
|
||||
const rings = document.querySelectorAll('.engine-ring');
|
||||
const brand = document.querySelector('#brand');
|
||||
const tagline = document.querySelector('#tagline');
|
||||
const svg = document.querySelector('#connectors-svg');
|
||||
|
||||
// Initial States
|
||||
gsap.set([engine, engineLabel, inputs, outputs, brand, tagline], { opacity: 0 });
|
||||
gsap.set(engine, { scale: 0.5 });
|
||||
gsap.set(inputs, { x: -50 });
|
||||
gsap.set(outputs, { x: 50 });
|
||||
|
||||
// Build Animation
|
||||
|
||||
// 1. Background & Engine Entrance
|
||||
tl.to('.bg-layer', { opacity: 1, duration: 2, ease: "power2.inOut" }, 0);
|
||||
tl.fromTo(engine, { opacity: 0, scale: 0.5 }, { opacity: 1, scale: 1, duration: 1.5, ease: "power3.out" }, 0.5);
|
||||
tl.fromTo(engineLabel, { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 1, ease: "power2.out" }, 1.2);
|
||||
|
||||
// 2. Header Info
|
||||
tl.fromTo([brand, tagline], { opacity: 0, y: -20 }, { opacity: 1, y: 0, stagger: 0.2, duration: 1, ease: "power2.out" }, 1);
|
||||
|
||||
// 3. Inputs Entrance (Left)
|
||||
tl.fromTo(inputs, { opacity: 0, x: -50 }, { opacity: 1, x: 0, stagger: 0.3, duration: 1, ease: "power2.out" }, 2);
|
||||
|
||||
// 4. Draw Connectors & Particle Trails
|
||||
const engineRect = engine.getBoundingClientRect();
|
||||
const engineX = engineRect.left + engineRect.width / 2;
|
||||
const engineY = engineRect.top + engineRect.height / 2;
|
||||
|
||||
inputs.forEach((input, i) => {
|
||||
const rect = input.getBoundingClientRect();
|
||||
const startX = rect.right;
|
||||
const startY = rect.top + rect.height / 2;
|
||||
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
path.setAttribute("class", "trail-path");
|
||||
const cp1x = startX + (engineX - startX) * 0.5;
|
||||
const d = `M ${startX} ${startY} Q ${cp1x} ${startY} ${engineX} ${engineY}`;
|
||||
path.setAttribute("d", d);
|
||||
svg.appendChild(path);
|
||||
|
||||
const length = path.getTotalLength();
|
||||
gsap.set(path, { strokeDasharray: length, strokeDashoffset: length });
|
||||
|
||||
tl.to(path, { strokeDashoffset: 0, duration: 1.5, ease: "power1.inOut" }, 2.5 + (i * 0.3));
|
||||
|
||||
// Particle trail effect
|
||||
for(let j=0; j<5; j++) {
|
||||
const particle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
||||
particle.setAttribute("r", "3");
|
||||
particle.setAttribute("fill", "#ffffff");
|
||||
svg.appendChild(particle);
|
||||
|
||||
tl.to(particle, {
|
||||
duration: 1.5,
|
||||
repeat: 5,
|
||||
motionPath: {
|
||||
path: path,
|
||||
align: path,
|
||||
autoRotate: true
|
||||
},
|
||||
ease: "none",
|
||||
opacity: 0
|
||||
}, 2.5 + (i * 0.3) + (j * 0.2));
|
||||
}
|
||||
});
|
||||
|
||||
outputs.forEach((output, i) => {
|
||||
const rect = output.getBoundingClientRect();
|
||||
const endX = rect.left;
|
||||
const endY = rect.top + rect.height / 2;
|
||||
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
path.setAttribute("class", "trail-path");
|
||||
const cp1x = engineX + (endX - engineX) * 0.5;
|
||||
const d = `M ${engineX} ${engineY} Q ${cp1x} ${endY} ${endX} ${endY}`;
|
||||
path.setAttribute("d", d);
|
||||
svg.appendChild(path);
|
||||
|
||||
const length = path.getTotalLength();
|
||||
gsap.set(path, { strokeDasharray: length, strokeDashoffset: length });
|
||||
|
||||
tl.to(path, { strokeDashoffset: 0, duration: 1.5, ease: "power1.inOut" }, 5 + (i * 0.5));
|
||||
});
|
||||
|
||||
// 5. Outputs Entrance (Right)
|
||||
tl.fromTo(outputs, { opacity: 0, x: 50 }, { opacity: 1, x: 0, stagger: 0.5, duration: 1.2, ease: "power3.out" }, 5.5);
|
||||
|
||||
// 6. Ambient Motion (Continuous)
|
||||
tl.to('#ring-1', { rotation: 360, duration: 10, repeat: 1, ease: "none" }, 0);
|
||||
tl.to('#ring-2', { rotation: -360, duration: 15, repeat: 1, ease: "none" }, 0);
|
||||
|
||||
tl.to(engine, {
|
||||
scale: 1.05,
|
||||
duration: 2,
|
||||
repeat: 8,
|
||||
yoyo: true,
|
||||
ease: "sine.inOut"
|
||||
}, 2.0);
|
||||
|
||||
// Subtle drift for inputs and outputs
|
||||
tl.to(inputs, { y: "+=10", duration: 3, repeat: 5, yoyo: true, ease: "sine.inOut", stagger: 0.2 }, 3);
|
||||
tl.to(outputs, { y: "-=15", duration: 4, repeat: 4, yoyo: true, ease: "sine.inOut", stagger: 0.4 }, 6);
|
||||
|
||||
// 7. Exit Animations
|
||||
const exitStart = D - 2;
|
||||
tl.to([inputs, outputs, engine, engineLabel, brand, tagline, svg], {
|
||||
opacity: 0,
|
||||
y: -30,
|
||||
stagger: 0.1,
|
||||
duration: 1,
|
||||
ease: "power2.in"
|
||||
}, exitStart);
|
||||
|
||||
// Pin the end
|
||||
tl.set({}, {}, D);
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user