335 lines
9.0 KiB
Plaintext
335 lines
9.0 KiB
Plaintext
---
|
|
// ParallaxHero.astro — Osmo-style multi-layer GSAP parallax, Petronas theme
|
|
---
|
|
|
|
<div class="parallax">
|
|
<section class="parallax__header">
|
|
<div class="parallax__visuals">
|
|
|
|
<div data-parallax-layers class="parallax__layers">
|
|
|
|
<!-- LAYER 1 — bg photo (slowest, most travel) -->
|
|
<img
|
|
src="/image_zb1c2e65feww76x.png"
|
|
loading="eager"
|
|
data-parallax-layer="1"
|
|
alt=""
|
|
class="parallax__layer-img"
|
|
/>
|
|
|
|
<!-- LAYER 2 — teal colour grade (follows bg) -->
|
|
<div data-parallax-layer="2" class="parallax__layer-tint"></div>
|
|
|
|
<!-- LAYER 3 — title (mid speed) -->
|
|
<div data-parallax-layer="3" class="parallax__layer-title">
|
|
|
|
<h1 class="parallax__title">
|
|
<span class="parallax__title-accent">/</span>slashroot
|
|
</h1>
|
|
<!-- <div class="parallax__eyebrow font-mono">
|
|
<span style="color:var(--petronas-teal)">$</span> cat /var/log/adventures.log
|
|
</div> -->
|
|
<div class="parallax__cta">
|
|
<a href="/blog" class="btn btn-primary">Read the logs</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- LAYER 4 — figure foreground PNG (barely moves = pops forward) -->
|
|
<div data-parallax-layer="4" class="parallax__layer-figure" aria-hidden="true">
|
|
<img
|
|
src="/image_zb1c25zb1c25zb1c.png"
|
|
alt=""
|
|
class="parallax__figure-img"
|
|
/>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Bottom fade -->
|
|
<div class="parallax__fade"></div>
|
|
|
|
<!-- Scroll hint inside header for absolute positioning -->
|
|
<div class="parallax__scroll-hint" id="scroll-hint" aria-hidden="true">
|
|
<span class="font-mono">scroll</span>
|
|
<div class="scroll-line"></div>
|
|
</div>
|
|
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js" is:inline></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js" is:inline></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/lenis@1.1.13/dist/lenis.min.js" is:inline></script>
|
|
|
|
<script is:inline>
|
|
if (history.scrollRestoration) {
|
|
history.scrollRestoration = "manual";
|
|
}
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
gsap.registerPlugin(ScrollTrigger);
|
|
|
|
document.querySelectorAll('[data-parallax-layers]').forEach((trigger) => {
|
|
const header = trigger.closest('.parallax__header');
|
|
const tl = gsap.timeline({
|
|
scrollTrigger: {
|
|
trigger: header,
|
|
start: "top top",
|
|
end: "bottom top",
|
|
scrub: 0,
|
|
invalidateOnRefresh: true
|
|
}
|
|
});
|
|
|
|
// tl.to('[data-parallax-layer="1"]', { yPercent: 40, ease: "none" }, 0)
|
|
tl.fromTo('[data-parallax-layer="1"]',
|
|
{ yPercent: 0 }, // FORCE the start position
|
|
{ yPercent: 40, ease: "none" }, // The end position
|
|
0 // Keep your 0 position parameter
|
|
)
|
|
.to('[data-parallax-layer="2"]', { yPercent: 40, ease: "none" }, 0)
|
|
.to('[data-parallax-layer="3"]', { yPercent: 20, ease: "none" }, 0)
|
|
.to('[data-parallax-layer="4"]', { yPercent: 6, ease: "none" }, 0);
|
|
});
|
|
|
|
// Lenis smooth scroll
|
|
const lenis = new Lenis();
|
|
lenis.on('scroll', ScrollTrigger.update);
|
|
gsap.ticker.add((time) => { lenis.raf(time * 1000); });
|
|
gsap.ticker.lagSmoothing(0);
|
|
|
|
// Fade scroll hint
|
|
const hint = document.getElementById('scroll-hint');
|
|
if (hint) {
|
|
ScrollTrigger.create({
|
|
start: 60,
|
|
onEnter: () => gsap.to(hint, { opacity: 0, duration: 0.4 }),
|
|
onLeaveBack: () => gsap.to(hint, { opacity: 1, duration: 0.4 }),
|
|
});
|
|
}
|
|
|
|
// Entrance animation
|
|
const intro = gsap.timeline({ defaults: { ease: "power2.out" } });
|
|
intro
|
|
.from('.parallax__eyebrow', { opacity: 0, y: 20, duration: 0.8 }, 0.3)
|
|
.from('.parallax__title', { opacity: 0, y: 32, duration: 1.0 }, 0.5)
|
|
.from('.parallax__subtitle', { opacity: 0, y: 16, duration: 0.8 }, 0.72)
|
|
.from('.parallax__cta', { opacity: 0, y: 12, duration: 0.7 }, 0.92)
|
|
.from('.parallax__figure-img', { opacity: 0, y: 24, duration: 1.0 }, 0.25);
|
|
|
|
window.addEventListener('load', () => {
|
|
ScrollTrigger.refresh();
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.parallax {
|
|
position: relative;
|
|
}
|
|
|
|
/* Osmo natural height — no 200vh hack */
|
|
.parallax__header {
|
|
min-height: 100svh;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
/* Osmo: absolute + 120% height, no sticky */
|
|
.parallax__visuals {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 110%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.parallax__layers {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* Osmo magic offset — pulled up so it has room to scroll down */
|
|
.parallax__layer-img {
|
|
position: absolute;
|
|
top: -20.5%;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 117.5%;
|
|
object-fit: cover;
|
|
object-position: center 30%;
|
|
display: block;
|
|
filter: saturate(0.25) brightness(0.45);
|
|
pointer-events: none;
|
|
will-change: transform;
|
|
}
|
|
|
|
/* Layer 2 — teal tint, same geometry as bg */
|
|
.parallax__layer-tint {
|
|
position: absolute;
|
|
top: -17.5%;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 117.5%;
|
|
background:
|
|
radial-gradient(ellipse 65% 50% at 50% 60%, rgba(0,210,190,0.13) 0%, transparent 65%),
|
|
linear-gradient(to bottom,
|
|
rgba(5,10,10,0.65) 0%,
|
|
transparent 20%,
|
|
transparent 65%,
|
|
rgba(5,10,10,0.85) 100%
|
|
),
|
|
linear-gradient(to right,
|
|
rgba(5,10,10,0.5) 0%,
|
|
transparent 25%,
|
|
transparent 75%,
|
|
rgba(5,10,10,0.5) 100%
|
|
);
|
|
will-change: transform;
|
|
}
|
|
|
|
/* Layer 3 — title: same top offset as bg, centred vertically */
|
|
.parallax__layer-title {
|
|
position: absolute;
|
|
top: -20.5%;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 117.5%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0 1.5rem;
|
|
padding-bottom: 12vh;
|
|
text-align: center;
|
|
z-index: 1;
|
|
will-change: transform;
|
|
}
|
|
|
|
.parallax__eyebrow {
|
|
font-size: clamp(0.68rem, 1.4vw, 0.88rem);
|
|
color: var(--text-muted);
|
|
letter-spacing: 0.12em;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.parallax__title {
|
|
font-family: var(--font-display);
|
|
font-weight: 700;
|
|
font-size: clamp(4rem, 13vw, 11rem);
|
|
letter-spacing: -0.02em;
|
|
line-height: 0.92;
|
|
color: var(--text-primary);
|
|
text-shadow:
|
|
0 0 120px rgba(0,210,190,0.2),
|
|
0 4px 60px rgba(0,0,0,0.95);
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
|
|
.parallax__title-accent { color: var(--petronas-teal); }
|
|
|
|
.parallax__subtitle {
|
|
font-size: clamp(0.72rem, 1.8vw, 0.95rem);
|
|
color: var(--text-muted);
|
|
letter-spacing: 0.05em;
|
|
margin-bottom: 2.5rem;
|
|
max-width: 54ch;
|
|
}
|
|
|
|
.parallax__cta {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
width: 120%;
|
|
gap: 2rem;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
}
|
|
|
|
.parallax__cta-ghost {
|
|
font-size: 0.88rem;
|
|
color: var(--text-muted);
|
|
letter-spacing: 0.06em;
|
|
transition: color 150ms;
|
|
}
|
|
.parallax__cta-ghost:hover { color: var(--petronas-teal); }
|
|
|
|
/* Layer 4 — figure: same Osmo offset as bg so GSAP travel is consistent.
|
|
SVG is constrained with max-width so it doesn't fill the whole container. */
|
|
.parallax__layer-figure {
|
|
position: absolute;
|
|
top: -9.5%;
|
|
left: 0;
|
|
width: 120%;
|
|
height: 105.5%;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: flex-end;
|
|
padding-bottom: 2vh;
|
|
z-index: 2;
|
|
pointer-events: none;
|
|
will-change: transform;
|
|
}
|
|
|
|
.parallax__figure-img {
|
|
width: clamp(200px, 50vh, 650px);
|
|
height: 70vh;
|
|
display: block;
|
|
flex-shrink: 0;
|
|
object-fit: contain;
|
|
object-position: bottom center;
|
|
filter: drop-shadow(0 -8px 24px rgba(0,210,190,0.15));
|
|
}
|
|
|
|
/* ── Bottom fade ─────────────────────────────────────────────── */
|
|
.parallax__fade {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 180px;
|
|
background: linear-gradient(to bottom, transparent, var(--bg-void));
|
|
z-index: 10;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Scroll hint — absolute inside the 100svh header */
|
|
.parallax__scroll-hint {
|
|
position: absolute;
|
|
bottom: 2.5rem;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
z-index: 30;
|
|
font-size: 0.62rem;
|
|
letter-spacing: 0.18em;
|
|
text-transform: uppercase;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.scroll-line {
|
|
width: 1px;
|
|
height: 48px;
|
|
background: linear-gradient(to bottom, var(--petronas-teal), transparent);
|
|
transform-origin: top;
|
|
animation: scroll-grow 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes scroll-grow {
|
|
0% { transform: scaleY(0); opacity: 1; }
|
|
65% { transform: scaleY(1); opacity: 1; }
|
|
100% { transform: scaleY(1); opacity: 0; }
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.parallax__figure-svg { width: 110px; }
|
|
.parallax__cta { gap: 1rem; }
|
|
}
|
|
</style>
|