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 Designer Portfolio Hero Template with Vinyl Loader</title>
<meta name="description" content="Free GSAP designer portfolio landing page hero from ToolsWaves" />
<meta name="generator" content="ToolsWaves - https://toolswaves.in/landing-pages" />
<!-- Open Graph -->
<meta property="og:title" content="Free Designer Portfolio Hero Template with Vinyl Loader" />
<meta property="og:description" content="Free GSAP designer portfolio landing page hero from ToolsWaves" />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://toolswaves.in/og?title=Free%20Designer%20Portfolio%20Hero%20Template%20with%20Vinyl%20Loader&category=Landing%20Page&icon=%F0%9F%93%84" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Free Designer Portfolio Hero Template with Vinyl Loader" />
<meta name="twitter:description" content="Free GSAP designer portfolio landing page hero from ToolsWaves" />
<meta name="twitter:image" content="https://toolswaves.in/og?title=Free%20Designer%20Portfolio%20Hero%20Template%20with%20Vinyl%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: #0a0a0a;
--ink: #ffffff;
--muted: rgba(255, 255, 255, 0.55);
--line: rgba(255, 255, 255, 0.12);
--lime: #c9ff2d;
--lime-deep: #a8dd15;
--accent: #c9ff2d;
}
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: 4.5rem;
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, background 0.3s;
}
.back-link:hover { opacity: 1; transform: translateY(-2px); background: rgba(255, 255, 255, 0.14); }
/* =========================================================
LOADER — rotating vinyl with counter
========================================================= */
.loader {
position: fixed;
inset: 0;
z-index: 999;
background: var(--bg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1.5rem;
}
.loader__stage {
position: relative;
width: min(320px, 55vw);
height: min(320px, 55vw);
display: flex;
align-items: center;
justify-content: center;
}
.loader__vinyl {
width: 100%;
height: 100%;
filter: drop-shadow(0 20px 60px rgba(201, 255, 45, 0.15));
}
.loader__counter {
position: absolute;
font-family: 'JetBrains Mono', monospace;
font-size: 1rem;
font-weight: 600;
color: var(--bg);
letter-spacing: 0.2em;
font-variant-numeric: tabular-nums;
z-index: 2;
}
.loader__label {
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
letter-spacing: 0.3em;
color: var(--muted);
text-transform: uppercase;
}
/* =========================================================
PORTFOLIO SECTION
========================================================= */
.portfolio {
position: relative;
width: 100%;
height: 100vh;
min-height: 780px;
overflow: hidden;
background: var(--bg);
}
/* ---------- Top nav ---------- */
.topnav {
position: relative;
z-index: 20;
display: flex;
align-items: center;
gap: 2rem;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--line);
background: rgba(10, 10, 10, 0.65);
backdrop-filter: blur(8px);
}
.topnav__logo {
display: inline-flex;
align-items: center;
gap: 0.6rem;
color: var(--ink);
text-decoration: none;
font-weight: 700;
font-size: 1.35rem;
letter-spacing: -0.01em;
will-change: transform;
}
.topnav__logo-mark { width: 36px; height: 36px; flex-shrink: 0; }
.topnav__logo-mark svg { width: 100%; height: 100%; display: block; }
.topnav__links {
margin: 0 auto;
display: flex;
gap: 2.5rem;
}
.topnav__links a {
position: relative;
color: var(--ink);
text-decoration: none;
font-weight: 600;
font-size: 0.85rem;
letter-spacing: 0.1em;
padding: 0.3rem 0;
will-change: transform;
}
.topnav__links a.is-active { color: var(--lime); }
.topnav__links a::after {
content: '';
position: absolute;
left: 0; bottom: -2px;
width: 100%; height: 1px;
background: var(--lime);
transform: scaleX(0);
transform-origin: right;
transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.topnav__links a.is-active::after { transform: scaleX(1); transform-origin: left; }
.topnav__links a:hover::after { transform: scaleX(1); transform-origin: left; }
.topnav__actions {
display: flex;
align-items: center;
gap: 0.75rem;
}
.topnav__cta {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: var(--lime);
color: var(--bg);
text-decoration: none;
padding: 0.75rem 1.2rem;
border-radius: 999px;
font-weight: 600;
font-size: 0.9rem;
will-change: transform;
transition: background 0.3s;
}
.topnav__cta svg { width: 16px; height: 16px; transition: transform 0.3s ease; }
.topnav__cta:hover { background: var(--lime-deep); }
.topnav__cta:hover svg { transform: translateX(3px); }
.topnav__menu {
width: 44px; height: 44px;
background: var(--ink);
border: none;
border-radius: 8px;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
padding: 0;
will-change: transform;
}
.topnav__menu span {
width: 18px;
height: 2px;
background: var(--bg);
border-radius: 2px;
transition: all 0.3s ease;
}
.topnav__menu:hover span:nth-child(1) { transform: translateY(1px); }
.topnav__menu:hover span:nth-child(3) { transform: translateY(-1px); }
/* ---------- Left rail ---------- */
.rail {
position: absolute;
z-index: 15;
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
}
.rail > * { pointer-events: auto; }
.rail--left {
top: 7rem; left: 0.5rem; bottom: 5rem;
gap: 2rem;
}
.rail--right {
top: 7rem; right: 0.75rem; bottom: 5rem;
gap: 1.5rem;
align-items: center;
}
.rail__dots {
display: grid;
grid-template-columns: repeat(3, 6px);
gap: 4px;
}
.rail__dots span {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--ink);
cursor: pointer;
will-change: transform;
transition: background 0.3s ease;
}
.rail__dots span:hover { background: var(--lime); }
.rail__phone {
writing-mode: vertical-rl;
transform: rotate(180deg);
color: var(--ink);
text-decoration: none;
font-size: 0.75rem;
letter-spacing: 0.25em;
font-family: 'JetBrains Mono', monospace;
transition: color 0.3s ease;
}
.rail__phone:hover { color: var(--lime); }
.rail__scroll {
margin-top: auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
font-size: 0.65rem;
letter-spacing: 0.35em;
color: var(--muted);
}
.rail__scroll-label {
writing-mode: vertical-rl;
transform: rotate(180deg);
}
.rail__scroll-line {
width: 1px;
height: 80px;
background: linear-gradient(180deg, rgba(255,255,255,0.4), transparent);
display: block;
transform-origin: top;
}
.rail__scroll-arrow { font-size: 0.8rem; opacity: 0.6; }
.rail__follow {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
font-size: 0.65rem;
letter-spacing: 0.35em;
color: var(--muted);
}
.rail__follow-label {
writing-mode: vertical-rl;
transform: rotate(180deg);
}
.rail__follow-line {
width: 1px;
height: 70px;
background: linear-gradient(180deg, rgba(255,255,255,0.4), transparent);
display: block;
}
.rail__follow-arrow { font-size: 0.8rem; opacity: 0.6; }
.rail__social {
display: flex;
flex-direction: column;
gap: 0.65rem;
margin-top: auto;
align-items: center;
}
.rail__social-icon {
width: 34px; height: 34px;
border-radius: 50%;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.12);
color: var(--ink);
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
font-size: 0.75rem;
font-weight: 700;
will-change: transform;
transition: background 0.3s ease, color 0.3s ease;
}
.rail__social-icon svg { width: 15px; height: 15px; }
.rail__social-icon:hover { background: var(--lime); color: var(--bg); }
/* ---------- Stage (vinyl + portrait) ---------- */
.stage {
position: absolute;
inset: 0;
z-index: 5;
pointer-events: none;
overflow: hidden;
}
.stage__vinyl {
position: absolute;
top: 45%;
left: 58%;
transform: translate(-50%, -50%);
width: min(820px, 85vw);
height: min(820px, 85vw);
opacity: 0.95;
will-change: transform;
}
.stage__portrait {
position: absolute;
top: 46%;
left: 66%;
transform: translate(-50%, -50%);
width: min(480px, 45vw);
height: min(620px, 80vh);
will-change: transform;
mask-image: radial-gradient(ellipse at center, #000 55%, transparent 95%);
-webkit-mask-image: radial-gradient(ellipse at center, #000 55%, transparent 95%);
}
.stage__portrait img {
width: 100%; height: 100%;
object-fit: cover;
object-position: center top;
user-select: none;
filter: contrast(1.05) saturate(0.9);
}
/* ---------- Main hero content ---------- */
.hero {
position: absolute;
left: 5rem;
top: 7rem;
right: 4rem;
bottom: 5rem;
z-index: 8;
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 1.5rem;
pointer-events: none;
}
.hero > * { pointer-events: auto; }
.hero__eyebrow {
position: relative;
display: inline-flex;
align-items: flex-start;
gap: 0.4rem;
color: var(--ink);
text-decoration: none;
font-size: 0.95rem;
font-weight: 500;
line-height: 1.4;
padding-bottom: 0.4rem;
max-width: max-content;
will-change: transform;
}
.hero__eyebrow-text {
border-bottom: 1px solid rgba(255, 255, 255, 0.4);
padding-bottom: 0.2rem;
}
.hero__eyebrow-arrow {
display: inline-block;
font-size: 0.85em;
transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.hero__eyebrow:hover .hero__eyebrow-arrow {
transform: translate(4px, -4px) rotate(3deg);
}
.hero__title {
font-family: 'Inter', sans-serif;
font-weight: 800;
font-size: clamp(3rem, 9vw, 8rem);
line-height: 0.95;
letter-spacing: -0.04em;
color: var(--ink);
margin: auto 0;
max-width: 780px;
}
.hero__line {
display: block;
overflow: hidden;
padding-bottom: 0.05em;
}
.hero__char {
display: inline-block;
will-change: transform, color;
transition: color 0.3s ease;
}
.hero__char.is-space { width: 0.25em; }
.hero__play {
display: inline-flex;
align-items: center;
gap: 1rem;
text-decoration: none;
color: var(--ink);
margin-top: auto;
max-width: max-content;
will-change: transform;
}
.hero__play-scribble { width: 70px; height: 52px; }
.hero__play-scribble path {
stroke-dasharray: 120;
stroke-dashoffset: 120;
}
.hero__play-btn {
position: relative;
width: 62px;
height: 62px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.hero__play-btn > svg:first-child {
width: 100%; height: 100%;
transition: transform 0.3s ease;
}
.hero__play-ring {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
will-change: transform;
}
.hero__play:hover .hero__play-btn > svg:first-child {
transform: scale(1.08);
}
.hero__play-label {
display: flex;
flex-direction: column;
font-weight: 500;
font-size: 1rem;
line-height: 1.1;
}
/* ---------- Marquee ---------- */
.marquee {
position: absolute;
left: 0; right: 0; bottom: 0;
z-index: 10;
background: var(--lime);
color: var(--bg);
padding: 1.1rem 0;
overflow: hidden;
white-space: nowrap;
}
.marquee__track {
display: flex;
gap: 0;
width: max-content;
will-change: transform;
}
.marquee__group {
display: flex;
gap: 3rem;
padding: 0 1.5rem;
align-items: center;
}
.marquee__item {
font-size: clamp(1rem, 1.7vw, 1.5rem);
font-weight: 700;
letter-spacing: -0.01em;
white-space: nowrap;
cursor: default;
}
/* ---------- Initial hidden states ---------- */
[data-magnetic], [data-rail], [data-social], [data-eyebrow],
[data-title-line] .hero__char, [data-play], [data-portrait] {
opacity: 0;
}
/* ---------- Responsive ---------- */
@media (max-width: 1100px) {
.topnav__links { display: none; }
.hero { left: 4rem; right: 3rem; top: 6rem; }
.rail--left { left: 0.25rem; }
.rail--right { right: 0.25rem; }
.stage__portrait { left: 72%; width: 55vw; }
.stage__vinyl { left: 62%; width: 90vw; height: 90vw; }
}
@media (max-width: 768px) {
.rail--right .rail__follow { display: none; }
.rail--left .rail__phone { display: none; }
.rail__social { gap: 0.4rem; }
.rail__social-icon { width: 28px; height: 28px; }
.hero { left: 3rem; right: 3rem; top: 5rem; bottom: 4rem; }
.hero__title { font-size: clamp(2.3rem, 11vw, 6rem); }
.stage__portrait { left: 55%; width: 75vw; top: 50%; opacity: 0.35; }
.stage__vinyl { left: 50%; width: 110vw; height: 110vw; opacity: 0.7; }
.topnav__menu { width: 38px; height: 38px; }
}
@media (max-width: 480px) {
.rail { gap: 1rem; }
.rail--left { top: 5rem; }
.rail--right { top: 5rem; }
.rail__scroll, .rail__follow { display: none; }
.rail__dots { display: none; }
.topnav { padding: 0.75rem 1rem; gap: 0.75rem; }
.topnav__logo-text { display: none; }
.topnav__cta { padding: 0.5rem 0.9rem; font-size: 0.8rem; }
.hero__eyebrow { font-size: 0.85rem; }
}
</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 — Portfolio Dark</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@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="banner-portfolio-dark.css">
</head>
<body>
<!-- ========== LOADER ========== -->
<div class="loader" id="loader">
<div class="loader__stage">
<svg class="loader__vinyl" id="loader-vinyl" viewBox="0 0 400 400" aria-hidden="true">
<g id="loader-rings"></g>
<circle cx="200" cy="200" r="62" fill="#c9ff2d"/>
<circle cx="200" cy="200" r="62" fill="none" stroke="#0a0a0a" stroke-width="1" stroke-dasharray="2 4"/>
<circle cx="200" cy="200" r="7" fill="#0a0a0a"/>
</svg>
<div class="loader__counter" id="loader-counter">000</div>
</div>
<div class="loader__label" id="loader-label">LOADING SESSION</div>
</div>
<section class="portfolio" id="portfolio">
<!-- ========== TOP NAV ========== -->
<header class="topnav">
<a href="#" class="topnav__logo" data-magnetic>
<span class="topnav__logo-mark">
<svg viewBox="0 0 40 40">
<circle cx="20" cy="20" r="20" fill="#c9ff2d"/>
<path d="M20 8v24M8 20h24M12 12l16 16M28 12L12 28" stroke="#0a0a0a" stroke-width="1.5"/>
</svg>
</span>
<span class="topnav__logo-text">Brand</span>
</a>
<nav class="topnav__links">
<a href="#" class="is-active" data-magnetic data-link>Home</a>
<a href="#" data-magnetic data-link>About</a>
<a href="#" data-magnetic data-link>Works</a>
<a href="#" data-magnetic data-link>Services</a>
<a href="#" data-magnetic data-link>Testimonial</a>
<a href="#" data-magnetic data-link>Blog</a>
<a href="#" data-magnetic data-link>Contact</a>
</nav>
<div class="topnav__actions">
<a href="#" class="topnav__cta" data-magnetic>
<span>Let's Talk</span>
<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>
</a>
<button class="topnav__menu" data-magnetic aria-label="Menu">
<span></span><span></span><span></span>
</button>
</div>
</header>
<!-- ========== LEFT RAIL ========== -->
<aside class="rail rail--left">
<div class="rail__dots" data-rail>
<span></span><span></span><span></span>
<span></span><span></span><span></span>
<span></span><span></span><span></span>
</div>
<a href="#" class="rail__phone" data-rail>+(02)-574-328-301</a>
<div class="rail__scroll" data-rail>
<span class="rail__scroll-label">SCROLL DOWN</span>
<span class="rail__scroll-line"></span>
<span class="rail__scroll-arrow">↓</span>
</div>
</aside>
<!-- ========== RIGHT RAIL ========== -->
<aside class="rail rail--right">
<div class="rail__follow" data-rail>
<span class="rail__follow-label">FOLLOW ME</span>
<span class="rail__follow-line"></span>
<span class="rail__follow-arrow">↓</span>
</div>
<div class="rail__social" data-rail>
<a href="#" class="rail__social-icon" data-social aria-label="Facebook">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M13.5 21v-8h2.7l.4-3h-3.1V8c0-.9.3-1.5 1.5-1.5h1.6V3.8c-.3 0-1.2-.1-2.3-.1-2.3 0-3.9 1.4-3.9 4V10H7.8v3h2.5v8h3.2z"/></svg>
</a>
<a href="#" class="rail__social-icon" data-social aria-label="Twitter">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.9 7.3c0 .2 0 .4 0 .5 0 5.4-4.1 11.6-11.6 11.6C5 19.5 2.8 18.8 1 17.6c.3 0 .6.1 1 .1 1.9 0 3.7-.7 5.1-1.8-1.8 0-3.3-1.2-3.9-2.9.3 0 .5.1.8.1.4 0 .7 0 1.1-.1-1.9-.4-3.4-2.1-3.4-4.2v-.1c.6.3 1.2.5 1.9.5C2.4 8.4 1.6 7 1.6 5.5c0-.8.2-1.5.6-2.1 2.1 2.5 5.1 4.1 8.6 4.3-.1-.3-.1-.6-.1-.9 0-2.3 1.8-4.1 4.1-4.1 1.2 0 2.3.5 3 1.3.9-.2 1.8-.5 2.6-1-.3 1-1 1.8-1.8 2.3.8-.1 1.6-.3 2.4-.6-.6.8-1.3 1.5-2.1 2.1z"/></svg>
</a>
<a href="#" class="rail__social-icon" data-social aria-label="LinkedIn">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.5 3h-15C3.7 3 3 3.7 3 4.5v15c0 .8.7 1.5 1.5 1.5h15c.8 0 1.5-.7 1.5-1.5v-15c0-.8-.7-1.5-1.5-1.5zM8.3 18H5.7V9.8h2.6V18zM7 8.6c-.8 0-1.5-.7-1.5-1.5S6.2 5.6 7 5.6s1.5.7 1.5 1.5S7.8 8.6 7 8.6zM18.3 18h-2.6v-4.2c0-1 0-2.3-1.4-2.3s-1.6 1.1-1.6 2.2V18H10.1V9.8h2.5v1.1h.1c.3-.7 1.2-1.4 2.5-1.4 2.7 0 3.2 1.8 3.2 4V18z"/></svg>
</a>
<a href="#" class="rail__social-icon" data-social aria-label="Behance">
<span>Be</span>
</a>
<a href="#" class="rail__social-icon" data-social aria-label="Dribbble">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<circle cx="12" cy="12" r="9"/>
<path d="M3 12c6 0 13 1 18 7M5 6c4 2 11 8 14 15M20 6c-4 4-12 6-17 4"/>
</svg>
</a>
</div>
</aside>
<!-- ========== CENTER VINYL + PORTRAIT ========== -->
<div class="stage" id="stage">
<svg class="stage__vinyl" id="stage-vinyl" viewBox="0 0 800 800" aria-hidden="true">
<defs>
<radialGradient id="vinylGrad" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#202022"/>
<stop offset="100%" stop-color="#0a0a0a"/>
</radialGradient>
</defs>
<circle cx="400" cy="400" r="380" fill="url(#vinylGrad)"/>
<g id="stage-rings" fill="none"></g>
<circle cx="400" cy="400" r="40" fill="#0a0a0a" stroke="#2a2a2a" stroke-width="1"/>
</svg>
<div class="stage__portrait" id="portrait" data-portrait>
<img src="https://images.unsplash.com/photo-1568602471122-7832951cc4c5?auto=format&fit=crop&w=900&q=80"
alt="Portrait"
draggable="false"
onerror="this.style.display='none'">
</div>
</div>
<!-- ========== MAIN CONTENT ========== -->
<div class="hero" id="hero">
<a href="#" class="hero__eyebrow" data-magnetic data-eyebrow>
<span class="hero__eyebrow-text">Currently available for freelance<br>worldwide</span>
<span class="hero__eyebrow-arrow">↗</span>
</a>
<h1 class="hero__title" aria-label="Creative Visual Designer">
<span class="hero__line" data-title-line>Creative Visual</span>
<span class="hero__line" data-title-line>Designer</span>
</h1>
<a href="#" class="hero__play" data-play>
<svg class="hero__play-scribble" viewBox="0 0 80 60" aria-hidden="true">
<path d="M10 30 Q 20 10, 40 20 T 70 35" stroke="#c9ff2d" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M65 30 L 70 35 L 65 40" stroke="#c9ff2d" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="hero__play-btn">
<svg viewBox="0 0 32 32" fill="none">
<circle cx="16" cy="16" r="15" stroke="#ffffff" stroke-width="1" fill="none"/>
<path d="M13 11l9 5-9 5z" fill="#ffffff"/>
</svg>
<svg class="hero__play-ring" viewBox="0 0 32 32" fill="none" aria-hidden="true">
<circle cx="16" cy="16" r="15.5" stroke="#c9ff2d" stroke-width="0.7" stroke-dasharray="4 3"/>
</svg>
</span>
<span class="hero__play-label">
<span>Work</span>
<span>Process</span>
</span>
</a>
</div>
<!-- ========== MARQUEE ========== -->
<div class="marquee" id="marquee">
<div class="marquee__track" id="marquee-track">
<div class="marquee__group">
<span class="marquee__item">✴ Website Design & Logo</span>
<span class="marquee__item">✴ Business Branding</span>
<span class="marquee__item">✴ Mobile Application Design</span>
<span class="marquee__item">✴ UI/UX Mobile Design</span>
<span class="marquee__item">✴ Motion & Interaction</span>
</div>
<div class="marquee__group" aria-hidden="true">
<span class="marquee__item">✴ Website Design & Logo</span>
<span class="marquee__item">✴ Business Branding</span>
<span class="marquee__item">✴ Mobile Application Design</span>
<span class="marquee__item">✴ UI/UX Mobile Design</span>
<span class="marquee__item">✴ Motion & Interaction</span>
</div>
</div>
</div>
</section>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="banner-portfolio-dark.js"></script>
</body>
</html>
</body>
</html>JS banner-portfolio-dark.js — GSAP animation timeline
(() => {
const root = document.getElementById('portfolio');
if (!root) return;
// -------------------------------------------------------------------
// GENERATE VINYL CONCENTRIC RINGS (both loader + stage)
// -------------------------------------------------------------------
const loaderRingsG = document.getElementById('loader-rings');
const stageRingsG = document.getElementById('stage-rings');
// Loader vinyl: rings from 70 to 195
for (let r = 70; r <= 195; r += 3) {
const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
c.setAttribute('cx', '200');
c.setAttribute('cy', '200');
c.setAttribute('r', r);
c.setAttribute('fill', 'none');
c.setAttribute('stroke', r % 12 === 0 ? '#c9ff2d' : '#1a1a1a');
c.setAttribute('stroke-width', r % 12 === 0 ? '0.4' : (r % 6 === 0 ? '0.8' : '0.3'));
loaderRingsG.appendChild(c);
}
// Stage vinyl: rings from 50 to 380
for (let r = 50; r <= 380; r += 4) {
const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
c.setAttribute('cx', '400');
c.setAttribute('cy', '400');
c.setAttribute('r', r);
c.setAttribute('fill', 'none');
c.setAttribute('stroke', '#1f1f20');
c.setAttribute('stroke-width', r % 20 === 0 ? '1.2' : '0.4');
c.setAttribute('opacity', r % 20 === 0 ? '0.7' : '0.35');
stageRingsG.appendChild(c);
}
// -------------------------------------------------------------------
// 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 loaderVinyl = document.getElementById('loader-vinyl');
const loaderCounter = document.getElementById('loader-counter');
const loaderLabel = document.getElementById('loader-label');
const stageVinyl = document.getElementById('stage-vinyl');
const portrait = document.getElementById('portrait');
const magnetics = root.querySelectorAll('[data-magnetic]');
const rails = root.querySelectorAll('[data-rail]');
const socials = root.querySelectorAll('[data-social]');
const eyebrow = root.querySelector('[data-eyebrow]');
const titleChars = root.querySelectorAll('.hero__char');
const playBtn = root.querySelector('[data-play]');
const playRing = root.querySelector('.hero__play-ring');
const playScribble = root.querySelector('.hero__play-scribble path');
const marqueeTrack = document.getElementById('marquee-track');
const dots = root.querySelectorAll('.rail__dots span');
// -------------------------------------------------------------------
// INITIAL STATES
// -------------------------------------------------------------------
gsap.set(magnetics, { y: -20, opacity: 0 });
gsap.set(rails, { x: (i, el) => el.closest('.rail--left') ? -30 : 30, opacity: 0 });
gsap.set(socials, { x: 30, opacity: 0 });
gsap.set(eyebrow, { y: 20, opacity: 0 });
gsap.set(titleChars, { yPercent: 110, opacity: 0 });
gsap.set(playBtn, { y: 30, opacity: 0 });
gsap.set(portrait, { x: 120, opacity: 0, scale: 0.95 });
gsap.set(stageVinyl, { scale: 0.4, opacity: 0, rotation: -30 });
// -------------------------------------------------------------------
// LOADER TIMELINE
// -------------------------------------------------------------------
const loaderTl = gsap.timeline({ onComplete: playScene });
// Vinyl continuously spins while counter ticks
const vinylSpin = gsap.to(loaderVinyl, {
rotation: 360,
duration: 2.5,
repeat: -1,
ease: 'none',
transformOrigin: 'center center',
});
const p = { v: 0 };
loaderTl.to(p, {
v: 100,
duration: 2.3,
ease: 'power1.inOut',
onUpdate: () => {
loaderCounter.textContent = String(Math.floor(p.v)).padStart(3, '0');
},
});
loaderTl.call(() => {
loaderLabel.textContent = 'SESSION READY';
});
// Exit: vinyl expands/fades, label + counter fade up
loaderTl.to([loaderCounter, loaderLabel], {
y: -20, opacity: 0, duration: 0.4, ease: 'power3.in', stagger: 0.05,
}, '+=0.25');
loaderTl.to(loaderVinyl, {
scale: 3.6,
opacity: 0,
duration: 1,
ease: 'power3.inOut',
onComplete: () => vinylSpin.kill(),
}, '-=0.3');
loaderTl.to(loader, {
opacity: 0,
duration: 0.4,
ease: 'power2.inOut',
}, '-=0.5');
loaderTl.set(loader, { display: 'none' });
// -------------------------------------------------------------------
// MAIN SCENE ENTRANCE
// -------------------------------------------------------------------
function playScene() {
const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
// Nav + actions
tl.to(magnetics, {
y: 0, opacity: 1,
duration: 0.6,
stagger: 0.05,
}, 0);
// Left + right rails
tl.to(rails, {
x: 0, opacity: 1,
duration: 0.7,
stagger: 0.08,
}, 0.25);
// Socials stagger
tl.to(socials, {
x: 0, opacity: 1,
duration: 0.5,
stagger: 0.06,
}, 0.4);
// Stage vinyl scales + rotates in
tl.to(stageVinyl, {
scale: 1, opacity: 0.95, rotation: 0,
duration: 1.5,
ease: 'power4.out',
}, 0.2);
// Portrait slides in
tl.to(portrait, {
x: 0, opacity: 1, scale: 1,
duration: 1.4,
ease: 'power4.out',
}, 0.5);
// Eyebrow link
tl.to(eyebrow, {
y: 0, opacity: 1,
duration: 0.7,
}, 0.9);
// Title chars stagger
tl.to(titleChars, {
yPercent: 0, opacity: 1,
duration: 1.1,
stagger: 0.02,
ease: 'expo.out',
}, 1);
// Play button
tl.to(playBtn, {
y: 0, opacity: 1,
duration: 0.8,
ease: 'back.out(1.6)',
}, 1.6);
// Draw scribble
tl.to(playScribble, {
strokeDashoffset: 0,
duration: 0.9,
ease: 'power2.out',
}, 1.8);
// Start continuous animations
tl.call(startContinuous, null, 2.1);
tl.call(enableInteractions, null, 2.1);
}
// -------------------------------------------------------------------
// CONTINUOUS ANIMATIONS
// -------------------------------------------------------------------
let vinylRot = null;
let playRingRot = null;
let marqueeAnim = null;
function startContinuous() {
// Stage vinyl slow spin
vinylRot = gsap.to(stageVinyl, {
rotation: '+=360',
duration: 60,
repeat: -1,
ease: 'none',
transformOrigin: 'center center',
});
// Play button ring spin
playRingRot = gsap.to(playRing, {
rotation: 360,
duration: 8,
repeat: -1,
ease: 'none',
transformOrigin: 'center center',
});
// Marquee scroll (infinite)
const groupWidth = marqueeTrack.querySelector('.marquee__group').offsetWidth;
marqueeAnim = gsap.to(marqueeTrack, {
x: -groupWidth,
duration: 28,
ease: 'none',
repeat: -1,
modifiers: { x: gsap.utils.unitize(x => parseFloat(x) % groupWidth) },
});
// Scroll-down line pulses
const scrollLine = root.querySelector('.rail__scroll-line');
if (scrollLine) {
gsap.fromTo(scrollLine, { scaleY: 0.3 }, {
scaleY: 1,
duration: 1.6,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
transformOrigin: 'top',
});
}
}
// -------------------------------------------------------------------
// INTERACTIONS
// -------------------------------------------------------------------
function enableInteractions() {
// ---- Magnetic elements ----
magnetics.forEach((el) => {
const strength = el.classList.contains('topnav__cta') ? 0.35
: el.classList.contains('topnav__menu') ? 0.45
: el.classList.contains('topnav__logo') ? 0.2
: el.classList.contains('hero__eyebrow') ? 0.18
: 0.25;
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)' });
});
});
// ---- Title char proximity + hover ----
const hero = document.getElementById('hero');
let mx = -9999, my = -9999;
hero.addEventListener('mousemove', (e) => {
mx = e.clientX; my = e.clientY;
});
hero.addEventListener('mouseleave', () => {
mx = -9999; my = -9999;
titleChars.forEach((c) => {
gsap.to(c, { y: 0, 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 = 160;
if (dist < PROX) {
const s = 1 - dist / PROX;
gsap.set(c, { y: -s * 22 });
} else {
gsap.set(c, { y: 0 });
}
});
});
titleChars.forEach((c) => {
c.addEventListener('mouseenter', () => {
gsap.to(c, { color: '#c9ff2d', duration: 0.2 });
});
c.addEventListener('mouseleave', () => {
gsap.to(c, { color: '#ffffff', duration: 0.4 });
});
});
// ---- Social icons: spring scale + rotate ----
socials.forEach((s) => {
s.addEventListener('mouseenter', () => {
gsap.to(s, {
scale: 1.18,
rotation: -8,
duration: 0.4,
ease: 'back.out(2)',
});
});
s.addEventListener('mouseleave', () => {
gsap.to(s, {
scale: 1,
rotation: 0,
duration: 0.5,
ease: 'elastic.out(1, 0.4)',
});
});
});
// ---- Grid dots: wave ripple on hover ----
dots.forEach((dot, i) => {
dot.addEventListener('mouseenter', () => {
dots.forEach((d, j) => {
const delay = Math.abs(j - i) * 0.04;
gsap.to(d, {
scale: 1.8,
delay,
duration: 0.25,
yoyo: true,
repeat: 1,
ease: 'power2.inOut',
});
});
});
});
// ---- Play button: ring speeds up + scribble redraws ----
playBtn.addEventListener('mouseenter', () => {
if (playRingRot) playRingRot.timeScale(4);
gsap.fromTo(playScribble,
{ strokeDashoffset: 120 },
{ strokeDashoffset: 0, duration: 0.6, ease: 'power2.out' }
);
});
playBtn.addEventListener('mouseleave', () => {
if (playRingRot) playRingRot.timeScale(1);
});
// ---- Marquee pauses on hover ----
const marqueeEl = document.getElementById('marquee');
marqueeEl.addEventListener('mouseenter', () => {
if (marqueeAnim) gsap.to(marqueeAnim, { timeScale: 0.2, duration: 0.4 });
});
marqueeEl.addEventListener('mouseleave', () => {
if (marqueeAnim) gsap.to(marqueeAnim, { timeScale: 1, duration: 0.4 });
});
// ---- Portrait tilt + vinyl parallax on mouse ----
let tx = 0, ty = 0, cxPos = 0, cyPos = 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(() => {
cxPos += (tx - cxPos) * 0.05;
cyPos += (ty - cyPos) * 0.05;
gsap.set(portrait, {
x: cxPos * 15,
y: cyPos * 10,
rotation: cxPos * 1.2,
});
});
}
})();