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 Media Platform Hero Template with Waveform Loader</title>
<meta name="description" content="Free GSAP media platform landing page hero from ToolsWaves" />
<meta name="generator" content="ToolsWaves - https://toolswaves.in/landing-pages" />
<!-- Open Graph -->
<meta property="og:title" content="Free Media Platform Hero Template with Waveform Loader" />
<meta property="og:description" content="Free GSAP media platform landing page hero from ToolsWaves" />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://toolswaves.in/og?title=Free%20Media%20Platform%20Hero%20Template%20with%20Waveform%20Loader&category=Landing%20Page&icon=%F0%9F%93%84" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Free Media Platform Hero Template with Waveform Loader" />
<meta name="twitter:description" content="Free GSAP media platform landing page hero from ToolsWaves" />
<meta name="twitter:image" content="https://toolswaves.in/og?title=Free%20Media%20Platform%20Hero%20Template%20with%20Waveform%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: #000000;
--ink: #ffffff;
--muted: rgba(255, 255, 255, 0.55);
--line: rgba(255, 255, 255, 0.12);
--cream: #f2e8d5;
--accent: #ffffff;
}
html, body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg);
color: var(--ink);
overflow: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.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);
opacity: 0.85;
transition: opacity 0.3s, transform 0.3s;
}
.back-link:hover { opacity: 1; transform: translateY(-2px); }
/* =========================================================
LOADER — audio waveform bars
========================================================= */
.loader {
position: fixed;
inset: 0;
z-index: 999;
background: var(--bg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2.5rem;
}
.loader__wave {
display: flex;
align-items: center;
gap: 4px;
height: 120px;
}
.loader__wave span {
display: block;
width: 5px;
min-height: 5px;
background: var(--ink);
border-radius: 3px;
transform-origin: center;
will-change: transform, height;
}
.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(--ink);
font-weight: 600;
font-variant-numeric: tabular-nums;
min-width: 3ch;
text-align: right;
}
/* =========================================================
MEDIA GRID SECTION
========================================================= */
.mediagrid {
position: relative;
width: 100%;
height: 100vh;
min-height: 780px;
overflow: hidden;
background: var(--bg);
}
.mediagrid__topbar {
position: absolute;
top: 0; left: 0; right: 0;
height: 84px;
background: linear-gradient(90deg,
#c9bf98 0%,
#6c6450 25%,
#1a1a18 55%,
#000 100%);
opacity: 0.9;
pointer-events: none;
transform-origin: left;
}
/* ---------- Top nav ---------- */
.topnav {
position: relative;
z-index: 30;
display: flex;
align-items: center;
gap: 2rem;
padding: 1.2rem 2rem;
}
.topnav__logo {
display: inline-flex;
align-items: center;
color: var(--ink);
text-decoration: none;
will-change: transform;
}
.topnav__logo-mark {
display: inline-flex;
align-items: center;
gap: 3px;
height: 24px;
}
.topnav__logo-mark span {
width: 2px;
background: var(--ink);
border-radius: 2px;
will-change: transform;
}
.topnav__logo-mark span:nth-child(1) { height: 30%; }
.topnav__logo-mark span:nth-child(2) { height: 80%; }
.topnav__logo-mark span:nth-child(3) { height: 45%; }
.topnav__logo-mark span:nth-child(4) { height: 95%; }
.topnav__logo-mark span:nth-child(5) { height: 55%; }
.topnav__logo-mark span:nth-child(6) { height: 25%; }
.topnav__links {
display: flex;
gap: 2rem;
margin-left: 2rem;
}
.topnav__links a {
position: relative;
color: var(--ink);
text-decoration: none;
font-weight: 500;
font-size: 0.95rem;
padding: 0.3rem 0;
display: inline-flex;
align-items: center;
gap: 0.3rem;
will-change: transform;
}
.caret {
font-size: 0.65em;
opacity: 0.7;
transition: transform 0.3s ease;
}
.topnav__links a:hover .caret { transform: translateY(2px); }
.topnav__links a::after {
content: '';
position: absolute;
left: 0; bottom: 0;
width: 100%; height: 1px;
background: var(--ink);
transform: scaleX(0);
transform-origin: right;
transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.topnav__links a:hover::after { transform: scaleX(1); transform-origin: left; }
.topnav__actions {
margin-left: auto;
display: flex;
align-items: center;
gap: 1.25rem;
}
.topnav__action {
color: var(--ink);
text-decoration: none;
font-weight: 500;
font-size: 0.95rem;
will-change: transform;
position: relative;
padding: 0.3rem 0;
}
.topnav__action::after {
content: '';
position: absolute;
left: 0; bottom: 0;
width: 100%; height: 1px;
background: var(--ink);
transform: scaleX(0);
transform-origin: right;
transition: transform 0.35s cubic-bezier(0.65, 0, 0.35, 1);
}
.topnav__action:hover::after { transform: scaleX(1); transform-origin: left; }
.topnav__cta {
background: var(--ink);
color: var(--bg);
text-decoration: none;
padding: 0.7rem 1.3rem;
border-radius: 999px;
font-weight: 600;
font-size: 0.9rem;
display: inline-block;
will-change: transform;
transition: background 0.3s;
}
.topnav__cta:hover { background: #e5e5e5; }
.topnav__mark {
width: 36px; height: 36px;
background: linear-gradient(135deg, #ff3b3b, #c22020);
color: var(--ink);
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 6px;
will-change: transform;
}
.topnav__mark svg { width: 20px; height: 20px; }
/* ---------- Image grid ---------- */
.grid {
position: absolute;
inset: 0;
z-index: 5;
pointer-events: none;
}
.tile {
position: absolute;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.55);
pointer-events: auto;
cursor: pointer;
will-change: transform, filter;
background: #1a1a1a;
}
.tile img {
width: 100%; height: 100%;
object-fit: cover;
user-select: none;
display: block;
transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1), filter 0.4s ease;
filter: saturate(1.05);
}
.tile:hover img { transform: scale(1.08); filter: saturate(1.2) brightness(1.05); }
/* Positions (mimic the Frame.io-style scatter) */
.tile--tl-1 { top: 8%; left: -2%; width: 260px; height: 190px; transform: rotate(-1deg); }
.tile--tl-2 { top: 38%; left: -3%; width: 280px; height: 210px; transform: rotate(1deg); }
.tile--tc { top: 2%; left: 25%; width: 260px; height: 150px; transform: rotate(1deg); }
.tile--tr-1 { top: 0%; left: 60%; width: 230px; height: 170px; transform: rotate(-1.5deg); }
.tile--tr-2 { top: 10%; right: -2%; width: 280px; height: 190px; transform: rotate(1deg); }
.tile--r-1 { top: 40%; right: -1%; width: 240px; height: 210px; transform: rotate(-1deg); }
.tile--r-2 { top: 68%; right: 4%; width: 280px; height: 200px; transform: rotate(1deg); }
.tile--l-1 { top: 5%; left: 2%; width: 250px; height: 160px; display: none; }
.tile--l-2 { top: 68%; left: 4%; width: 280px; height: 200px; transform: rotate(-1deg); }
.tile--bl { bottom: 10%; left: -2%; width: 280px; height: 200px; transform: rotate(1deg); }
.tile--bc-1 { bottom: 2%; left: 28%; width: 240px; height: 140px; transform: rotate(-1deg); }
.tile--bc-2 { bottom: 6%; left: 46%; width: 220px; height: 140px; transform: rotate(1deg); }
.tile--bc-3 { bottom: 4%; left: 62%; width: 240px; height: 160px; transform: rotate(-1deg); }
.tile--br { bottom: 14%; right: -2%; width: 280px; height: 200px; transform: rotate(1deg); display: none; }
/* ---------- Center content ---------- */
.center {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
text-align: center;
max-width: 560px;
padding: 0 1.5rem;
pointer-events: none;
}
.center > * { pointer-events: auto; }
.center__title {
font-family: 'Inter', sans-serif;
font-weight: 700;
font-size: clamp(2.2rem, 5vw, 4.2rem);
line-height: 1.05;
letter-spacing: -0.025em;
color: var(--ink);
margin-bottom: 1.5rem;
}
.center__line {
display: block;
overflow: hidden;
padding-bottom: 0.05em;
}
.center__char {
display: inline-block;
will-change: transform, color;
transition: color 0.3s ease;
}
.center__char.is-space { width: 0.25em; }
.center__desc {
font-size: 1rem;
line-height: 1.6;
color: var(--muted);
font-weight: 400;
}
/* ---------- Initial hidden states ---------- */
[data-magnetic], [data-link], [data-tile], [data-desc], [data-topbar] { opacity: 0; }
[data-title-line] .center__char { opacity: 0; }
/* ---------- Responsive ---------- */
@media (max-width: 1100px) {
.topnav__links { display: none; }
.topnav__action { display: none; }
.topnav { gap: 1rem; }
.tile--tc, .tile--tr-1, .tile--r-2 { display: none; }
}
@media (max-width: 768px) {
.tile {
transform: none !important;
}
.tile--tl-2, .tile--r-2, .tile--bl, .tile--bc-3 { display: none; }
.tile--tl-1 { width: 40vw; height: 28vw; left: -8%; }
.tile--tr-2 { width: 42vw; height: 30vw; right: -6%; }
.tile--r-1 { width: 40vw; height: 30vw; right: -10%; top: 42%; }
.tile--l-2 { width: 40vw; height: 28vw; left: -8%; top: 62%; }
.tile--bc-1 { width: 40vw; height: 26vw; left: 8%; bottom: 4%; }
.tile--bc-2 { width: 40vw; height: 26vw; right: 8%; left: auto; bottom: 4%; }
.center { max-width: 90%; }
.center__desc { font-size: 0.9rem; }
}
@media (max-width: 480px) {
.tile--r-1, .tile--l-2 { display: none; }
.tile--tl-1 { top: 6rem; left: -8%; width: 45vw; height: 32vw; }
.tile--tr-2 { top: 6rem; right: -8%; width: 45vw; height: 32vw; }
.tile--bc-1, .tile--bc-2 { top: auto; bottom: 1rem; width: 45vw; height: 28vw; }
.tile--bc-1 { left: -5%; }
.tile--bc-2 { right: -5%; left: auto; }
.topnav { padding: 1rem; }
.topnav__cta { padding: 0.5rem 1rem; font-size: 0.8rem; }
.topnav__mark { width: 30px; height: 30px; }
}
</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 — Media Gallery</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&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="banner-media-gallery.css">
</head>
<body>
<!-- ========== LOADER ========== -->
<div class="loader" id="loader">
<div class="loader__wave" id="loader-wave"></div>
<div class="loader__meta">
<span class="loader__label" id="loader-label">INITIALIZING WORKSPACE</span>
<span class="loader__counter" id="loader-counter">00%</span>
</div>
</div>
<section class="mediagrid" id="mediagrid">
<!-- Top gradient accent -->
<div class="mediagrid__topbar" data-topbar></div>
<!-- ========== TOP NAV ========== -->
<header class="topnav">
<a href="#" class="topnav__logo" data-magnetic>
<span class="topnav__logo-mark" id="logo-mark">
<span></span><span></span><span></span><span></span><span></span><span></span>
</span>
</a>
<nav class="topnav__links">
<a href="#" data-magnetic data-link>Features <span class="caret">▾</span></a>
<a href="#" data-magnetic data-link>Enterprise <span class="caret">▾</span></a>
<a href="#" data-magnetic data-link>Resources <span class="caret">▾</span></a>
<a href="#" data-magnetic data-link>Pricing</a>
</nav>
<div class="topnav__actions">
<a href="#" class="topnav__action" data-magnetic>Contact Us</a>
<a href="#" class="topnav__action" data-magnetic>Sign In</a>
<a href="#" class="topnav__cta" data-magnetic>Start Free Trial</a>
<a href="#" class="topnav__mark" data-magnetic aria-label="Brand mark">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3L2 21h5l1.5-3h7L17 21h5L12 3zm-2.5 13L12 10l2.5 6h-5z"/></svg>
</a>
</div>
</header>
<!-- ========== IMAGE GRID (scattered around edges) ========== -->
<div class="grid" id="grid">
<!-- Top row -->
<div class="tile tile--tl-1" data-tile data-depth="0.4">
<img src="https://images.unsplash.com/photo-1517694712202-14dd9538aa97?auto=format&fit=crop&w=500&q=75"
alt="" draggable="false" onerror="this.parentElement.style.display='none'">
</div>
<div class="tile tile--tl-2" data-tile data-depth="0.3">
<img src="https://images.unsplash.com/photo-1536440136628-849c177e76a1?auto=format&fit=crop&w=500&q=75"
alt="" draggable="false" onerror="this.parentElement.style.display='none'">
</div>
<div class="tile tile--tc" data-tile data-depth="0.5">
<img src="https://images.unsplash.com/photo-1492691527719-9d1e07e534b4?auto=format&fit=crop&w=500&q=75"
alt="" draggable="false" onerror="this.parentElement.style.display='none'">
</div>
<div class="tile tile--tr-1" data-tile data-depth="0.35">
<img src="https://images.unsplash.com/photo-1449247709967-d4461a6a6103?auto=format&fit=crop&w=500&q=75"
alt="" draggable="false" onerror="this.parentElement.style.display='none'">
</div>
<div class="tile tile--tr-2" data-tile data-depth="0.45">
<img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=500&q=75"
alt="" draggable="false" onerror="this.parentElement.style.display='none'">
</div>
<!-- Right column -->
<div class="tile tile--r-1" data-tile data-depth="0.3">
<img src="https://images.unsplash.com/photo-1517694712202-14dd9538aa97?auto=format&fit=crop&w=500&q=75"
alt="" draggable="false" onerror="this.parentElement.style.display='none'">
</div>
<div class="tile tile--r-2" data-tile data-depth="0.5">
<img src="https://images.unsplash.com/photo-1536440136628-849c177e76a1?auto=format&fit=crop&w=500&q=75"
alt="" draggable="false" onerror="this.parentElement.style.display='none'">
</div>
<!-- Left column -->
<div class="tile tile--l-1" data-tile data-depth="0.35">
<img src="https://images.unsplash.com/photo-1517694712202-14dd9538aa97?auto=format&fit=crop&w=500&q=75"
alt="" draggable="false" onerror="this.parentElement.style.display='none'">
</div>
<div class="tile tile--l-2" data-tile data-depth="0.4">
<img src="https://images.unsplash.com/photo-1536440136628-849c177e76a1?auto=format&fit=crop&w=500&q=75"
alt="" draggable="false" onerror="this.parentElement.style.display='none'">
</div>
<!-- Bottom row -->
<div class="tile tile--bl" data-tile data-depth="0.4">
<img src="https://images.unsplash.com/photo-1519225421980-715cb0215aed?auto=format&fit=crop&w=500&q=75"
alt="" draggable="false" onerror="this.parentElement.style.display='none'">
</div>
<div class="tile tile--bc-1" data-tile data-depth="0.3">
<img src="https://images.unsplash.com/photo-1436491865332-7a61a109cc05?auto=format&fit=crop&w=500&q=75"
alt="" draggable="false" onerror="this.parentElement.style.display='none'">
</div>
<div class="tile tile--bc-2" data-tile data-depth="0.35">
<img src="https://images.unsplash.com/photo-1489599192825-a7fc2e3fbfbd?auto=format&fit=crop&w=500&q=75"
alt="" draggable="false" onerror="this.parentElement.style.display='none'">
</div>
<div class="tile tile--bc-3" data-tile data-depth="0.45">
<img src="https://images.unsplash.com/photo-1474511320723-9a56873867b5?auto=format&fit=crop&w=500&q=75"
alt="" draggable="false" onerror="this.parentElement.style.display='none'">
</div>
<div class="tile tile--br" data-tile data-depth="0.5">
<img src="https://images.unsplash.com/photo-1536440136628-849c177e76a1?auto=format&fit=crop&w=500&q=75"
alt="" draggable="false" onerror="this.parentElement.style.display='none'">
</div>
</div>
<!-- ========== CENTER CONTENT ========== -->
<div class="center">
<h1 class="center__title" aria-label="Collaborate and conquer.">
<span class="center__line" data-title-line>Collaborate</span>
<span class="center__line" data-title-line>and conquer.</span>
</h1>
<p class="center__desc" data-desc>
See how brands, agencies, and production teams of all sizes
worldwide use our platform to accelerate their workflow and
empower collaboration.
</p>
</div>
</section>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="banner-media-gallery.js"></script>
</body>
</html>
</body>
</html>JS banner-media-gallery.js — GSAP animation timeline
(() => {
const root = document.getElementById('mediagrid');
if (!root) return;
// -------------------------------------------------------------------
// BUILD WAVEFORM BARS IN LOADER
// -------------------------------------------------------------------
const wave = document.getElementById('loader-wave');
const BAR_COUNT = 28;
for (let i = 0; i < BAR_COUNT; i++) {
wave.appendChild(document.createElement('span'));
}
const waveBars = wave.querySelectorAll('span');
// -------------------------------------------------------------------
// SPLIT TITLE LINES 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 = 'center__char';
if (ch === ' ') {
span.classList.add('is-space');
span.innerHTML = ' ';
} else {
span.textContent = ch;
}
line.appendChild(span);
});
});
// -------------------------------------------------------------------
// REFERENCES
// -------------------------------------------------------------------
const loader = document.getElementById('loader');
const loaderCounter = document.getElementById('loader-counter');
const loaderLabel = document.getElementById('loader-label');
const topbar = root.querySelector('[data-topbar]');
const magnetics = root.querySelectorAll('[data-magnetic]');
const tiles = root.querySelectorAll('[data-tile]');
const titleChars = root.querySelectorAll('.center__char');
const desc = root.querySelector('[data-desc]');
const logoMark = document.getElementById('logo-mark');
const logoBars = logoMark.querySelectorAll('span');
// -------------------------------------------------------------------
// INITIAL STATES
// -------------------------------------------------------------------
gsap.set(topbar, { scaleX: 0, opacity: 1, transformOrigin: 'left' });
gsap.set(magnetics, { y: -20, opacity: 0 });
tiles.forEach((tile, i) => {
// Remember the final CSS rotation so parallax can preserve it
const baseRot = getRotationFromTransform(getComputedStyle(tile).transform);
tile.dataset.baseRot = baseRot;
gsap.set(tile, {
opacity: 0,
scale: 0.7,
y: 40,
rotation: baseRot,
});
});
gsap.set(titleChars, { yPercent: 110, opacity: 0 });
gsap.set(desc, { y: 20, opacity: 0 });
// -------------------------------------------------------------------
// LOADER TIMELINE
// -------------------------------------------------------------------
const loaderTl = gsap.timeline({ onComplete: playScene });
// Kick off a continuous waveform animation on bars (independent heights)
const waveTweens = [];
waveBars.forEach((bar, i) => {
const tween = gsap.to(bar, {
height: () => gsap.utils.random(18, 110),
duration: () => gsap.utils.random(0.25, 0.55),
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
delay: i * 0.03,
});
waveTweens.push(tween);
});
// Counter 0 -> 100
const p = { v: 0 };
loaderTl.to(p, {
v: 100,
duration: 2.2,
ease: 'power1.inOut',
onUpdate: () => {
loaderCounter.textContent = String(Math.floor(p.v)).padStart(2, '0') + '%';
},
});
loaderTl.call(() => {
loaderLabel.textContent = 'WORKSPACE READY';
});
// Freeze waveform on final beat (stop the yoyo loops)
loaderTl.call(() => {
waveTweens.forEach((t) => t.pause());
}, null, '+=0.1');
// Bars collapse to thin line then scatter outward
loaderTl.to(waveBars, {
height: 4,
duration: 0.4,
ease: 'power3.inOut',
}, '+=0.05');
loaderTl.to(waveBars, {
y: (i) => (i % 2 === 0 ? -1 : 1) * gsap.utils.random(100, 300),
x: (i) => (i - BAR_COUNT / 2) * gsap.utils.random(8, 20),
opacity: 0,
rotation: () => gsap.utils.random(-60, 60),
duration: 0.9,
ease: 'power3.in',
stagger: 0.008,
}, '+=0.1');
loaderTl.to([loaderLabel, loaderCounter], {
opacity: 0,
y: -10,
duration: 0.35,
ease: 'power2.in',
stagger: 0.05,
}, '-=0.7');
loaderTl.to(loader, {
opacity: 0,
duration: 0.5,
ease: 'power2.out',
}, '-=0.2');
loaderTl.set(loader, { display: 'none' });
// -------------------------------------------------------------------
// MAIN SCENE ENTRANCE
// -------------------------------------------------------------------
function playScene() {
const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
// Gradient topbar draws across
tl.to(topbar, {
scaleX: 1,
duration: 1.1,
ease: 'power4.inOut',
}, 0);
// Nav elements
tl.to(magnetics, {
y: 0, opacity: 1,
duration: 0.6,
stagger: 0.05,
}, 0.2);
// Tiles: scale + slide in from random offsets
tl.to(tiles, {
opacity: 1,
scale: 1,
y: 0,
rotation: (i, el) => parseFloat(el.dataset.baseRot) || 0,
duration: 1.2,
stagger: {
each: 0.06,
from: 'random',
},
ease: 'power4.out',
}, 0.35);
// Title chars reveal
tl.to(titleChars, {
yPercent: 0, opacity: 1,
duration: 1,
stagger: 0.025,
ease: 'expo.out',
}, 0.9);
// Description fades up
tl.to(desc, {
y: 0, opacity: 1,
duration: 0.8,
}, 1.4);
tl.call(enableInteractions, null, 1.6);
}
// -------------------------------------------------------------------
// INTERACTIVE BEHAVIORS
// -------------------------------------------------------------------
function enableInteractions() {
// ---- Magnetic elements ----
magnetics.forEach((el) => {
const strength = el.classList.contains('topnav__cta') ? 0.35
: el.classList.contains('topnav__mark') ? 0.4
: el.classList.contains('topnav__logo') ? 0.25
: 0.22;
el.addEventListener('mousemove', (e) => {
const r = el.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const dx = (e.clientX - cx) * strength;
const dy = (e.clientY - cy) * strength;
gsap.to(el, { x: dx, y: dy, 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 waveform pulse on hover ----
const logoEl = root.querySelector('.topnav__logo');
logoEl.addEventListener('mouseenter', () => {
gsap.to(logoBars, {
scaleY: () => gsap.utils.random(0.4, 1.8),
duration: 0.3,
yoyo: true,
repeat: 3,
stagger: { each: 0.05, from: 'random' },
ease: 'sine.inOut',
transformOrigin: 'center',
});
});
// ---- Tile hover: bring forward with scale + deeper shadow ----
tiles.forEach((tile) => {
const baseRot = parseFloat(tile.dataset.baseRot) || 0;
tile.addEventListener('mouseenter', () => {
gsap.to(tile, {
scale: 1.08,
rotation: baseRot * 0.3,
zIndex: 20,
boxShadow: '0 40px 80px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.15)',
duration: 0.5,
ease: 'power3.out',
});
});
tile.addEventListener('mouseleave', () => {
gsap.to(tile, {
scale: 1,
rotation: baseRot,
zIndex: 5,
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.55)',
duration: 0.6,
ease: 'power3.out',
});
});
});
// ---- Title char proximity + hover ----
const center = document.querySelector('.center');
let mx = -9999, my = -9999;
center.addEventListener('mousemove', (e) => {
mx = e.clientX; my = e.clientY;
});
center.addEventListener('mouseleave', () => {
mx = -9999; my = -9999;
titleChars.forEach((c) => {
gsap.to(c, { y: 0, color: '#ffffff', duration: 0.5, ease: 'power3.out' });
});
});
gsap.ticker.add(() => {
if (mx < 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 = mx - cx;
const dy = my - cy;
const dist = Math.sqrt(dx * dx + dy * dy);
const PROX = 150;
if (dist < PROX) {
const s = 1 - dist / PROX;
gsap.set(c, { y: -s * 18 });
} else {
gsap.set(c, { y: 0 });
}
});
});
titleChars.forEach((c) => {
c.addEventListener('mouseenter', () => {
gsap.to(c, { color: '#c9bf98', duration: 0.2 });
});
c.addEventListener('mouseleave', () => {
gsap.to(c, { color: '#ffffff', duration: 0.4 });
});
});
// ---- Tile parallax with per-tile depth ----
let tX = 0, tY = 0, cX = 0, cY = 0;
root.addEventListener('mousemove', (e) => {
const r = root.getBoundingClientRect();
tX = ((e.clientX - r.left) / r.width - 0.5) * 2;
tY = ((e.clientY - r.top) / r.height - 0.5) * 2;
});
root.addEventListener('mouseleave', () => { tX = 0; tY = 0; });
gsap.ticker.add(() => {
cX += (tX - cX) * 0.06;
cY += (tY - cY) * 0.06;
tiles.forEach((tile) => {
// Don't override hover transform
if (tile.matches(':hover')) return;
const depth = parseFloat(tile.dataset.depth) || 0.3;
const baseRot = parseFloat(tile.dataset.baseRot) || 0;
gsap.set(tile, {
x: -cX * 40 * depth,
y: -cY * 28 * depth,
rotation: baseRot + cX * 0.8 * depth,
});
});
});
}
// Helper: parse rotation from CSS matrix transform
function getRotationFromTransform(transform) {
if (!transform || transform === 'none') return 0;
const match = transform.match(/matrix\(([^)]+)\)/);
if (!match) return 0;
const values = match[1].split(',').map(parseFloat);
const angle = Math.atan2(values[1], values[0]) * (180 / Math.PI);
return angle;
}
})();