FruitBank/Presentation/Nop.Web/Themes/DatesExpress/Content/fruitbank-scroll-demo.html

1122 lines
33 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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: 175 (indices 074)
// Used for clockwise (forward) and
// counterclockwise (reverse) segments.
//
// Vault open frames: 76323 (indices 75322)
// Played once at the end when vault opens.
//
// Scroll segments:
// 020% → Dial forward (1→75) + Text 1
// 2040% → Dial reverse (75→1) + Text 2
// 4060% → Dial forward (1→75) + Text 3
// 6078% → Dial reverse (75→1) + Text 4
// 78100% → 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 060%, hold 6085%, fade out 85100%
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>