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 Event Landing Page Template with Countdown and Ticket Loader</title>
<meta name="description" content="Free GSAP event landing page hero from ToolsWaves" />
<meta name="generator" content="ToolsWaves - https://toolswaves.in/landing-pages" />
<!-- Open Graph -->
<meta property="og:title" content="Free Event Landing Page Template with Countdown and Ticket Loader" />
<meta property="og:description" content="Free GSAP event landing page hero from ToolsWaves" />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://toolswaves.in/og?title=Free%20Event%20Landing%20Page%20Template%20with%20Countdown%20and%20Ticket%20Loader&category=Landing%20Page&icon=%F0%9F%93%84" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Free Event Landing Page Template with Countdown and Ticket Loader" />
<meta name="twitter:description" content="Free GSAP event landing page hero from ToolsWaves" />
<meta name="twitter:image" content="https://toolswaves.in/og?title=Free%20Event%20Landing%20Page%20Template%20with%20Countdown%20and%20Ticket%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: #0a0810;
--bg-2: #110c18;
--ink: #ffffff;
--muted: rgba(255, 255, 255, 0.55);
--line: rgba(255, 255, 255, 0.15);
--accent:#d14a7a;
--violet:#5a2c8a;
--cream: #f5ecd8;
}
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.08);
border: 1px solid rgba(255, 255, 255, 0.18);
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%);
mix-blend-mode: difference;
will-change: transform;
}
.cursor-dot {
width: 6px; height: 6px;
background: var(--cream);
border-radius: 50%;
}
.cursor-ring {
width: 36px; height: 36px;
border: 1px solid rgba(245, 236, 216, 0.6);
border-radius: 50%;
transition: width 0.3s ease, height 0.3s ease, border-color 0.3s ease;
}
.cursor-ring.is-hover { width: 60px; height: 60px; border-color: var(--cream); }
@media (hover: none), (pointer: coarse) { .cursor-dot, .cursor-ring { display: none; } }
/* =========================================================
LOADER — Concert ticket
========================================================= */
.loader {
position: fixed;
inset: 0;
z-index: 999;
background: radial-gradient(ellipse at center, var(--bg-2) 0%, var(--bg) 80%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1.75rem;
}
.ticket {
position: relative;
width: min(520px, 82vw);
height: 180px;
display: flex;
background: var(--cream);
color: #1a1a1a;
border-radius: 6px;
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.55),
0 10px 20px rgba(0, 0, 0, 0.35);
overflow: hidden;
will-change: transform;
}
.ticket__half {
position: relative;
padding: 1rem 1.25rem;
display: flex;
flex-direction: column;
justify-content: space-between;
will-change: transform;
}
.ticket__half--left { flex: 0 0 72%; }
.ticket__half--right {
flex: 1;
background: #1a1a1a;
color: var(--cream);
align-items: center;
justify-content: center;
text-align: center;
}
.ticket__perf {
width: 1px;
border-left: 2px dashed rgba(26, 26, 26, 0.35);
margin: 12px 0;
background: transparent;
}
.ticket__top, .ticket__bottom {
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'JetBrains Mono', monospace;
font-size: 0.62rem;
letter-spacing: 0.2em;
color: rgba(26, 26, 26, 0.6);
}
.ticket__sm {
font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem;
letter-spacing: 0.2em;
font-weight: 600;
}
.ticket__main {
display: flex;
align-items: flex-end;
gap: 1rem;
}
.ticket__date {
font-family: 'Bebas Neue', sans-serif;
font-size: 1.4rem;
line-height: 1;
color: var(--accent);
letter-spacing: 0.05em;
}
.ticket__event {
font-family: 'Bebas Neue', sans-serif;
font-size: 2.3rem;
line-height: 0.85;
letter-spacing: 0.02em;
color: #1a1a1a;
}
.ticket__barcode {
display: flex;
gap: 1px;
height: 28px;
align-items: stretch;
flex: 1;
margin-right: 0.75rem;
}
.ticket__barcode span {
display: block;
background: #1a1a1a;
flex: 1;
}
.ticket__stub {
display: flex;
flex-direction: column;
gap: 0.25rem;
align-items: center;
}
.ticket__stub-no {
font-family: 'Bebas Neue', sans-serif;
font-size: 2rem;
line-height: 1;
letter-spacing: 0.05em;
color: var(--accent);
}
.ticket__stamp {
position: absolute;
top: 18%;
right: 18%;
font-family: 'Bebas Neue', sans-serif;
font-size: 1.6rem;
color: var(--accent);
border: 3px solid var(--accent);
padding: 0.2rem 0.8rem;
letter-spacing: 0.15em;
transform: rotate(-14deg) scale(3);
opacity: 0;
z-index: 5;
text-shadow: 1px 1px 0 rgba(209, 74, 122, 0.4);
}
.loader__meta {
display: flex;
gap: 1.25rem;
align-items: center;
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
letter-spacing: 0.25em;
color: var(--muted);
}
.loader__counter {
color: var(--cream);
font-weight: 600;
font-variant-numeric: tabular-nums;
min-width: 3ch;
}
/* =========================================================
EVENT SECTION
========================================================= */
.event {
position: relative;
width: 100%;
height: 100vh;
min-height: 720px;
overflow: hidden;
background: var(--bg);
}
/* ---------- Split background ---------- */
.bg { position: absolute; inset: 0; z-index: 1; }
.bg__half {
position: absolute;
top: 0; bottom: 0;
width: 50%;
overflow: hidden;
will-change: transform;
}
.bg__half--left { left: 0; }
.bg__half--right { right: 0; }
.bg__half img,
.bg__half video {
width: 100%; height: 100%;
object-fit: cover;
filter: saturate(1.1) contrast(1.05);
}
.bg__overlay {
position: absolute;
inset: 0;
background:
radial-gradient(ellipse at center, rgba(10, 8, 16, 0.45) 0%, rgba(10, 8, 16, 0.85) 100%),
linear-gradient(180deg, rgba(10, 8, 16, 0.4) 0%, rgba(10, 8, 16, 0.2) 30%, rgba(10, 8, 16, 0.8) 100%),
linear-gradient(90deg, rgba(90, 44, 138, 0.35) 0%, rgba(10, 8, 16, 0) 25%, rgba(10, 8, 16, 0) 75%, rgba(209, 74, 122, 0.2) 100%);
pointer-events: none;
}
.bg__divider {
position: absolute;
top: 0; bottom: 0;
left: 50%;
width: 1px;
background: linear-gradient(180deg, transparent, rgba(255,255,255,0.25) 15%, rgba(255,255,255,0.25) 85%, transparent);
transform-origin: center;
z-index: 2;
pointer-events: none;
}
.bg__divider::before {
content: '';
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 6px; height: 40px;
background: rgba(255, 255, 255, 0.5);
border-radius: 4px;
}
/* ---------- Top nav ---------- */
.topnav {
position: absolute;
top: 0; left: 0; right: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
gap: 2rem;
padding: 1.25rem 2rem;
background: rgba(10, 8, 16, 0.6);
backdrop-filter: blur(10px);
}
.topnav__left, .topnav__right {
display: flex;
gap: 2rem;
flex: 1;
}
.topnav__left { justify-content: flex-end; }
.topnav__right { justify-content: flex-start; }
.topnav a {
position: relative;
color: var(--ink);
text-decoration: none;
font-weight: 600;
font-size: 0.85rem;
letter-spacing: 0.12em;
padding: 0.3rem 0;
cursor: none;
will-change: transform;
display: inline-flex;
align-items: center;
gap: 0.3rem;
}
.topnav a.is-active { color: var(--cream); }
.topnav a::after {
content: '';
position: absolute;
left: 0; bottom: -2px;
width: 100%; height: 1px;
background: var(--cream);
transform: scaleX(0);
transform-origin: right;
transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.topnav a.is-active::after,
.topnav a:hover::after { transform: scaleX(1); transform-origin: left; }
.caret { font-size: 0.65em; opacity: 0.7; transition: transform 0.3s; }
.topnav a:hover .caret { transform: translateY(2px); }
.topnav__logo {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-family: 'Inter', sans-serif;
font-weight: 700;
font-size: 0.95rem;
letter-spacing: 0.18em;
padding: 0 0.75rem;
cursor: none;
will-change: transform;
text-decoration: none;
color: var(--ink);
white-space: nowrap;
}
.topnav__logo::after { display: none; }
.topnav__logo-divider {
color: var(--accent);
font-size: 1.2em;
font-weight: 400;
}
/* ---------- Hero ---------- */
.hero {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 15;
text-align: center;
width: min(700px, 90vw);
padding: 0 1rem;
}
.hero__title {
font-family: 'Bebas Neue', sans-serif;
font-size: clamp(2.4rem, 5.5vw, 4.5rem);
line-height: 1;
letter-spacing: 0.02em;
color: var(--ink);
margin-bottom: 1rem;
text-shadow: 0 8px 40px rgba(0, 0, 0, 0.6);
}
.hero__line {
display: block;
overflow: hidden;
padding-bottom: 0.02em;
}
.hero__char {
display: inline-block;
will-change: transform, color;
transition: color 0.3s ease;
}
.hero__char.is-space { width: 0.2em; }
.hero__date {
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
letter-spacing: 0.35em;
color: var(--muted);
margin-bottom: 1.75rem;
}
/* Progress bar */
.progress {
position: relative;
width: 100%;
max-width: 460px;
margin: 0 auto 1.5rem;
height: 20px;
display: flex;
align-items: center;
}
.progress__ticks {
position: absolute;
inset: 0;
display: flex;
justify-content: space-between;
align-items: center;
pointer-events: none;
}
.progress__ticks span {
flex: 1;
height: 100%;
border-right: 1px solid rgba(255, 255, 255, 0.06);
}
.progress__fill {
position: absolute;
left: 0; top: 50%;
height: 2px;
width: 0%;
background: var(--cream);
transform: translateY(-50%);
box-shadow: 0 0 10px rgba(245, 236, 216, 0.6);
transform-origin: left;
}
.progress__handle {
position: absolute;
top: 50%;
left: 0;
width: 10px; height: 10px;
background: var(--cream);
border-radius: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 0 0 4px rgba(245, 236, 216, 0.2);
}
/* Countdown */
.countdown {
display: inline-flex;
align-items: flex-start;
gap: 2rem;
font-family: 'Inter', sans-serif;
}
.countdown__dash {
font-size: 2rem;
line-height: 1;
color: var(--muted);
margin-top: 0.3rem;
}
.countdown__cell {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
min-width: 60px;
}
.countdown__value {
font-size: clamp(1.75rem, 3vw, 2.5rem);
font-weight: 700;
line-height: 1;
color: var(--ink);
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
position: relative;
will-change: transform;
}
.countdown__label {
font-size: 0.7rem;
letter-spacing: 0.2em;
color: var(--muted);
text-transform: uppercase;
}
/* ---------- Right-side quicklinks ---------- */
.quicklinks {
position: absolute;
top: 50%;
right: 3rem;
transform: translateY(-50%);
z-index: 18;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.quicklinks a {
position: relative;
display: inline-flex;
align-items: center;
gap: 1rem;
color: var(--ink);
text-decoration: none;
font-weight: 700;
font-size: 1rem;
letter-spacing: 0.15em;
cursor: none;
will-change: transform;
}
.quicklinks__bar {
display: inline-block;
width: 40px;
height: 1px;
background: var(--cream);
transition: width 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.quicklinks a:hover .quicklinks__bar { width: 70px; }
.quicklinks__text {
position: relative;
transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.quicklinks a:hover .quicklinks__text {
transform: translateX(6px);
color: var(--cream);
}
/* ---------- Bottom meta ---------- */
.meta {
position: absolute;
bottom: 1.5rem;
left: 0; right: 0;
z-index: 15;
display: flex;
justify-content: center;
gap: 3rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.7rem;
letter-spacing: 0.25em;
color: var(--muted);
}
/* ---------- Initial hidden states ---------- */
[data-magnetic], [data-link], [data-quick], [data-fade], [data-title-line] .hero__char, [data-bg] { opacity: 0; }
[data-title-line] .hero__char { opacity: 0; }
/* ---------- Responsive ---------- */
@media (max-width: 900px) {
.topnav__left, .topnav__right { gap: 1.25rem; }
.quicklinks { right: 1.25rem; gap: 0.5rem; }
.quicklinks a { font-size: 0.85rem; }
.quicklinks__bar { width: 24px; }
.countdown { gap: 1rem; }
.countdown__cell { min-width: 50px; }
.ticket { height: 150px; }
.ticket__event { font-size: 1.8rem; }
}
@media (max-width: 640px) {
.topnav {
display: grid;
grid-template-columns: 1fr;
padding: 0.6rem 1rem;
}
.topnav__left, .topnav__right { display: none; }
.topnav__logo { justify-self: center; }
.quicklinks {
position: absolute;
top: auto;
bottom: 4rem;
right: 50%;
transform: translateX(50%);
flex-direction: row;
gap: 1rem;
}
.quicklinks__bar { display: none; }
.quicklinks a { font-size: 0.7rem; }
.meta { display: none; }
.hero__date { font-size: 0.65rem; margin-bottom: 1rem; }
.progress { max-width: 280px; }
.countdown { gap: 0.7rem; }
.countdown__dash { display: none; }
.countdown__cell { min-width: 42px; }
.countdown__value { font-size: 1.5rem; }
.countdown__label { font-size: 0.6rem; }
.bg__divider { 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 — Event Countdown</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=Bebas+Neue&family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="banner-event-countdown.css">
</head>
<body>
<!-- Custom cursor -->
<div class="cursor-dot" id="cursor-dot"></div>
<div class="cursor-ring" id="cursor-ring"></div>
<!-- ========== LOADER — CONCERT TICKET ========== -->
<div class="loader" id="loader">
<div class="ticket" id="ticket">
<div class="ticket__half ticket__half--left" id="ticket-left">
<div class="ticket__top">
<span class="ticket__sm">ADMIT ONE</span>
<span class="ticket__sm">#00427</span>
</div>
<div class="ticket__main">
<div class="ticket__date">12·12·26</div>
<div class="ticket__event">NIGHT<br>RHYTHM</div>
</div>
<div class="ticket__bottom">
<div class="ticket__barcode" id="barcode"></div>
<span class="ticket__sm">GATE A</span>
</div>
</div>
<div class="ticket__perf"></div>
<div class="ticket__half ticket__half--right" id="ticket-right">
<div class="ticket__stub">
<span class="ticket__sm">STUB</span>
<div class="ticket__stub-no">427</div>
<span class="ticket__sm">10 PM</span>
</div>
</div>
<div class="ticket__stamp" id="ticket-stamp">ADMITTED</div>
</div>
<div class="loader__meta">
<span class="loader__label" id="loader-label">SOUND CHECK</span>
<span class="loader__counter" id="loader-counter">00%</span>
</div>
</div>
<section class="event" id="event">
<!-- ========== SPLIT BACKGROUND (image + video) ========== -->
<div class="bg">
<div class="bg__half bg__half--left" data-bg>
<img src="https://images.unsplash.com/photo-1514320291840-2e0a9bf2a9ae?auto=format&fit=crop&w=1200&q=75"
alt="" draggable="false" onerror="this.style.display='none'">
</div>
<div class="bg__half bg__half--right" data-bg>
<video autoplay loop muted playsinline
poster="https://images.unsplash.com/photo-1501281668745-f7f57925c3b4?auto=format&fit=crop&w=1200&q=75">
<source src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4" type="video/mp4">
</video>
</div>
<div class="bg__overlay"></div>
<div class="bg__divider" id="divider"></div>
</div>
<!-- ========== TOP NAV ========== -->
<header class="topnav">
<nav class="topnav__left">
<a href="#" class="is-active" data-magnetic data-link>HOME</a>
<a href="#" data-magnetic data-link>EVENTS <span class="caret">▾</span></a>
<a href="#" data-magnetic data-link>GALLERY</a>
</nav>
<a href="#" class="topnav__logo" data-magnetic>
<span>BRAND</span>
<span class="topnav__logo-divider">/</span>
<span>CLUB</span>
</a>
<nav class="topnav__right">
<a href="#" data-magnetic data-link>PRODUCTS</a>
<a href="#" data-magnetic data-link>BLOG</a>
<a href="#" data-magnetic data-link>CONTACTS</a>
</nav>
</header>
<!-- ========== MAIN CONTENT ========== -->
<div class="hero" id="hero">
<h1 class="hero__title" aria-label="Night Rhythm Live Party">
<span class="hero__line" data-title-line>NIGHT RHYTHM</span>
<span class="hero__line" data-title-line>LIVE PARTY</span>
</h1>
<div class="hero__date" data-fade>12 DECEMBER 2026 FROM 10PM</div>
<!-- Progress bar -->
<div class="progress" data-fade>
<div class="progress__ticks" id="progress-ticks"></div>
<div class="progress__fill" id="progress-fill"></div>
<div class="progress__handle" id="progress-handle"></div>
</div>
<!-- Countdown -->
<div class="countdown" data-fade>
<span class="countdown__dash">—</span>
<div class="countdown__cell">
<span class="countdown__value" id="days">000</span>
<span class="countdown__label">Days</span>
</div>
<div class="countdown__cell">
<span class="countdown__value" id="hours">00</span>
<span class="countdown__label">Hours</span>
</div>
<div class="countdown__cell">
<span class="countdown__value" id="mins">00</span>
<span class="countdown__label">Mins</span>
</div>
<div class="countdown__cell">
<span class="countdown__value" id="secs">00</span>
<span class="countdown__label">Secs</span>
</div>
</div>
</div>
<!-- ========== RIGHT-SIDE QUICK LINKS ========== -->
<nav class="quicklinks" aria-label="Event quick links">
<a href="#" data-quick>
<span class="quicklinks__bar"></span>
<span class="quicklinks__text">DETAILS</span>
</a>
<a href="#" data-quick>
<span class="quicklinks__bar"></span>
<span class="quicklinks__text">GALLERY</span>
</a>
<a href="#" data-quick>
<span class="quicklinks__bar"></span>
<span class="quicklinks__text">LOCATION</span>
</a>
</nav>
<!-- ========== BOTTOM META ========== -->
<div class="meta" data-fade>
<span class="meta__cell">EST. 2015</span>
<span class="meta__cell">SEASON 08</span>
<span class="meta__cell">+40 MINS SET</span>
</div>
</section>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="banner-event-countdown.js"></script>
</body>
</html>
</body>
</html>JS banner-event-countdown.js — GSAP animation timeline
(() => {
const root = document.getElementById('event');
if (!root) return;
// -------------------------------------------------------------------
// EVENT TARGET DATE
// -------------------------------------------------------------------
const TARGET_DATE = new Date('2026-12-12T22:00:00').getTime();
const BASE_DATE = new Date('2026-04-20T00:00:00').getTime(); // used for progress %
// -------------------------------------------------------------------
// BUILD BARCODE + PROGRESS TICKS
// -------------------------------------------------------------------
const barcode = document.getElementById('barcode');
for (let i = 0; i < 45; i++) {
const b = document.createElement('span');
b.style.flexGrow = String(0.4 + Math.random() * 1.4);
barcode.appendChild(b);
}
const progressTicks = document.getElementById('progress-ticks');
for (let i = 0; i < 40; i++) progressTicks.appendChild(document.createElement('span'));
// -------------------------------------------------------------------
// SPLIT TITLE INTO CHARS
// -------------------------------------------------------------------
const titleLines = root.querySelectorAll('[data-title-line]');
titleLines.forEach((line) => {
const text = line.textContent;
line.textContent = '';
[...text].forEach((ch) => {
const span = document.createElement('span');
span.className = 'hero__char';
if (ch === ' ') { span.classList.add('is-space'); span.innerHTML = ' '; }
else span.textContent = ch;
line.appendChild(span);
});
});
// -------------------------------------------------------------------
// REFERENCES
// -------------------------------------------------------------------
const loader = document.getElementById('loader');
const ticket = document.getElementById('ticket');
const ticketLeft = document.getElementById('ticket-left');
const ticketRight = document.getElementById('ticket-right');
const stamp = document.getElementById('ticket-stamp');
const loaderCounter = document.getElementById('loader-counter');
const loaderLabel = document.getElementById('loader-label');
const barcodeBars = barcode.querySelectorAll('span');
const cursorDot = document.getElementById('cursor-dot');
const cursorRing = document.getElementById('cursor-ring');
const bgHalves = root.querySelectorAll('[data-bg]');
const divider = document.getElementById('divider');
const magnetics = root.querySelectorAll('[data-magnetic]');
const titleChars = root.querySelectorAll('.hero__char');
const fades = root.querySelectorAll('[data-fade]');
const quickLinks = root.querySelectorAll('[data-quick]');
const daysEl = document.getElementById('days');
const hoursEl = document.getElementById('hours');
const minsEl = document.getElementById('mins');
const secsEl = document.getElementById('secs');
const progressFill = document.getElementById('progress-fill');
const progressHandle = document.getElementById('progress-handle');
// -------------------------------------------------------------------
// INITIAL STATES
// -------------------------------------------------------------------
gsap.set(bgHalves[0], { xPercent: -100 });
gsap.set(bgHalves[1], { xPercent: 100 });
gsap.set(divider, { scaleY: 0, opacity: 0, transformOrigin: 'center' });
gsap.set(magnetics, { y: -20, opacity: 0 });
gsap.set(titleChars, { yPercent: 110, opacity: 0 });
gsap.set(fades, { y: 20, opacity: 0 });
gsap.set(quickLinks, { x: 40, opacity: 0 });
gsap.set(progressFill, { width: '0%' });
gsap.set(progressHandle, { left: '0%' });
// -------------------------------------------------------------------
// LOADER TIMELINE — Concert ticket
// -------------------------------------------------------------------
const loaderTl = gsap.timeline({ onComplete: playScene });
// Ticket entrance (slides in + slight rotate)
loaderTl.from(ticket, {
y: 40, rotation: -8, opacity: 0, scale: 0.9,
duration: 0.7, ease: 'power3.out',
}, 0);
// Barcode bars scale in sequentially
loaderTl.from(barcodeBars, {
scaleY: 0, duration: 0.5,
stagger: 0.015, ease: 'power2.out',
transformOrigin: 'bottom',
}, 0.3);
// Counter + progress
const p = { v: 0 };
loaderTl.to(p, {
v: 100,
duration: 2.2,
ease: 'power1.inOut',
onUpdate: () => {
loaderCounter.textContent = Math.floor(p.v) + '%';
if (p.v > 25 && loaderLabel.textContent === 'SOUND CHECK') loaderLabel.textContent = 'LIGHTS UP';
if (p.v > 60 && loaderLabel.textContent === 'LIGHTS UP') loaderLabel.textContent = 'DROP THE BEAT';
if (p.v > 90 && loaderLabel.textContent === 'DROP THE BEAT')loaderLabel.textContent = 'READY';
},
}, 0.1);
// Stamp lands (scale from big + rotate)
loaderTl.to(stamp, {
scale: 1,
opacity: 1,
duration: 0.4,
ease: 'back.out(2.5)',
}, '-=0.3');
loaderTl.to(ticket, {
scale: 1.03, duration: 0.08, yoyo: true, repeat: 1, ease: 'sine.inOut',
}, '<');
// Tear: left slides left, right slides right
loaderTl.to(ticketLeft, {
x: '-=60', rotation: -5, duration: 0.7, ease: 'power3.in',
}, '+=0.3');
loaderTl.to(ticketRight, {
x: '+=60', rotation: 5, duration: 0.7, ease: 'power3.in',
}, '<');
// Everything fades + scales down
loaderTl.to(ticket, {
y: -40, scale: 0.92, opacity: 0, duration: 0.6, ease: 'power3.in',
}, '-=0.2');
loaderTl.to([loaderCounter, loaderLabel], {
y: 10, opacity: 0, duration: 0.3, stagger: 0.05,
}, '<');
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: 'power4.out' } });
// Background halves slide in from sides (like a ticket tear reverse)
tl.to(bgHalves[0], { xPercent: 0, opacity: 1, duration: 1.2 }, 0);
tl.to(bgHalves[1], { xPercent: 0, opacity: 1, duration: 1.2 }, 0);
// Divider grows
tl.to(divider, {
scaleY: 1, opacity: 1,
duration: 1,
ease: 'power4.inOut',
}, 0.4);
// Nav
tl.to(magnetics, {
y: 0, opacity: 1, duration: 0.6, stagger: 0.04, ease: 'power3.out',
}, 0.3);
// Title chars
tl.to(titleChars, {
yPercent: 0, opacity: 1,
duration: 1,
stagger: 0.025,
ease: 'expo.out',
}, 0.6);
// Fades
tl.to(fades, {
y: 0, opacity: 1,
duration: 0.7,
stagger: 0.12,
ease: 'power3.out',
}, 1);
// Quick links
tl.to(quickLinks, {
x: 0, opacity: 1,
duration: 0.7,
stagger: 0.1,
ease: 'power3.out',
}, 1);
// Progress bar fills to computed %
const pct = computeProgressPercent();
tl.to(progressFill, {
width: pct + '%',
duration: 1.4,
ease: 'power2.out',
}, 1.2);
tl.to(progressHandle, {
left: pct + '%',
duration: 1.4,
ease: 'power2.out',
}, 1.2);
tl.call(startLoops, null, 1.5);
tl.call(enableInteractions, null, 1.5);
}
// -------------------------------------------------------------------
// COUNTDOWN
// -------------------------------------------------------------------
function updateCountdown() {
const now = Date.now();
const diff = Math.max(0, TARGET_DATE - now);
const days = Math.floor(diff / 86400000);
const hours = Math.floor((diff / 3600000) % 24);
const mins = Math.floor((diff / 60000) % 60);
const secs = Math.floor((diff / 1000) % 60);
daysEl.textContent = String(days).padStart(3, '0');
hoursEl.textContent = String(hours).padStart(2, '0');
minsEl.textContent = String(mins).padStart(2, '0');
secsEl.textContent = String(secs).padStart(2, '0');
}
function computeProgressPercent() {
// Rough: progress from base date to target
const total = TARGET_DATE - BASE_DATE;
const elapsed = Date.now() - BASE_DATE;
if (total <= 0) return 100;
const pct = Math.max(0, Math.min(100, (elapsed / total) * 100));
return pct;
}
function startLoops() {
updateCountdown();
setInterval(updateCountdown, 1000);
}
// -------------------------------------------------------------------
// 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-quick], .countdown__cell');
hovers.forEach((el) => {
el.addEventListener('mouseenter', () => cursorRing.classList.add('is-hover'));
el.addEventListener('mouseleave', () => cursorRing.classList.remove('is-hover'));
});
// ---- Magnetic ----
magnetics.forEach((el) => {
const s = el.classList.contains('topnav__logo') ? 0.18 : 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) * s,
y: (e.clientY - cy) * s,
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)' });
});
});
// ---- Title char proximity + individual hover ----
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) * 18 });
else gsap.set(c, { y: 0 });
});
});
titleChars.forEach((c) => {
c.addEventListener('mouseenter', () => gsap.to(c, { color: '#d14a7a', duration: 0.2 }));
c.addEventListener('mouseleave', () => gsap.to(c, { color: '#ffffff', duration: 0.4 }));
});
// ---- Countdown cells: scramble on hover ----
const cells = [
{ el: daysEl, valueFn: () => daysEl.textContent, len: 3 },
{ el: hoursEl, valueFn: () => hoursEl.textContent, len: 2 },
{ el: minsEl, valueFn: () => minsEl.textContent, len: 2 },
{ el: secsEl, valueFn: () => secsEl.textContent, len: 2 },
];
cells.forEach(({ el, valueFn, len }) => {
const cellParent = el.closest('.countdown__cell');
cellParent.addEventListener('mouseenter', () => {
const final = valueFn();
let t = 0;
const scramble = () => {
t++;
if (t >= 8) {
el.textContent = final;
gsap.to(el, { scale: 1.18, duration: 0.15, yoyo: true, repeat: 1, ease: 'sine.inOut' });
return;
}
el.textContent = Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
setTimeout(scramble, 35);
};
scramble();
});
});
// ---- Quicklinks on hover: reveal sweeping bar ----
quickLinks.forEach((q) => {
const bar = q.querySelector('.quicklinks__bar');
q.addEventListener('mouseenter', () => {
gsap.fromTo(bar,
{ width: 40, background: '#ffffff' },
{ width: 70, background: '#d14a7a', duration: 0.4, ease: 'power3.out' }
);
});
q.addEventListener('mouseleave', () => {
gsap.to(bar, { width: 40, background: '#f5ecd8', duration: 0.5, ease: 'power3.out' });
});
});
// ---- Background image/video parallax (opposite directions) ----
let bx = 0, by = 0, bxC = 0, byC = 0;
root.addEventListener('mousemove', (e) => {
const r = root.getBoundingClientRect();
bx = ((e.clientX - r.left) / r.width - 0.5) * 2;
by = ((e.clientY - r.top) / r.height - 0.5) * 2;
});
root.addEventListener('mouseleave', () => { bx = 0; by = 0; });
gsap.ticker.add(() => {
bxC += (bx - bxC) * 0.05;
byC += (by - byC) * 0.05;
gsap.set(bgHalves[0], { x: -bxC * 20, y: -byC * 10 });
gsap.set(bgHalves[1], { x: bxC * 20, y: -byC * 10 });
gsap.set(divider, { x: bxC * 6 });
});
// ---- Center handle pulses continuously ----
gsap.to(progressHandle, {
scale: 1.25,
duration: 0.8,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
transformOrigin: 'center center',
});
// ---- Divider subtle shimmer ----
const divHighlight = divider;
gsap.to(divHighlight, {
opacity: 0.55,
duration: 2.4,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
});
}
})();