1122 lines
33 KiB
HTML
1122 lines
33 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>FruitBank — Scroll Demo</title>
|
||
|
||
<!-- Lenis smooth scroll -->
|
||
<script src="https://unpkg.com/lenis@1.1.18/dist/lenis.min.js"></script>
|
||
<!-- GSAP + ScrollTrigger -->
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
|
||
|
||
<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=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap"
|
||
rel="stylesheet"
|
||
/>
|
||
|
||
<style>
|
||
/* =============================================
|
||
RESET & BASE
|
||
============================================= */
|
||
*,
|
||
*::before,
|
||
*::after {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:root {
|
||
--green-deep: #1a3c22;
|
||
--green-primary: #2d7a3a;
|
||
--green-light: #8cb63c;
|
||
--orange-warm: #f4a236;
|
||
--orange-accent: #e8734a;
|
||
--cream: #f5f7f2;
|
||
--text-primary: #2c2c2c;
|
||
--text-muted: #6b7c6e;
|
||
--white: #fff;
|
||
}
|
||
|
||
html.lenis,
|
||
html.lenis body {
|
||
height: auto;
|
||
}
|
||
.lenis.lenis-smooth {
|
||
scroll-behavior: auto !important;
|
||
}
|
||
|
||
body {
|
||
font-family: "DM Sans", sans-serif;
|
||
color: var(--text-primary);
|
||
background: var(--white);
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
h1,
|
||
h2,
|
||
h3 {
|
||
font-family: "Playfair Display", serif;
|
||
}
|
||
|
||
/* =============================================
|
||
LOADING SCREEN
|
||
============================================= */
|
||
.loading-screen {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100vh;
|
||
background: var(--green-deep);
|
||
z-index: 10000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: opacity 0.6s ease;
|
||
}
|
||
|
||
.loading-screen.hidden {
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.loading-brand {
|
||
font-family: "Playfair Display", serif;
|
||
font-size: 32px;
|
||
color: var(--white);
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.loading-bar-track {
|
||
width: 200px;
|
||
height: 3px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.loading-bar-fill {
|
||
height: 100%;
|
||
width: 0%;
|
||
background: var(--orange-warm);
|
||
border-radius: 2px;
|
||
transition: width 0.15s;
|
||
}
|
||
|
||
.loading-pct {
|
||
margin-top: 12px;
|
||
font-size: 13px;
|
||
color: rgba(255, 255, 255, 0.4);
|
||
letter-spacing: 2px;
|
||
}
|
||
|
||
/* =============================================
|
||
FIXED CANVAS LAYER (replaces vault placeholder)
|
||
============================================= */
|
||
.vault-fixed-layer {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 0;
|
||
pointer-events: none;
|
||
background: #000;
|
||
overflow: hidden;
|
||
}
|
||
|
||
#vaultCanvas {
|
||
min-width: 100vw;
|
||
}
|
||
|
||
/* =============================================
|
||
TEXT OVERLAY SYSTEM
|
||
============================================= */
|
||
.vault-text-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100vh;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
z-index: 3;
|
||
pointer-events: none;
|
||
padding: 40px;
|
||
padding-top: 10vh;
|
||
}
|
||
|
||
.vault-text-block {
|
||
position: absolute;
|
||
top: 10vh;
|
||
text-align: center;
|
||
max-width: 700px;
|
||
opacity: 0;
|
||
transform: translateY(50px);
|
||
padding: 20px;
|
||
}
|
||
|
||
.vault-text-block h2 {
|
||
font-size: clamp(28px, 5vw, 56px);
|
||
color: #1a1a1a;
|
||
line-height: 1.15;
|
||
margin-bottom: 16px;
|
||
font-weight: 600;
|
||
text-shadow: 0 1px 20px rgba(255, 255, 255, 0.6);
|
||
}
|
||
|
||
.vault-text-block h2 em {
|
||
color: var(--orange-warm);
|
||
font-style: italic;
|
||
}
|
||
|
||
.vault-text-block p {
|
||
font-size: clamp(15px, 2vw, 20px);
|
||
color: rgba(30, 30, 30, 0.75);
|
||
line-height: 1.6;
|
||
max-width: 520px;
|
||
margin: 0 auto;
|
||
text-shadow: 0 1px 15px rgba(255, 255, 255, 0.4);
|
||
}
|
||
|
||
/* Scroll hint */
|
||
.scroll-hint {
|
||
position: fixed;
|
||
bottom: 40px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
z-index: 10;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
opacity: 1;
|
||
transition: opacity 0.5s;
|
||
}
|
||
|
||
.scroll-hint span {
|
||
font-size: 11px;
|
||
letter-spacing: 3px;
|
||
text-transform: uppercase;
|
||
color: rgba(30, 30, 30, 0.5);
|
||
}
|
||
|
||
.scroll-arrow {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-right: 2px solid rgba(30, 30, 30, 0.4);
|
||
border-bottom: 2px solid rgba(30, 30, 30, 0.4);
|
||
transform: rotate(45deg);
|
||
animation: bounceArrow 2s ease infinite;
|
||
}
|
||
|
||
@keyframes bounceArrow {
|
||
0%,
|
||
100% {
|
||
transform: rotate(45deg) translateY(0);
|
||
}
|
||
50% {
|
||
transform: rotate(45deg) translateY(6px);
|
||
}
|
||
}
|
||
|
||
/* Direction label */
|
||
.direction-label {
|
||
position: fixed;
|
||
bottom: 30px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
z-index: 50;
|
||
font-size: 13px;
|
||
letter-spacing: 3px;
|
||
text-transform: uppercase;
|
||
color: rgba(30, 30, 30, 0.5);
|
||
font-family: "DM Sans", sans-serif;
|
||
pointer-events: none;
|
||
opacity: 0;
|
||
transition: opacity 0.4s;
|
||
}
|
||
|
||
/* =============================================
|
||
PAGE CONTENT WRAPPER
|
||
============================================= */
|
||
.page-wrapper {
|
||
position: relative;
|
||
z-index: 5;
|
||
}
|
||
|
||
/* Vault section scroll height */
|
||
.vault-scroll-container {
|
||
height: 600vh;
|
||
position: relative;
|
||
}
|
||
|
||
/* =============================================
|
||
TRANSITION — VAULT TO PRODUCTS
|
||
============================================= */
|
||
.section-transition-vault {
|
||
position: relative;
|
||
z-index: 5;
|
||
height: 200px;
|
||
background: linear-gradient(to bottom, transparent, var(--white));
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* =============================================
|
||
PRODUCT SECTION (MOCK)
|
||
============================================= */
|
||
.section-products {
|
||
background: var(--white);
|
||
padding: 80px 5%;
|
||
position: relative;
|
||
}
|
||
|
||
.section-products .section-header {
|
||
text-align: center;
|
||
margin-bottom: 50px;
|
||
}
|
||
|
||
.section-products .section-header h2 {
|
||
font-size: clamp(24px, 3.5vw, 42px);
|
||
color: var(--green-deep);
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.section-products .section-header p {
|
||
color: var(--text-muted);
|
||
font-size: 17px;
|
||
}
|
||
|
||
.product-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||
gap: 24px;
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.product-card {
|
||
background: var(--cream);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
transition:
|
||
transform 0.3s ease,
|
||
box-shadow 0.3s ease;
|
||
}
|
||
|
||
.product-card:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.product-card .product-img {
|
||
height: 200px;
|
||
background: linear-gradient(
|
||
135deg,
|
||
var(--green-light),
|
||
var(--green-primary)
|
||
);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 64px;
|
||
}
|
||
|
||
.product-card .product-info {
|
||
padding: 20px;
|
||
}
|
||
|
||
.product-card .product-name {
|
||
font-family: "Playfair Display", serif;
|
||
font-size: 18px;
|
||
margin-bottom: 6px;
|
||
color: var(--green-deep);
|
||
}
|
||
|
||
.product-card .product-price {
|
||
font-weight: 700;
|
||
color: var(--green-primary);
|
||
font-size: 16px;
|
||
}
|
||
|
||
.product-card .product-unit {
|
||
font-size: 13px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* =============================================
|
||
FORKLIFT + STATS SECTION
|
||
============================================= */
|
||
.section-forklift {
|
||
position: relative;
|
||
overflow: hidden;
|
||
background: var(--green-deep);
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.forklift-scroll-container {
|
||
height: 300vh;
|
||
position: relative;
|
||
}
|
||
|
||
.forklift-sticky {
|
||
position: sticky;
|
||
top: 0;
|
||
height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 60px;
|
||
padding: 40px 5%;
|
||
}
|
||
|
||
.forklift-visual {
|
||
flex: 1;
|
||
max-width: 500px;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
height: 70vh;
|
||
position: relative;
|
||
}
|
||
|
||
.forklift-body {
|
||
position: relative;
|
||
width: 200px;
|
||
height: 120px;
|
||
}
|
||
|
||
.forklift-chassis {
|
||
position: absolute;
|
||
bottom: 0;
|
||
width: 200px;
|
||
height: 80px;
|
||
background: linear-gradient(145deg, var(--orange-warm), #d4882e);
|
||
border-radius: 8px 8px 4px 4px;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.forklift-chassis::before {
|
||
content: "";
|
||
position: absolute;
|
||
top: -35px;
|
||
left: 20px;
|
||
width: 100px;
|
||
height: 50px;
|
||
background: linear-gradient(145deg, var(--orange-warm), #c07828);
|
||
border-radius: 6px 6px 0 0;
|
||
}
|
||
|
||
.forklift-wheel {
|
||
position: absolute;
|
||
bottom: -12px;
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 50%;
|
||
background: #333;
|
||
border: 4px solid #555;
|
||
}
|
||
.forklift-wheel.front {
|
||
left: 20px;
|
||
}
|
||
.forklift-wheel.rear {
|
||
right: 20px;
|
||
}
|
||
|
||
.forklift-mast {
|
||
position: absolute;
|
||
left: -20px;
|
||
bottom: 0;
|
||
width: 8px;
|
||
height: 350px;
|
||
background: linear-gradient(to right, #666, #888, #666);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.forklift-forks {
|
||
position: absolute;
|
||
left: -60px;
|
||
bottom: 60px;
|
||
width: 70px;
|
||
}
|
||
|
||
.fork-arms {
|
||
width: 70px;
|
||
height: 6px;
|
||
background: #888;
|
||
position: relative;
|
||
border-radius: 1px;
|
||
box-shadow: 0 16px 0 #888;
|
||
}
|
||
|
||
.pallet {
|
||
position: absolute;
|
||
top: -55px;
|
||
left: -15px;
|
||
width: 100px;
|
||
height: 50px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 40px;
|
||
line-height: 1;
|
||
background: rgba(160, 120, 60, 0.3);
|
||
border-radius: 6px;
|
||
border: 2px solid rgba(160, 120, 60, 0.5);
|
||
}
|
||
|
||
.stats-panel {
|
||
flex: 1;
|
||
max-width: 500px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 40px;
|
||
}
|
||
|
||
.stat-item {
|
||
opacity: 0;
|
||
transform: translateY(40px);
|
||
}
|
||
|
||
.stat-number {
|
||
font-family: "Playfair Display", serif;
|
||
font-size: clamp(36px, 5vw, 64px);
|
||
font-weight: 700;
|
||
color: var(--orange-warm);
|
||
line-height: 1;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: clamp(14px, 1.8vw, 18px);
|
||
color: rgba(255, 255, 255, 0.6);
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
/* =============================================
|
||
FOOTER
|
||
============================================= */
|
||
.section-footer {
|
||
background: var(--green-deep);
|
||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||
padding: 60px 5% 30px;
|
||
text-align: center;
|
||
color: rgba(255, 255, 255, 0.4);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.section-footer .footer-brand {
|
||
font-family: "Playfair Display", serif;
|
||
font-size: 24px;
|
||
color: var(--white);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
/* =============================================
|
||
SCROLL PROGRESS BAR
|
||
============================================= */
|
||
.scroll-progress {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
height: 3px;
|
||
background: var(--orange-warm);
|
||
z-index: 9999;
|
||
width: 0%;
|
||
}
|
||
|
||
/* =============================================
|
||
MOBILE
|
||
============================================= */
|
||
@media (max-width: 768px) {
|
||
#vaultCanvas {
|
||
min-width: 170vw;
|
||
}
|
||
|
||
.forklift-sticky {
|
||
flex-direction: column;
|
||
gap: 30px;
|
||
}
|
||
|
||
.forklift-visual {
|
||
height: 40vh;
|
||
max-width: 300px;
|
||
}
|
||
|
||
.stats-panel {
|
||
gap: 24px;
|
||
text-align: center;
|
||
}
|
||
|
||
.forklift-body {
|
||
transform: scale(0.7);
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Loading screen -->
|
||
<div class="loading-screen" id="loadingScreen">
|
||
<div class="loading-brand">FruitBank</div>
|
||
<div class="loading-bar-track">
|
||
<div class="loading-bar-fill" id="loadingBarFill"></div>
|
||
</div>
|
||
<div class="loading-pct" id="loadingPct">0%</div>
|
||
</div>
|
||
|
||
<!-- Scroll progress bar -->
|
||
<div class="scroll-progress" id="scrollProgress"></div>
|
||
|
||
<!-- ==========================================
|
||
VAULT — FIXED CANVAS LAYER
|
||
========================================== -->
|
||
<div class="vault-fixed-layer" id="vaultLayer">
|
||
<canvas id="vaultCanvas"></canvas>
|
||
</div>
|
||
|
||
<!-- Text overlays -->
|
||
<div class="vault-text-overlay">
|
||
<div class="vault-text-block" id="text1">
|
||
<h2>Üdvözöljük a <em>FruitBank</em>-ban</h2>
|
||
<p>Ahol a frissesség zárt védelmet élvez, és a minőség sosem jár le.</p>
|
||
</div>
|
||
<div class="vault-text-block" id="text2">
|
||
<h2>Prémium gyümölcsök, <em>az egész világból</em></h2>
|
||
<p>Áfonya Peruból, szeder Hollandiából, mangó Brazíliából — közvetlenül a nagybanki raktárunkból.</p>
|
||
</div>
|
||
<div class="vault-text-block" id="text3">
|
||
<h2>A frissesség <em>nagybankja</em></h2>
|
||
<p>Hűtött logisztika. Napi szállítás. Válogatott I. osztályú minőség, minden tételben.</p>
|
||
</div>
|
||
<div class="vault-text-block" id="text4">
|
||
<h2>Rendeljen <em>online</em>, egyszerűen</h2>
|
||
<p>Nagykereskedelmi árak, kényelmes webshop — mert a friss gyümölcs mindenkié.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Direction label -->
|
||
<div class="direction-label" id="directionLabel"></div>
|
||
|
||
<!-- Scroll hint -->
|
||
<div class="scroll-hint" id="scrollHint">
|
||
<span>Görgessen a nyitáshoz</span>
|
||
<div class="scroll-arrow"></div>
|
||
</div>
|
||
|
||
<!-- ==========================================
|
||
PAGE CONTENT
|
||
========================================== -->
|
||
<div class="page-wrapper">
|
||
<!-- Vault scroll spacer -->
|
||
<div class="vault-scroll-container" id="vaultScrollContainer"></div>
|
||
|
||
<!-- Transition -->
|
||
<div class="section-transition-vault"></div>
|
||
|
||
<!-- Product section -->
|
||
<section class="section-products">
|
||
<div class="section-header">
|
||
<h2>Our Fresh Selection</h2>
|
||
<p>Handpicked from the best farms, ready for your business</p>
|
||
</div>
|
||
<div class="product-grid">
|
||
<div class="product-card">
|
||
<div class="product-img">🍎</div>
|
||
<div class="product-info">
|
||
<div class="product-name">Royal Gala Apples</div>
|
||
<div class="product-price">
|
||
€1.85 <span class="product-unit">/ kg</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="product-card">
|
||
<div
|
||
class="product-img"
|
||
style="background: linear-gradient(135deg, #f4a236, #e8734a)"
|
||
>
|
||
🥕
|
||
</div>
|
||
<div class="product-info">
|
||
<div class="product-name">Organic Carrots</div>
|
||
<div class="product-price">
|
||
€0.95 <span class="product-unit">/ kg</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="product-card">
|
||
<div
|
||
class="product-img"
|
||
style="background: linear-gradient(135deg, #e8d44a, #8cb63c)"
|
||
>
|
||
🍋
|
||
</div>
|
||
<div class="product-info">
|
||
<div class="product-name">Sicilian Lemons</div>
|
||
<div class="product-price">
|
||
€2.40 <span class="product-unit">/ kg</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="product-card">
|
||
<div
|
||
class="product-img"
|
||
style="background: linear-gradient(135deg, #6b3fa0, #2d7a3a)"
|
||
>
|
||
🍇
|
||
</div>
|
||
<div class="product-info">
|
||
<div class="product-name">Red Globe Grapes</div>
|
||
<div class="product-price">
|
||
€3.20 <span class="product-unit">/ kg</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Forklift + Stats -->
|
||
<section class="section-forklift">
|
||
<div class="forklift-scroll-container" id="forkliftScrollContainer">
|
||
<div class="forklift-sticky">
|
||
<div class="forklift-visual">
|
||
<div class="forklift-body">
|
||
<div class="forklift-mast"></div>
|
||
<div class="forklift-forks" id="forkliftForks">
|
||
<div class="fork-arms"></div>
|
||
<div class="pallet">🍈🍈</div>
|
||
</div>
|
||
<div class="forklift-chassis">
|
||
<div class="forklift-wheel front"></div>
|
||
<div class="forklift-wheel rear"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="stats-panel">
|
||
<div class="stat-item" id="stat1">
|
||
<div class="stat-number">
|
||
<span class="counter" data-target="12500">0</span>+
|
||
</div>
|
||
<div class="stat-label">Tonnes of Fruit Sold Yearly</div>
|
||
</div>
|
||
<div class="stat-item" id="stat2">
|
||
<div class="stat-number">
|
||
<span class="counter" data-target="3200">0</span>+
|
||
</div>
|
||
<div class="stat-label">Happy Partners & Clients</div>
|
||
</div>
|
||
<div class="stat-item" id="stat3">
|
||
<div class="stat-number">
|
||
<span class="counter" data-target="8400">0</span>+
|
||
</div>
|
||
<div class="stat-label">Monthly Orders Fulfilled</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Footer -->
|
||
<div class="section-footer">
|
||
<div class="footer-brand">FruitBank</div>
|
||
<p>© 2026 FruitBank Wholesale Produce. All rights reserved.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
// =============================================
|
||
// FRAME CONFIG
|
||
//
|
||
// Dial frames: 1–75 (indices 0–74)
|
||
// Used for clockwise (forward) and
|
||
// counterclockwise (reverse) segments.
|
||
//
|
||
// Vault open frames: 76–323 (indices 75–322)
|
||
// Played once at the end when vault opens.
|
||
//
|
||
// Scroll segments:
|
||
// 0–20% → Dial forward (1→75) + Text 1
|
||
// 20–40% → Dial reverse (75→1) + Text 2
|
||
// 40–60% → Dial forward (1→75) + Text 3
|
||
// 60–78% → Dial reverse (75→1) + Text 4
|
||
// 78–100% → Vault opening (76→323)
|
||
// =============================================
|
||
var TOTAL_FRAMES = 323;
|
||
var DIAL_START = 0; // index of first dial frame
|
||
var DIAL_END = 74; // index of last dial frame
|
||
var OPEN_START = 75; // index of first vault-open frame
|
||
var OPEN_END = 322; // index of last vault-open frame
|
||
var LERP_SPEED = 0.07;
|
||
var CROSSFADE = true;
|
||
|
||
var frames = [];
|
||
var loadedCount = 0;
|
||
|
||
// =============================================
|
||
// CANVAS SETUP
|
||
// =============================================
|
||
var canvas = document.getElementById("vaultCanvas");
|
||
var ctx = canvas.getContext("2d");
|
||
var canvasW = 0,
|
||
canvasH = 0;
|
||
|
||
function setupCanvas() {
|
||
var sample = frames[0];
|
||
if (!sample || !sample.naturalWidth) return;
|
||
var dpr = window.devicePixelRatio || 1;
|
||
var isMobile = window.innerWidth <= 768;
|
||
|
||
// Always fit width — fill edge-to-edge horizontally,
|
||
// let height overflow (hidden behind content anyway)
|
||
var imgAspect = sample.naturalWidth / sample.naturalHeight;
|
||
var vpW = window.innerWidth;
|
||
|
||
var displayW = vpW;
|
||
var displayH = vpW / imgAspect;
|
||
|
||
if (isMobile) {
|
||
displayW *= 1.7;
|
||
displayH *= 1.7;
|
||
}
|
||
|
||
canvas.style.width = displayW + "px";
|
||
canvas.style.height = displayH + "px";
|
||
canvas.width = Math.round(displayW * dpr);
|
||
canvas.height = Math.round(displayH * dpr);
|
||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||
canvasW = displayW;
|
||
canvasH = displayH;
|
||
}
|
||
|
||
// =============================================
|
||
// SCROLL SEGMENTS
|
||
// =============================================
|
||
var segments = [
|
||
{ start: 0.0, end: 0.2, type: "dial", dir: 1, textIdx: 0 },
|
||
{ start: 0.2, end: 0.4, type: "dial", dir: -1, textIdx: 1 },
|
||
{ start: 0.4, end: 0.6, type: "dial", dir: 1, textIdx: 2 },
|
||
{ start: 0.6, end: 0.78, type: "dial", dir: -1, textIdx: 3 },
|
||
{ start: 0.78, end: 1.0, type: "open", dir: 0, textIdx: -1 },
|
||
];
|
||
|
||
var textBlocks = [
|
||
document.getElementById("text1"),
|
||
document.getElementById("text2"),
|
||
document.getElementById("text3"),
|
||
document.getElementById("text4"),
|
||
];
|
||
|
||
var vaultContainer = document.getElementById("vaultScrollContainer");
|
||
var vaultLayer = document.getElementById("vaultLayer");
|
||
var scrollHint = document.getElementById("scrollHint");
|
||
var directionLabel = document.getElementById("directionLabel");
|
||
var progressBar = document.getElementById("scrollProgress");
|
||
var textOverlay = document.querySelector(".vault-text-overlay");
|
||
|
||
// =============================================
|
||
// FRAME RENDERING WITH LERP + CROSSFADE
|
||
// =============================================
|
||
var currentFrame = 0; // floating point frame index
|
||
var targetFrame = 0;
|
||
|
||
function getVaultProgress() {
|
||
var rect = vaultContainer.getBoundingClientRect();
|
||
var scrolled = -rect.top;
|
||
var total = rect.height - window.innerHeight;
|
||
return Math.max(0, Math.min(1, scrolled / total));
|
||
}
|
||
|
||
function calcTargetFrame(progress) {
|
||
// Find active segment
|
||
var seg = null;
|
||
var segProgress = 0;
|
||
|
||
for (var i = 0; i < segments.length; i++) {
|
||
if (progress >= segments[i].start && progress < segments[i].end) {
|
||
seg = segments[i];
|
||
segProgress = (progress - seg.start) / (seg.end - seg.start);
|
||
break;
|
||
}
|
||
}
|
||
// At or past 100%
|
||
if (!seg && progress >= 1) {
|
||
seg = segments[segments.length - 1];
|
||
segProgress = 1;
|
||
}
|
||
if (!seg) return 0;
|
||
|
||
if (seg.type === "dial") {
|
||
if (seg.dir === 1) {
|
||
// Forward: frame 0 → 74
|
||
return DIAL_START + segProgress * (DIAL_END - DIAL_START);
|
||
} else {
|
||
// Reverse: frame 74 → 0
|
||
return DIAL_END - segProgress * (DIAL_END - DIAL_START);
|
||
}
|
||
} else {
|
||
// Vault opening: frame 75 → 322
|
||
return OPEN_START + segProgress * (OPEN_END - OPEN_START);
|
||
}
|
||
}
|
||
|
||
function renderFrame() {
|
||
if (canvasW === 0) return;
|
||
|
||
var idxA = Math.floor(currentFrame);
|
||
var idxB = Math.ceil(currentFrame);
|
||
idxA = Math.max(0, Math.min(TOTAL_FRAMES - 1, idxA));
|
||
idxB = Math.max(0, Math.min(TOTAL_FRAMES - 1, idxB));
|
||
var blend = currentFrame - Math.floor(currentFrame);
|
||
|
||
var imgA = frames[idxA];
|
||
var imgB = frames[idxB];
|
||
|
||
if (imgA && imgA.complete && imgA.naturalWidth) {
|
||
ctx.clearRect(0, 0, canvasW, canvasH);
|
||
|
||
if (
|
||
CROSSFADE &&
|
||
blend > 0.01 &&
|
||
imgB &&
|
||
imgB.complete &&
|
||
imgB.naturalWidth &&
|
||
idxA !== idxB
|
||
) {
|
||
ctx.globalAlpha = 1;
|
||
ctx.drawImage(imgA, 0, 0, canvasW, canvasH);
|
||
ctx.globalAlpha = blend;
|
||
ctx.drawImage(imgB, 0, 0, canvasW, canvasH);
|
||
ctx.globalAlpha = 1;
|
||
} else {
|
||
ctx.drawImage(imgA, 0, 0, canvasW, canvasH);
|
||
}
|
||
}
|
||
}
|
||
|
||
// =============================================
|
||
// MAIN LOOP — 60fps, independent of scroll events
|
||
// =============================================
|
||
function mainLoop() {
|
||
var progress = getVaultProgress();
|
||
|
||
// Scroll progress bar
|
||
var totalScroll =
|
||
document.documentElement.scrollHeight - window.innerHeight;
|
||
var scrollPct = (window.scrollY / totalScroll) * 100;
|
||
progressBar.style.width = scrollPct + "%";
|
||
|
||
// Scroll hint
|
||
scrollHint.style.opacity = Math.max(0, 1 - progress * 8);
|
||
|
||
// Calculate target frame from scroll
|
||
targetFrame = calcTargetFrame(progress);
|
||
|
||
// Lerp toward target
|
||
var diff = targetFrame - currentFrame;
|
||
if (Math.abs(diff) > 0.05) {
|
||
currentFrame += diff * LERP_SPEED;
|
||
} else {
|
||
currentFrame = targetFrame;
|
||
}
|
||
|
||
// Render
|
||
renderFrame();
|
||
|
||
// Direction label
|
||
var activeSeg = null;
|
||
for (var i = 0; i < 4; i++) {
|
||
if (progress >= segments[i].start && progress < segments[i].end) {
|
||
activeSeg = segments[i];
|
||
break;
|
||
}
|
||
}
|
||
if (activeSeg) {
|
||
directionLabel.style.opacity = "1";
|
||
directionLabel.textContent =
|
||
activeSeg.dir > 0 ? "↻ Jobbra" : "↺ Balra";
|
||
} else {
|
||
directionLabel.style.opacity = "0";
|
||
}
|
||
|
||
// Text blocks
|
||
textBlocks.forEach(function (block, idx) {
|
||
var seg = null;
|
||
for (var j = 0; j < segments.length; j++) {
|
||
if (segments[j].textIdx === idx) {
|
||
seg = segments[j];
|
||
break;
|
||
}
|
||
}
|
||
if (!seg) return;
|
||
|
||
var textProgress = (progress - seg.start) / (seg.end - seg.start);
|
||
|
||
if (textProgress < 0 || textProgress > 1) {
|
||
block.style.opacity = "0";
|
||
block.style.transform = "translateY(50px)";
|
||
} else {
|
||
// Fade in 0–60%, hold 60–85%, fade out 85–100%
|
||
var alpha = 0;
|
||
if (textProgress < 0.6) {
|
||
alpha = textProgress / 0.6;
|
||
} else if (textProgress < 0.85) {
|
||
alpha = 1;
|
||
} else {
|
||
alpha = 1 - (textProgress - 0.85) / 0.15;
|
||
}
|
||
block.style.opacity = alpha;
|
||
var yShift = (1 - Math.min(1, textProgress / 0.6)) * 50;
|
||
block.style.transform = "translateY(" + yShift + "px)";
|
||
}
|
||
});
|
||
|
||
// Hide vault layer when scrolled well past
|
||
if (progress >= 1) {
|
||
var pastVault =
|
||
window.scrollY -
|
||
(vaultContainer.offsetTop + vaultContainer.offsetHeight);
|
||
if (pastVault > 200) {
|
||
var fadeOut = Math.max(0, 1 - (pastVault - 200) / 300);
|
||
vaultLayer.style.opacity = fadeOut;
|
||
}
|
||
} else {
|
||
vaultLayer.style.opacity = "1";
|
||
}
|
||
|
||
// Hide text overlay when not in vault section
|
||
var rect = vaultContainer.getBoundingClientRect();
|
||
var inVault = rect.top < window.innerHeight && rect.bottom > 0;
|
||
textOverlay.style.display = inVault ? "flex" : "none";
|
||
|
||
requestAnimationFrame(mainLoop);
|
||
}
|
||
|
||
// =============================================
|
||
// FORKLIFT + STATS
|
||
// =============================================
|
||
var forkliftContainer = document.getElementById(
|
||
"forkliftScrollContainer",
|
||
);
|
||
var forkliftForks = document.getElementById("forkliftForks");
|
||
var counters = document.querySelectorAll(".counter");
|
||
var statItems = document.querySelectorAll(".stat-item");
|
||
var FORK_MIN_Y = 60;
|
||
var FORK_MAX_Y = 280;
|
||
var currentForkY = FORK_MIN_Y;
|
||
var counterValues = Array.from(counters).map(function () {
|
||
return 0;
|
||
});
|
||
|
||
function forkliftLoop() {
|
||
var rect = forkliftContainer.getBoundingClientRect();
|
||
var scrolled = -rect.top;
|
||
var total = rect.height - window.innerHeight;
|
||
var progress = Math.max(0, Math.min(1, scrolled / total));
|
||
|
||
var targetForkY = FORK_MIN_Y + (FORK_MAX_Y - FORK_MIN_Y) * progress;
|
||
currentForkY += (targetForkY - currentForkY) * 0.08;
|
||
forkliftForks.style.bottom = currentForkY + "px";
|
||
|
||
statItems.forEach(function (item, i) {
|
||
var staggerStart = i * 0.2;
|
||
var itemProgress = Math.max(
|
||
0,
|
||
Math.min(1, (progress - staggerStart) / (1 - staggerStart)),
|
||
);
|
||
if (itemProgress > 0.05) {
|
||
var fadeIn = Math.min(1, (itemProgress - 0.05) / 0.3);
|
||
item.style.opacity = fadeIn;
|
||
item.style.transform = "translateY(" + (1 - fadeIn) * 40 + "px)";
|
||
}
|
||
var target = parseInt(counters[i].dataset.target);
|
||
var currentTarget = target * Math.min(1, itemProgress / 0.7);
|
||
counterValues[i] += (currentTarget - counterValues[i]) * 0.08;
|
||
counters[i].textContent = Math.round(
|
||
counterValues[i],
|
||
).toLocaleString();
|
||
});
|
||
|
||
requestAnimationFrame(forkliftLoop);
|
||
}
|
||
|
||
// =============================================
|
||
// PRELOAD FRAMES
|
||
// =============================================
|
||
var loadingBarFill = document.getElementById("loadingBarFill");
|
||
var loadingPct = document.getElementById("loadingPct");
|
||
var loadingScreen = document.getElementById("loadingScreen");
|
||
|
||
function preloadFrames() {
|
||
return new Promise(function (resolve) {
|
||
for (var i = 1; i <= TOTAL_FRAMES; i++) {
|
||
var img = new Image();
|
||
img.src = "frames/frame_" + String(i).padStart(4, "0") + ".jpg";
|
||
img.onload = img.onerror = function () {
|
||
loadedCount++;
|
||
var pct = Math.round((loadedCount / TOTAL_FRAMES) * 100);
|
||
loadingBarFill.style.width = pct + "%";
|
||
loadingPct.textContent = pct + "%";
|
||
if (loadedCount === TOTAL_FRAMES) resolve();
|
||
};
|
||
frames.push(img);
|
||
}
|
||
});
|
||
}
|
||
|
||
// =============================================
|
||
// INIT
|
||
// =============================================
|
||
preloadFrames().then(function () {
|
||
// Hide loading screen
|
||
loadingScreen.classList.add("hidden");
|
||
setTimeout(function () {
|
||
loadingScreen.style.display = "none";
|
||
}, 700);
|
||
|
||
// Lenis smooth scroll
|
||
var lenis = new Lenis({
|
||
duration: 1.8,
|
||
easing: function (t) {
|
||
return Math.min(1, 1.001 - Math.pow(2, -10 * t));
|
||
},
|
||
orientation: "vertical",
|
||
smoothWheel: true,
|
||
smoothTouch: true,
|
||
wheelMultiplier: 0.5,
|
||
touchMultiplier: 0.5,
|
||
});
|
||
|
||
gsap.registerPlugin(ScrollTrigger);
|
||
lenis.on("scroll", ScrollTrigger.update);
|
||
gsap.ticker.add(function (time) {
|
||
lenis.raf(time * 1000);
|
||
});
|
||
gsap.ticker.lagSmoothing(0);
|
||
|
||
// Canvas
|
||
setupCanvas();
|
||
window.addEventListener("resize", setupCanvas);
|
||
|
||
// Draw first frame
|
||
currentFrame = 0;
|
||
renderFrame();
|
||
|
||
// Start loops
|
||
requestAnimationFrame(mainLoop);
|
||
requestAnimationFrame(forkliftLoop);
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|