Copy Code<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Free AI Voice SaaS Hero Template with Sonar-Pulse Loader</title>
<meta name="description" content="Free GSAP AI SaaS landing page hero from ToolsWaves" />
<meta name="generator" content="ToolsWaves - https://toolswaves.in/landing-pages" />
<!-- Open Graph -->
<meta property="og:title" content="Free AI Voice SaaS Hero Template with Sonar-Pulse Loader" />
<meta property="og:description" content="Free GSAP AI SaaS landing page hero from ToolsWaves" />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://toolswaves.in/og?title=Free%20AI%20Voice%20SaaS%20Hero%20Template%20with%20Sonar-Pulse%20Loader&category=Landing%20Page&icon=%F0%9F%93%84" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Free AI Voice SaaS Hero Template with Sonar-Pulse Loader" />
<meta name="twitter:description" content="Free GSAP AI SaaS landing page hero from ToolsWaves" />
<meta name="twitter:image" content="https://toolswaves.in/og?title=Free%20AI%20Voice%20SaaS%20Hero%20Template%20with%20Sonar-Pulse%20Loader&category=Landing%20Page&icon=%F0%9F%93%84" />
<!-- Bootstrap (used by template โ replace with your own framework if you prefer) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #0a0418;
--bg-2: #050210;
--ink: #ffffff;
--muted: rgba(255, 255, 255, 0.6);
--line: rgba(255, 255, 255, 0.1);
--purple: #8b2fe6;
--purple-deep: #6c1dc4;
--purple-soft: #c084fc;
--lavender: #e9d5ff;
--glass-pink: #ec4cff;
--chip-bg: rgba(139, 47, 230, 0.08);
--chip-border: rgba(139, 47, 230, 0.25);
}
html, body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg);
color: var(--ink);
overflow: hidden;
cursor: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@media (hover: none), (pointer: coarse) { html, body { cursor: auto; } }
.back-link {
position: fixed;
bottom: 0.75rem;
left: 0.75rem;
z-index: 1000;
color: var(--ink);
text-decoration: none;
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.12em;
padding: 0.45rem 0.9rem;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 999px;
backdrop-filter: blur(8px);
cursor: none;
opacity: 0.85;
transition: opacity 0.3s, transform 0.3s;
}
.back-link:hover { opacity: 1; transform: translateY(-2px); }
/* =========================================================
CUSTOM CURSOR
========================================================= */
.cursor-dot, .cursor-ring {
position: fixed;
top: 0; left: 0;
pointer-events: none;
z-index: 10000;
transform: translate(-50%, -50%);
will-change: transform;
}
.cursor-dot {
width: 6px; height: 6px;
background: var(--purple-soft);
border-radius: 50%;
}
.cursor-ring {
width: 34px; height: 34px;
border: 1px solid rgba(192, 132, 252, 0.5);
border-radius: 50%;
transition: width 0.3s, height 0.3s, border-color 0.3s, background 0.3s;
}
.cursor-ring.is-hover {
width: 60px; height: 60px;
border-color: var(--purple-soft);
background: rgba(192, 132, 252, 0.08);
}
@media (hover: none), (pointer: coarse) { .cursor-dot, .cursor-ring { display: none; } }
/* =========================================================
LOADER โ Pulsing orb + voice waves
========================================================= */
.loader {
position: fixed;
inset: 0;
z-index: 999;
background: radial-gradient(ellipse at center, var(--bg) 0%, var(--bg-2) 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3rem;
}
.loader__stage {
position: relative;
width: 260px;
height: 260px;
display: flex;
align-items: center;
justify-content: center;
}
.loader__rings {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.loader__rings span {
position: absolute;
width: 80px; height: 80px;
border-radius: 50%;
border: 2px solid rgba(192, 132, 252, 0.5);
will-change: transform, opacity;
}
.loader__orb {
position: relative;
width: 90px; height: 90px;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, var(--purple-soft), var(--purple-deep) 70%);
box-shadow: 0 0 40px rgba(139, 47, 230, 0.6),
inset 0 0 20px rgba(233, 213, 255, 0.3);
z-index: 2;
will-change: transform;
display: flex;
align-items: center;
justify-content: center;
}
.loader__orb-inner {
position: absolute;
inset: 15%;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, rgba(255, 255, 255, 0.4), transparent 60%);
}
.loader__orb-core {
position: absolute;
inset: 35%;
border-radius: 50%;
background: rgba(255, 255, 255, 0.6);
filter: blur(4px);
will-change: transform;
}
.loader__check {
position: absolute;
width: 40px;
height: 40px;
z-index: 3;
opacity: 0;
}
.loader__bars {
position: absolute;
bottom: -30px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 3px;
align-items: center;
height: 40px;
}
.loader__bars span {
display: block;
width: 3px;
min-height: 4px;
background: var(--purple-soft);
border-radius: 2px;
will-change: transform, height;
box-shadow: 0 0 6px rgba(192, 132, 252, 0.5);
}
.loader__meta {
display: flex;
gap: 1.25rem;
align-items: center;
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
letter-spacing: 0.3em;
color: var(--muted);
text-transform: uppercase;
}
.loader__counter {
color: var(--purple-soft);
font-weight: 600;
font-variant-numeric: tabular-nums;
min-width: 3ch;
}
/* =========================================================
VOXR SECTION
========================================================= */
.voxr {
position: relative;
width: 100%;
height: 100vh;
min-height: 780px;
overflow: hidden;
background:
radial-gradient(ellipse at 75% 30%, rgba(139, 47, 230, 0.18) 0%, transparent 50%),
radial-gradient(ellipse at 30% 70%, rgba(139, 47, 230, 0.08) 0%, transparent 60%),
linear-gradient(180deg, var(--bg) 0%, var(--bg-2) 100%);
}
.voxr__glow {
position: absolute;
top: 50%; left: 50%;
width: 500px; height: 500px;
transform: translate(-50%, -50%);
background: radial-gradient(circle, rgba(139, 47, 230, 0.2) 0%, transparent 70%);
filter: blur(40px);
pointer-events: none;
z-index: 1;
will-change: transform;
}
/* ---------- Top nav ---------- */
.topnav {
position: relative;
z-index: 30;
display: flex;
align-items: center;
gap: 2rem;
padding: 1.5rem 2.5rem;
}
.topnav__logo {
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
color: var(--ink);
font-weight: 700;
font-size: 1.4rem;
letter-spacing: 0.02em;
cursor: none;
will-change: transform;
}
.topnav__logo-mark { width: 32px; height: 32px; }
.topnav__logo-mark svg { width: 100%; height: 100%; display: block; filter: drop-shadow(0 0 8px rgba(139, 47, 230, 0.5)); }
.topnav__logo-text em {
font-style: normal;
background: linear-gradient(135deg, var(--purple-soft), var(--purple));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.topnav__links {
margin: 0 auto;
display: flex;
gap: 2.5rem;
}
.topnav__links a {
position: relative;
color: var(--ink);
text-decoration: none;
font-weight: 500;
font-size: 1rem;
padding: 0.3rem 0;
cursor: none;
will-change: transform;
}
.topnav__links a::after {
content: '';
position: absolute;
left: 0; bottom: 0;
width: 100%; height: 1.5px;
background: var(--purple-soft);
transform: scaleX(0);
transform-origin: right;
transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
border-radius: 2px;
}
.topnav__links a:hover::after { transform: scaleX(1); transform-origin: left; }
.topnav__actions {
display: flex;
align-items: center;
gap: 0;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 999px;
padding: 0.3rem;
}
.topnav__contact {
color: var(--ink);
text-decoration: none;
padding: 0.7rem 1.4rem;
font-weight: 500;
font-size: 0.95rem;
cursor: none;
will-change: transform;
border-radius: 999px;
transition: background 0.3s;
}
.topnav__contact:hover { background: rgba(255, 255, 255, 0.06); }
.topnav__login {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: #fff;
color: var(--bg);
text-decoration: none;
padding: 0.5rem 0.5rem 0.5rem 1.25rem;
border-radius: 999px;
font-weight: 600;
font-size: 0.95rem;
cursor: none;
will-change: transform;
box-shadow: 0 0 30px rgba(139, 47, 230, 0.5);
position: relative;
}
.topnav__login-arrow {
width: 30px; height: 30px;
background: var(--purple);
color: #fff;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
transition: transform 0.3s;
}
.topnav__login-arrow svg { width: 14px; height: 14px; }
.topnav__login:hover .topnav__login-arrow { transform: rotate(-45deg); }
/* ---------- Hero ---------- */
.hero {
position: relative;
z-index: 10;
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 3rem;
padding: 2rem 3rem 3rem;
align-items: flex-start;
min-height: calc(100vh - 100px);
}
.hero__left { max-width: 720px; padding-top: 2rem; }
.hero__title {
font-weight: 700;
font-size: clamp(2.5rem, 5.5vw, 5rem);
line-height: 1.1;
letter-spacing: -0.02em;
color: var(--ink);
margin-bottom: 2rem;
}
.hero__line {
display: block;
overflow: hidden;
padding-bottom: 0.04em;
}
.hero__line em {
font-style: normal;
background: linear-gradient(135deg, var(--lavender) 0%, #c9a7e9 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero__char {
display: inline-block;
will-change: transform;
}
.hero__char.is-space { width: 0.25em; }
.hero__desc {
font-size: 1rem;
line-height: 1.6;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 2.5rem;
max-width: 560px;
}
.cta-big {
position: relative;
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: #fff;
color: var(--bg);
text-decoration: none;
padding: 0.7rem 0.7rem 0.7rem 1.75rem;
border-radius: 999px;
font-weight: 600;
font-size: 1rem;
cursor: none;
will-change: transform;
box-shadow: 0 0 50px rgba(139, 47, 230, 0.6),
0 0 20px rgba(139, 47, 230, 0.4);
}
.cta-big__arrow {
width: 38px; height: 38px;
background: var(--purple);
color: #fff;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
transition: transform 0.3s;
}
.cta-big__arrow svg { width: 16px; height: 16px; }
.cta-big:hover .cta-big__arrow { transform: rotate(-45deg); }
/* ---------- Chips (right column) ---------- */
.hero__chips {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
padding-top: 2rem;
}
.chip {
display: inline-flex;
align-items: center;
gap: 0.6rem;
padding: 0.8rem 1.25rem;
background: var(--chip-bg);
border: 1px solid var(--chip-border);
border-radius: 999px;
backdrop-filter: blur(10px);
font-size: 0.95rem;
color: var(--ink);
cursor: none;
will-change: transform;
transition: border-color 0.3s, background 0.3s;
}
.chip__icon {
width: 18px; height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.chip__icon svg { width: 100%; height: 100%; }
.chip:hover {
border-color: var(--purple-soft);
background: rgba(139, 47, 230, 0.15);
}
.chip:nth-child(1) { margin-left: 0; }
.chip:nth-child(2) { margin-left: 12%; }
.chip:nth-child(3) { margin-left: 4%; }
/* ---------- 3D Scene ---------- */
.scene {
position: absolute;
left: 45%;
right: 0;
bottom: 0;
height: 45%;
z-index: 5;
pointer-events: none;
perspective: 1000px;
overflow: hidden;
}
.scene__platform {
position: absolute;
left: 5%;
right: -10%;
bottom: 0;
height: 50%;
background: radial-gradient(ellipse at center top, rgba(236, 76, 255, 0.3) 0%, rgba(139, 47, 230, 0.15) 50%, transparent 100%);
border-radius: 100% 100% 0 0 / 60% 60% 0 0;
filter: blur(2px);
will-change: transform;
}
.scene__light {
position: absolute;
left: 30%;
right: 20%;
bottom: 10%;
height: 20%;
background: linear-gradient(90deg, transparent, rgba(236, 76, 255, 0.6), transparent);
filter: blur(8px);
border-radius: 50%;
will-change: transform;
}
.scene__check {
position: absolute;
left: 18%;
bottom: 35%;
width: 120px;
height: 140px;
transform-style: preserve-3d;
will-change: transform;
z-index: 3;
}
.scene__check-face {
width: 100%; height: 100%;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.05) 100%);
border: 1.5px solid rgba(236, 76, 255, 0.4);
border-radius: 20px 20px 40px 40px / 20px 20px 50px 50px;
backdrop-filter: blur(20px);
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 20px 40px rgba(139, 47, 230, 0.3),
inset 0 2px 6px rgba(255, 255, 255, 0.2),
0 0 30px rgba(236, 76, 255, 0.2);
}
.scene__check-face svg {
width: 48px;
height: 48px;
filter: drop-shadow(0 2px 6px rgba(236, 76, 255, 0.6));
}
.scene__orb {
position: absolute;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.4), rgba(233, 213, 255, 0.1) 50%, rgba(139, 47, 230, 0.05) 100%);
backdrop-filter: blur(6px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 10px 30px rgba(139, 47, 230, 0.25),
inset 0 3px 10px rgba(255, 255, 255, 0.2);
will-change: transform;
}
.scene__orb--1 { width: 70px; height: 70px; bottom: 58%; left: 42%; }
.scene__orb--2 { width: 60px; height: 60px; bottom: 48%; left: 60%; }
.scene__orb--3 { width: 50px; height: 50px; bottom: 65%; left: 78%; }
.scene__orb--4 { width: 44px; height: 44px; bottom: 40%; left: 88%; }
.scene__orb--5 { width: 38px; height: 38px; bottom: 70%; left: 92%; }
/* ---------- Initial hidden states ---------- */
[data-magnetic], [data-link], [data-fade], [data-title-line], [data-chip], [data-cta], [data-scene] {
opacity: 0;
}
/* ---------- Responsive ---------- */
@media (max-width: 1100px) {
.topnav__links { display: none; }
.hero { grid-template-columns: 1fr; gap: 2rem; padding: 2rem 1.5rem; }
.hero__chips { flex-direction: row; flex-wrap: wrap; padding-top: 0; }
.chip { margin-left: 0 !important; font-size: 0.8rem; padding: 0.6rem 1rem; }
.scene { left: 50%; height: 40%; }
.scene__check { width: 80px; height: 95px; }
}
@media (max-width: 760px) {
.topnav { padding: 1rem 1.25rem; gap: 0.5rem; }
.topnav__logo-text { font-size: 1.1rem; }
.topnav__contact { display: none; }
.hero { padding: 1rem 1.25rem; gap: 1.5rem; }
.hero__title { font-size: clamp(1.75rem, 9vw, 3rem); }
.hero__desc { font-size: 0.9rem; }
.cta-big { font-size: 0.9rem; padding: 0.55rem 0.55rem 0.55rem 1.4rem; }
.cta-big__arrow { width: 32px; height: 32px; }
.scene { height: 30%; left: 40%; }
.scene__check { display: none; }
.scene__orb { transform: scale(0.7); }
}
@media (max-width: 480px) {
.hero__chips { display: none; }
}
</style>
</head>
<body>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Banner โ Voxr AI</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=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="banner-voxr-ai.css">
</head>
<body>
<!-- Custom cursor -->
<div class="cursor-dot" id="cursor-dot"></div>
<div class="cursor-ring" id="cursor-ring"></div>
<!-- ========== LOADER โ Pulsing AI orb ========== -->
<div class="loader" id="loader">
<div class="loader__stage">
<div class="loader__rings" id="loader-rings"></div>
<div class="loader__orb" id="loader-orb">
<div class="loader__orb-inner"></div>
<div class="loader__orb-core"></div>
<svg class="loader__check" id="loader-check" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3">
<path d="M5 12l5 5 9-11" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="loader__bars" id="loader-bars"></div>
</div>
<div class="loader__meta">
<span class="loader__label" id="loader-label">INITIALIZING AI</span>
<span class="loader__counter" id="loader-counter">0%</span>
</div>
</div>
<section class="voxr" id="voxr">
<!-- Ambient radial glow that follows cursor -->
<div class="voxr__glow" id="glow"></div>
<!-- ========== TOP NAV ========== -->
<header class="topnav">
<a href="#" class="topnav__logo" data-magnetic>
<span class="topnav__logo-mark" id="logo-mark">
<svg viewBox="0 0 40 40" aria-hidden="true">
<path d="M6 6 L 20 30 L 34 6" stroke="url(#logoGrad)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<circle cx="32" cy="10" r="2.5" fill="#c084fc"/>
<defs>
<linearGradient id="logoGrad" x1="0" x2="1" y1="0" y2="1">
<stop offset="0%" stop-color="#e9d5ff"/>
<stop offset="100%" stop-color="#8b2fe6"/>
</linearGradient>
</defs>
</svg>
</span>
<span class="topnav__logo-text">VOXR <em>AI</em></span>
</a>
<nav class="topnav__links">
<a href="#" data-magnetic data-link>Features</a>
<a href="#" data-magnetic data-link>Pricing</a>
<a href="#" data-magnetic data-link>About Us</a>
</nav>
<div class="topnav__actions">
<a href="#" class="topnav__contact" data-magnetic>Contact us</a>
<a href="#" class="topnav__login" data-magnetic>
<span>Login</span>
<span class="topnav__login-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M5 12h14M13 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
</a>
</div>
</header>
<!-- ========== HERO ========== -->
<div class="hero" id="hero">
<!-- Left column -->
<div class="hero__left">
<h1 class="hero__title" aria-label="Stop Chasing Leads. Start Closing Them.">
<span class="hero__line" data-title-line>Stop Chasing</span>
<span class="hero__line" data-title-line><em>Leads.</em> Start</span>
<span class="hero__line" data-title-line><em>Closing Them.</em></span>
</h1>
<p class="hero__desc" data-fade>
AI-powered outbound calling that works leads at scale, qualifies
intent in real-time, and either transfers hot prospects to your
closers instantly or books meetings directly on their calendar.
</p>
<a href="#" class="cta-big" data-magnetic data-cta>
<span class="cta-big__label">Try it now</span>
<span class="cta-big__arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M5 12h14M13 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
</a>
</div>
<!-- Right column: feature chips -->
<div class="hero__chips">
<span class="chip" data-chip>
<span class="chip__icon">
<svg viewBox="0 0 24 24" fill="#c084fc"><path d="M12 2l2.4 7.6L22 12l-7.6 2.4L12 22l-2.4-7.6L2 12l7.6-2.4z"/></svg>
</span>
<span>Your smart scheduling assistant</span>
</span>
<span class="chip" data-chip>
<span class="chip__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="#c084fc" stroke-width="3">
<path d="M4 12l5 5 11-13" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
<span>Qualified appointment only</span>
</span>
<span class="chip" data-chip>
<span class="chip__icon">
<svg viewBox="0 0 24 24" fill="#c084fc">
<path d="M5 13l3-8 5 3 2-5 4 7-3 6-5-2-2 5z"/>
</svg>
</span>
<span>Increases efficiency by 80%</span>
</span>
</div>
</div>
<!-- ========== 3D SCENE (bottom) ========== -->
<div class="scene" id="scene">
<div class="scene__platform" data-scene></div>
<div class="scene__light" data-scene></div>
<div class="scene__check" data-scene>
<div class="scene__check-face">
<svg viewBox="0 0 48 48" fill="none" stroke="#ec4cff" stroke-width="4">
<path d="M10 24l10 10 18-22" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</div>
<div class="scene__orb scene__orb--1" data-scene></div>
<div class="scene__orb scene__orb--2" data-scene></div>
<div class="scene__orb scene__orb--3" data-scene></div>
<div class="scene__orb scene__orb--4" data-scene></div>
<div class="scene__orb scene__orb--5" data-scene></div>
</div>
</section>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="banner-voxr-ai.js"></script>
</body>
</html>
</body>
</html>JS banner-voxr-ai.js โ GSAP animation timeline
(() => {
const root = document.getElementById('voxr');
if (!root) return;
// -------------------------------------------------------------------
// SPLIT TITLE (wrap em/text nodes into char spans, preserving gradient)
// -------------------------------------------------------------------
const titleLines = root.querySelectorAll('[data-title-line]');
titleLines.forEach((line) => {
// Walk children and wrap text nodes' characters without losing em styling
const frag = document.createDocumentFragment();
line.childNodes.forEach((node) => {
if (node.nodeType === Node.TEXT_NODE) {
[...node.textContent].forEach((ch) => {
const span = document.createElement('span');
span.className = 'hero__char';
if (ch === ' ') { span.classList.add('is-space'); span.innerHTML = ' '; }
else span.textContent = ch;
frag.appendChild(span);
});
} else if (node.nodeType === Node.ELEMENT_NODE) {
// em element โ wrap its chars too but keep the em wrapper
const emText = node.textContent;
node.textContent = '';
[...emText].forEach((ch) => {
const span = document.createElement('span');
span.className = 'hero__char';
if (ch === ' ') { span.classList.add('is-space'); span.innerHTML = ' '; }
else span.textContent = ch;
node.appendChild(span);
});
frag.appendChild(node);
}
});
line.textContent = '';
line.appendChild(frag);
});
// -------------------------------------------------------------------
// BUILD LOADER RINGS + BARS
// -------------------------------------------------------------------
const ringsHost = document.getElementById('loader-rings');
const RING_COUNT = 4;
for (let i = 0; i < RING_COUNT; i++) ringsHost.appendChild(document.createElement('span'));
const ringEls = ringsHost.querySelectorAll('span');
const barsHost = document.getElementById('loader-bars');
const BAR_COUNT = 16;
for (let i = 0; i < BAR_COUNT; i++) barsHost.appendChild(document.createElement('span'));
const barEls = barsHost.querySelectorAll('span');
// -------------------------------------------------------------------
// REFERENCES
// -------------------------------------------------------------------
const loader = document.getElementById('loader');
const loaderOrb = document.getElementById('loader-orb');
const loaderCheck = document.getElementById('loader-check');
const loaderCounter = document.getElementById('loader-counter');
const loaderLabel = document.getElementById('loader-label');
const cursorDot = document.getElementById('cursor-dot');
const cursorRing = document.getElementById('cursor-ring');
const glow = document.getElementById('glow');
const magnetics = root.querySelectorAll('[data-magnetic]');
const titleChars = root.querySelectorAll('.hero__char');
const lines = root.querySelectorAll('[data-title-line]');
const fades = root.querySelectorAll('[data-fade]');
const chips = root.querySelectorAll('[data-chip]');
const cta = root.querySelector('[data-cta]');
const sceneEls = root.querySelectorAll('[data-scene]');
const logoMark = document.getElementById('logo-mark');
// -------------------------------------------------------------------
// INITIAL STATES
// -------------------------------------------------------------------
gsap.set(magnetics, { y: -15, opacity: 0 });
gsap.set(titleChars, { yPercent: 110, opacity: 0 });
gsap.set(lines, { opacity: 1 });
gsap.set(fades, { y: 20, opacity: 0 });
gsap.set(chips, { x: 40, opacity: 0 });
gsap.set(cta, { y: 30, opacity: 0, scale: 0.9 });
gsap.set(sceneEls, { opacity: 0 });
gsap.set(ringEls, { scale: 0.6, opacity: 0 });
// -------------------------------------------------------------------
// LOADER TIMELINE
// -------------------------------------------------------------------
const loaderTl = gsap.timeline({ onComplete: playScene });
// Rings emanate continuously
const ringTweens = [];
ringEls.forEach((r, i) => {
const t = gsap.fromTo(r,
{ scale: 0.8, opacity: 0.7 },
{
scale: 2.8,
opacity: 0,
duration: 2,
repeat: -1,
ease: 'power1.out',
delay: i * 0.5,
}
);
ringTweens.push(t);
});
// Orb pulses
gsap.to(loaderOrb, {
scale: 1.1,
duration: 0.8,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
// Voice bars animate heights
barEls.forEach((bar, i) => {
gsap.to(bar, {
height: () => gsap.utils.random(8, 38),
duration: () => gsap.utils.random(0.25, 0.5),
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
delay: i * 0.04,
});
});
// Counter + label rotation
const p = { v: 0 };
loaderTl.to(p, {
v: 100,
duration: 2.4,
ease: 'power1.inOut',
onUpdate: () => {
loaderCounter.textContent = Math.floor(p.v) + '%';
if (p.v > 25 && loaderLabel.textContent === 'INITIALIZING AI') loaderLabel.textContent = 'CONNECTING VOICE';
if (p.v > 55 && loaderLabel.textContent === 'CONNECTING VOICE') loaderLabel.textContent = 'TRAINING MODEL';
if (p.v > 90 && loaderLabel.textContent === 'TRAINING MODEL') loaderLabel.textContent = 'READY';
},
});
// Checkmark appears at end
loaderTl.to(loaderCheck, {
opacity: 1,
duration: 0.3,
ease: 'back.out(2)',
}, '+=0.1');
loaderTl.from(loaderCheck, {
scale: 0,
rotation: -45,
duration: 0.5,
ease: 'back.out(2.5)',
}, '<');
// Exit: orb expands, rings burst out, loader fades
loaderTl.to(ringEls, {
scale: 6,
opacity: 0,
duration: 0.8,
ease: 'power2.out',
onStart: () => ringTweens.forEach(t => t.pause()),
}, '+=0.3');
loaderTl.to(loaderOrb, {
scale: 2.5,
opacity: 0,
duration: 0.7,
ease: 'power3.in',
}, '-=0.5');
loaderTl.to([loaderCounter, loaderLabel, barsHost], {
y: 10, opacity: 0, duration: 0.3, stagger: 0.04,
}, '-=0.6');
loaderTl.to(loader, {
opacity: 0, duration: 0.4, ease: 'power2.inOut',
}, '-=0.2');
loaderTl.set(loader, { display: 'none' });
// -------------------------------------------------------------------
// MAIN SCENE ENTRANCE
// -------------------------------------------------------------------
function playScene() {
const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
// Nav
tl.to(magnetics, {
y: 0, opacity: 1,
duration: 0.6,
stagger: 0.05,
}, 0);
// Title chars
tl.to(titleChars, {
yPercent: 0, opacity: 1,
duration: 1.1,
stagger: 0.018,
ease: 'expo.out',
}, 0.3);
// Desc + CTA
tl.to(fades, {
y: 0, opacity: 1,
duration: 0.7,
stagger: 0.15,
}, 0.9);
tl.to(cta, {
y: 0, opacity: 1, scale: 1,
duration: 0.8,
ease: 'back.out(1.6)',
}, 1.1);
// Chips stagger in from right
tl.to(chips, {
x: 0, opacity: 1,
duration: 0.7,
stagger: 0.1,
ease: 'power4.out',
}, 0.8);
// 3D scene fades in
tl.to(sceneEls, {
opacity: 1,
duration: 1.2,
stagger: 0.08,
ease: 'power2.out',
}, 1);
tl.call(startContinuous, null, 1.8);
tl.call(enableInteractions, null, 1.8);
}
// -------------------------------------------------------------------
// CONTINUOUS
// -------------------------------------------------------------------
function startContinuous() {
// Orbs float at different rates
root.querySelectorAll('.scene__orb').forEach((orb, i) => {
gsap.to(orb, {
y: `-=${12 + i * 3}`,
duration: 2 + i * 0.4,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
delay: i * 0.15,
});
});
// Check icon floats + rotates slightly
const check = root.querySelector('.scene__check');
gsap.to(check, {
y: '-=15',
rotation: 3,
duration: 3,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
// Chips subtle float
chips.forEach((chip, i) => {
gsap.to(chip, {
y: '-=6',
duration: 2.2 + i * 0.3,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
delay: i * 0.2,
});
});
// Light sweep under checkmark
const light = root.querySelector('.scene__light');
gsap.to(light, {
x: 60, opacity: 0.6,
duration: 3,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
}
// -------------------------------------------------------------------
// INTERACTIONS
// -------------------------------------------------------------------
function enableInteractions() {
// ---- Custom cursor ----
let mx = window.innerWidth / 2, my = window.innerHeight / 2;
let rx = mx, ry = my;
window.addEventListener('mousemove', (e) => { mx = e.clientX; my = e.clientY; });
gsap.ticker.add(() => {
rx += (mx - rx) * 0.18;
ry += (my - ry) * 0.18;
gsap.set(cursorDot, { x: mx, y: my });
gsap.set(cursorRing, { x: rx, y: ry });
});
const hovers = root.querySelectorAll('a, button, [data-magnetic], [data-chip], .scene__orb, .hero__char');
hovers.forEach((el) => {
el.addEventListener('mouseenter', () => cursorRing.classList.add('is-hover'));
el.addEventListener('mouseleave', () => cursorRing.classList.remove('is-hover'));
});
// ---- Ambient glow follows cursor ----
let gx = 0, gy = 0, gcx = 0, gcy = 0;
root.addEventListener('mousemove', (e) => {
const r = root.getBoundingClientRect();
gx = e.clientX - r.left;
gy = e.clientY - r.top;
});
gsap.ticker.add(() => {
gcx += (gx - gcx) * 0.04;
gcy += (gy - gcy) * 0.04;
gsap.set(glow, { x: gcx - window.innerWidth / 2, y: gcy - window.innerHeight / 2 });
});
// ---- Magnetic ----
magnetics.forEach((el) => {
const strength = el.classList.contains('cta-big') ? 0.3
: el.classList.contains('topnav__login') ? 0.3
: el.classList.contains('topnav__logo') ? 0.2
: 0.22;
el.addEventListener('mousemove', (e) => {
const r = el.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
gsap.to(el, {
x: (e.clientX - cx) * strength,
y: (e.clientY - cy) * strength,
duration: 0.4, ease: 'power3.out',
});
});
el.addEventListener('mouseleave', () => {
gsap.to(el, { x: 0, y: 0, duration: 0.6, ease: 'elastic.out(1, 0.4)' });
});
});
// ---- Logo: V mark draws on hover ----
const logo = root.querySelector('.topnav__logo');
logo.addEventListener('mouseenter', () => {
gsap.fromTo(logoMark.querySelector('path'),
{ strokeDasharray: 80, strokeDashoffset: 80 },
{ strokeDashoffset: 0, duration: 0.6, ease: 'power2.out' }
);
});
// ---- Title char proximity ----
const hero = document.getElementById('hero');
let tmx = -9999, tmy = -9999;
hero.addEventListener('mousemove', (e) => { tmx = e.clientX; tmy = e.clientY; });
hero.addEventListener('mouseleave', () => {
tmx = -9999; tmy = -9999;
titleChars.forEach((c) => gsap.to(c, { y: 0, duration: 0.5, ease: 'power3.out' }));
});
gsap.ticker.add(() => {
if (tmx < 0) return;
titleChars.forEach((c) => {
const r = c.getBoundingClientRect();
if (r.width === 0) return;
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const dx = tmx - cx, dy = tmy - cy;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 150) gsap.set(c, { y: -(1 - dist / 150) * 16 });
else gsap.set(c, { y: 0 });
});
});
// ---- Chips: hover pop + shimmer ----
chips.forEach((chip) => {
chip.addEventListener('mouseenter', () => {
gsap.to(chip, {
scale: 1.04,
boxShadow: '0 10px 30px rgba(139, 47, 230, 0.35)',
duration: 0.3,
ease: 'back.out(2)',
});
// Icon wiggle
const icon = chip.querySelector('.chip__icon');
gsap.fromTo(icon,
{ rotation: -15 },
{ rotation: 15, duration: 0.1, yoyo: true, repeat: 3, ease: 'sine.inOut',
onComplete: () => gsap.to(icon, { rotation: 0, duration: 0.3 })
}
);
});
chip.addEventListener('mouseleave', () => {
gsap.to(chip, {
scale: 1, boxShadow: 'none',
duration: 0.4, ease: 'elastic.out(1, 0.4)',
});
});
});
// ---- CTA shimmer + click pulse ----
cta.addEventListener('click', (e) => {
e.preventDefault();
gsap.fromTo(cta,
{ scale: 1 },
{ scale: 0.95, duration: 0.1, yoyo: true, repeat: 1, ease: 'sine.inOut' }
);
// Burst dots
const rect = cta.getBoundingClientRect();
const cx = rect.left + rect.width - 19;
const cy = rect.top + rect.height / 2;
for (let i = 0; i < 6; i++) {
const dot = document.createElement('span');
dot.style.position = 'fixed';
dot.style.left = cx + 'px';
dot.style.top = cy + 'px';
dot.style.width = '5px';
dot.style.height = '5px';
dot.style.background = '#c084fc';
dot.style.borderRadius = '50%';
dot.style.pointerEvents = 'none';
dot.style.zIndex = '200';
dot.style.transform = 'translate(-50%, -50%)';
dot.style.boxShadow = '0 0 8px #c084fc';
document.body.appendChild(dot);
const a = (i / 6) * Math.PI * 2;
gsap.fromTo(dot,
{ x: 0, y: 0, opacity: 1 },
{
x: Math.cos(a) * 50,
y: Math.sin(a) * 50,
opacity: 0,
scale: 1.8,
duration: 0.6,
ease: 'power2.out',
onComplete: () => dot.remove(),
}
);
}
});
// ---- Scene orbs: mouse parallax + click scatter ----
const scene = document.getElementById('scene');
const orbs = root.querySelectorAll('.scene__orb');
let sx = 0, sy = 0, scx = 0, scy = 0;
root.addEventListener('mousemove', (e) => {
const r = root.getBoundingClientRect();
sx = ((e.clientX - r.left) / r.width - 0.5) * 2;
sy = ((e.clientY - r.top) / r.height - 0.5) * 2;
});
root.addEventListener('mouseleave', () => { sx = 0; sy = 0; });
gsap.ticker.add(() => {
scx += (sx - scx) * 0.05;
scy += (sy - scy) * 0.05;
orbs.forEach((orb, i) => {
const depth = 0.4 + i * 0.15;
gsap.set(orb, {
x: scx * 25 * depth,
y: scy * 18 * depth,
});
});
const check = root.querySelector('.scene__check');
if (check) gsap.set(check, {
rotationY: scx * 8,
rotationX: -scy * 6,
transformPerspective: 1000,
transformOrigin: 'center',
});
});
// ---- Orbs: click bounces them away ----
orbs.forEach((orb) => {
orb.style.pointerEvents = 'auto';
orb.addEventListener('mouseenter', () => {
gsap.to(orb, { scale: 1.15, duration: 0.3, ease: 'back.out(2)' });
});
orb.addEventListener('mouseleave', () => {
gsap.to(orb, { scale: 1, duration: 0.4, ease: 'elastic.out(1, 0.4)' });
});
orb.addEventListener('click', () => {
gsap.fromTo(orb,
{ x: 0 },
{ x: (Math.random() - 0.5) * 80, duration: 0.3, yoyo: true, repeat: 1, ease: 'power2.out' }
);
});
});
}
})();