FruitBank/Presentation/Nop.Web/Themes/CarHaven/Views/Home/Index.cshtml

956 lines
38 KiB
Plaintext
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.

@using Nop.Services.Localization
@using Nop.Core
@inject ILocalizationService localizationService
@inject IStoreContext storeContext
@{
Layout = "_ColumnsOne";
var homepageTitle = await localizationService.GetLocalizedAsync(await storeContext.GetCurrentStoreAsync(), s => s.HomepageTitle);
if (!string.IsNullOrEmpty(homepageTitle))
{
NopHtml.AddTitleParts(homepageTitle);
}
var homepageDescription = await localizationService.GetLocalizedAsync(await storeContext.GetCurrentStoreAsync(), s => s.HomepageDescription);
if (!string.IsNullOrEmpty(homepageDescription))
{
NopHtml.AddMetaDescriptionParts(homepageDescription);
}
NopHtml.AppendPageCssClassParts("html-home-page");
// Signal _Root.cshtml to render vault layers
ViewBag.IsHomePage = true;
}
<div class="page home-page">
<div class="page-body">
<!-- ==========================================
VAULT SCROLL SPACER — drives the animation
========================================== -->
<div class="vault-scroll-container" id="vaultScrollContainer"></div>
<!-- ==========================================
STANDARD HOMEPAGE CONTENT (solid bg)
========================================== -->
<div style="background: #fff; position: relative; z-index: 5; padding-top: 70px;">
@await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.HomepageTop })
@* Topic block replaced by immersive feature section below *@
</div>
<!-- ==========================================
MIÉRT A FRUITBANK — IMMERSIVE FEATURES
========================================== -->
<section class="fb-features" id="fbFeatures">
<div class="fb-features-header">
<h2>Miért a <em>FruitBank</em>?</h2>
<p>Nagykereskedelem, másképp.</p>
</div>
<div class="fb-feature" data-align="left">
<div class="fb-feature-img">
<img src="/images/uploaded/freshness.jpg" alt="Farmról frissen" loading="lazy" />
</div>
<div class="fb-feature-content">
<span class="fb-feature-num">01</span>
<h3>Farmról frissen, minden héten</h3>
<p>A megbízható nyugat-európai forgalmazóktól történő közvetlen beszerzés garantálja, hogy termékeink a lehető legfrissebb állapotban, kiváló ízminőségben, és versenyképes árakon érkeznek Önhöz.</p>
</div>
</div>
<div class="fb-feature" data-align="right">
<div class="fb-feature-img">
<img src="/images/uploaded/delivery.jpg" alt="Heti szállítás" loading="lazy" />
</div>
<div class="fb-feature-content">
<span class="fb-feature-num">02</span>
<h3>Hetente friss szállítmány</h3>
<p>A hőmérséklet-szabályozott heti forgásban működő logisztika biztosítja, hogy rendelése minden alkalommal frissen érkezzen meg.</p>
</div>
</div>
<div class="fb-feature" data-align="left">
<div class="fb-feature-img">
<img src="/images/uploaded/variety.jpg" alt="Páratlan választék" loading="lazy" />
</div>
<div class="fb-feature-content">
<span class="fb-feature-num">03</span>
<h3>Páratlan választék</h3>
<p>A mindennapi alapanyagoktól az egzotikus különlegességekig egész évben gyümölcs és zöldség széles választéka.</p>
</div>
</div>
<div class="fb-feature" data-align="right">
<div class="fb-feature-img">
<img src="/images/uploaded/sustainable.jpg" alt="Fenntartható forrásból" loading="lazy" />
</div>
<div class="fb-feature-content">
<span class="fb-feature-num">04</span>
<h3>Fenntartható forrásból</h3>
<p>Olyan forgalmazókkal dolgozunk együtt, akik osztoznak a felelős gazdálkodás és az élelmiszer-pazarlás csökkentése iránti elkötelezettségünkben.</p>
</div>
</div>
</section>
<!-- ==========================================
PRODUCT CATEGORIES
========================================== -->
<section class="fb-categories">
<div class="fb-categories-header">
<h2>Amit <em>importálunk</em></h2>
<p>Válogatott minőség, közvetlenül a világ legjobb termelőitől.</p>
</div>
<div class="fb-cat-grid">
<a href="/termékek" class="fb-cat-card">
<img src="/images/uploaded/cat-citrus.jpg" alt="Citrusfélék" loading="lazy" />
<div class="fb-cat-label">
<h3>Citrusfélék</h3>
</div>
<div class="fb-cat-hover">
<h3>Citrusfélék</h3>
<ul>
<li>Narancs</li>
<li>Citrom</li>
<li>Mandarin</li>
<li>Grépfrút</li>
<li>Lime</li>
<li>Pomelo</li>
</ul>
</div>
</a>
<a href="/termékek" class="fb-cat-card">
<img src="/images/uploaded/cat-exotic.jpg" alt="Egzotikus gyümölcsök" loading="lazy" />
<div class="fb-cat-label">
<h3>Egzotikus gyümölcsök</h3>
</div>
<div class="fb-cat-hover">
<h3>Egzotikus gyümölcsök</h3>
<ul>
<li>Mangó</li>
<li>Ananász</li>
<li>Papaya</li>
<li>Passiongyümölcs</li>
<li>Licsi</li>
<li>Sárkánygyümölcs</li>
<li>Datolyaszilva</li>
</ul>
</div>
</a>
<a href="/termékek" class="fb-cat-card">
<img src="/images/uploaded/cat-veggies.jpg" alt="Zöldségek" loading="lazy" />
<div class="fb-cat-label">
<h3>Zöldségek <span>szezonon kívül is!</span></h3>
</div>
<div class="fb-cat-hover">
<h3>Zöldségek <span>szezonon kívül is!</span></h3>
<ul>
<li>Paradicsom</li>
<li>Koktélparadicsom </li>
<li>Paprikafélék</li>
<li>Cukkini</li>
<li>Padlizsán</li>
<li>Kígyóuborka</li>
<li>Brokkoli</li>
<li>Jégsaláta</li>
</ul>
</div>
</a>
<a href="/termékek" class="fb-cat-card">
<img src="/images/uploaded/cat-fruits.jpg" alt="Gyümölcsök" loading="lazy" />
<div class="fb-cat-label">
<h3>Gyümölcsök <span>szezonon kívül</span></h3>
</div>
<div class="fb-cat-hover">
<h3>Gyümölcsök <span>szezonon kívül</span></h3>
<ul>
<li>Szőlő</li>
<li>Őszibarack</li>
<li>Nektarin</li>
<li>Szilva</li>
<li>Cseresznye</li>
<li>Körte</li>
</ul>
</div>
</a>
<a href="/termékek" class="fb-cat-card">
<img src="/images/uploaded/cat-berries.jpg" alt="Bogyós gyümölcsök" loading="lazy" />
<div class="fb-cat-label">
<h3>Bogyós gyümölcsök</h3>
</div>
<div class="fb-cat-hover">
<h3>Bogyós gyümölcsök</h3>
<ul>
<li>Áfonya</li>
<li>Szeder</li>
<li>Málna</li>
<li>Földieper</li>
<li>Ribizli</li>
</ul>
</div>
</a>
<a href="/termékek" class="fb-cat-card">
<img src="/images/uploaded/cat-spices.jpg" alt="Egzotikus fűszerek" loading="lazy" />
<div class="fb-cat-label">
<h3>Egzotikus fűszerek</h3>
</div>
<div class="fb-cat-hover">
<h3>Egzotikus fűszerek</h3>
<ul>
<li>Friss gyömbér</li>
<li>Petrezselyem</li>
<li>Citromfű</li>
<li>Kapor</li>
<li>Koriander</li>
<li>Friss menta</li>
<li>Bazsalikom</li>
</ul>
</div>
</a>
<a href="/termékek" class="fb-cat-card">
<img src="/images/uploaded/cat-dates.jpg" alt="Datolya" loading="lazy" />
<div class="fb-cat-label">
<h3>Datolya</h3>
</div>
<div class="fb-cat-hover">
<h3>Datolya</h3>
<ul>
<li>Medjool</li>
<li>Deglet Nour</li>
<li>Barhi</li>
<li>Mazafati</li>
</ul>
</div>
</a>
<a href="/termékek" class="fb-cat-card">
<img src="/images/uploaded/cat-farveggies.jpg" alt="Távoli zöldségek" loading="lazy" />
<div class="fb-cat-label">
<h3>Távoli zöldségek</h3>
</div>
<div class="fb-cat-hover">
<h3>Távoli zöldségek</h3>
<ul>
<li>Avokádó</li>
<li>Fokhagyma</li>
<li>Édesburgonya</li>
<li>Gyömbér</li>
<li>Fokhagyma tisztított</li>
</ul>
</div>
</a>
</div>
</section>
<!-- ==========================================
FORKLIFT + STATS SECTION
========================================== -->
<section class="section-forklift">
<div class="forklift-scroll-container" id="forkliftScrollContainer">
<div class="forklift-sticky">
<div class="forklift-visual">
<canvas id="forkliftCanvas"></canvas>
</div>
<div class="stats-panel">
<div class="stat-item" id="stat1">
<div class="stat-number"><span class="stat-counter" data-target="25000">0</span>+</div>
<div class="stat-label">tonna áru évente</div>
</div>
<div class="stat-item" id="stat2">
<div class="stat-number"><span class="stat-counter" data-target="12000">0</span>+</div>
<div class="stat-label">megrendelés évente</div>
</div>
<div class="stat-item" id="stat3">
<div class="stat-number"><span class="stat-counter" data-target="1000">0</span>+</div>
<div class="stat-label">elégedett ügyfél</div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<!-- ==========================================
VAULT + FORKLIFT SCROLL ANIMATION SCRIPT
========================================== -->
<script asp-location="Footer">
(function() {
// =============================================
// FRAME CONFIG
//
// Dial frames: 1-75 (indices 0-74)
// Vault open frames: 76-323 (indices 75-322)
//
// Scroll segments within vault-scroll-container:
// 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;
var DIAL_END = 74;
var OPEN_START = 75;
var OPEN_END = 322;
var LERP_SPEED = 0.07;
var CROSSFADE = true;
var frames = [];
var loadedCount = 0;
var lenisInstance = null;
// =============================================
// SNAP-TO-SEGMENT CONFIG
// After the user pauses scrolling, if they're
// past the threshold in a dial segment, Lenis
// smoothly snaps to the next segment start.
// =============================================
var SNAP_TRIGGER = 0.005; // 6% into segment triggers snap
var SNAP_DURATION = 1.6; // seconds for snap animation
var isSnapping = false;
var lastSnappedSeg = -1;
var userScrolling = false;
var userScrollTimer = null;
// =============================================
// CANVAS SETUP
// =============================================
var canvas = document.getElementById('vaultCanvas');
if (!canvas) return; // not on homepage
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;
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.00, end: 0.20, type: 'dial', dir: 1, textIdx: 0 },
{ start: 0.20, end: 0.40, type: 'dial', dir: -1, textIdx: 1 },
{ start: 0.40, end: 0.60, type: 'dial', dir: 1, textIdx: 2 },
{ start: 0.60, end: 0.78, type: 'dial', dir: -1, textIdx: 3 },
{ start: 0.78, end: 1.00, type: 'open', dir: 0, textIdx: -1 },
];
var textBlocks = [
document.getElementById('vaultText1'),
document.getElementById('vaultText2'),
document.getElementById('vaultText3'),
document.getElementById('vaultText4'),
];
var vaultContainer = document.getElementById('vaultScrollContainer');
var vaultLayer = document.getElementById('vaultLayer');
var scrollHint = document.getElementById('vaultScrollHint');
var directionLabel = document.getElementById('vaultDirectionLabel');
var progressBar = document.getElementById('vaultScrollProgress');
var textOverlay = document.getElementById('vaultTextOverlay');
// =============================================
// FRAME RENDERING WITH LERP + CROSSFADE
// =============================================
var currentFrame = 0;
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) {
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;
}
}
if (!seg && progress >= 1) {
seg = segments[segments.length - 1];
segProgress = 1;
}
if (!seg) return 0;
if (seg.type === 'dial') {
if (seg.dir === 1) {
return DIAL_START + segProgress * (DIAL_END - DIAL_START);
} else {
return DIAL_END - segProgress * (DIAL_END - DIAL_START);
}
} else {
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
// =============================================
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);
// Target frame
targetFrame = calcTargetFrame(progress);
// Lerp
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 ? '\u21BB Jobbra' : '\u21BA Balra';
} else {
directionLabel.style.opacity = '0';
}
// Text blocks
textBlocks.forEach(function(block, idx) {
if (!block) return;
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);
var isMobile = window.innerWidth <= 768;
var xPrefix = isMobile ? 'translateX(-50%) ' : '';
if (textProgress < 0 || textProgress > 1) {
block.style.opacity = '0';
block.style.transform = xPrefix + 'translateY(50px)';
} else {
// Fade in 0-60%, hold 60-85%, fade out 85-100%
var alpha = 0;
if (textProgress < 0.60) {
alpha = textProgress / 0.60;
} 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.60)) * 50;
block.style.transform = xPrefix + 'translateY(' + yShift + 'px)';
}
});
// Hide vault layer when scrolled 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 CANVAS ANIMATION
// =============================================
var FL_TOTAL = 120;
var FL_LERP = 0.06;
var flFrames = [];
var flLoaded = 0;
var flCurrentFrame = 0;
var flTargetFrame = 0;
var flCanvas = document.getElementById('forkliftCanvas');
var flCtx = flCanvas ? flCanvas.getContext('2d') : null;
var flCanvasW = 0, flCanvasH = 0;
function setupForkliftCanvas() {
var sample = flFrames[0];
if (!sample || !sample.naturalWidth || !flCanvas) return;
var dpr = window.devicePixelRatio || 1;
var isMobile = window.innerWidth <= 768;
// Let the canvas fill its container width
var container = flCanvas.parentElement;
var displayW = container ? container.clientWidth : (isMobile ? window.innerWidth * 1.2 : window.innerWidth * 0.5);
var displayH = displayW * (sample.naturalHeight / sample.naturalWidth);
flCanvas.style.width = displayW + 'px';
flCanvas.style.height = displayH + 'px';
flCanvas.width = Math.round(displayW * dpr);
flCanvas.height = Math.round(displayH * dpr);
flCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
flCanvasW = displayW;
flCanvasH = displayH;
}
function preloadForkliftFrames() {
return new Promise(function(resolve) {
for (var i = 1; i <= FL_TOTAL; i++) {
var img = new Image();
img.src = '/Themes/CarHaven/Content/forklift-frames/frame_' + String(i).padStart(4, '0') + '.jpg';
img.onload = img.onerror = function() {
flLoaded++;
if (flLoaded === FL_TOTAL) resolve();
};
flFrames.push(img);
}
});
}
var forkliftContainer = document.getElementById('forkliftScrollContainer');
var counters = document.querySelectorAll('.stat-counter');
var statItems = document.querySelectorAll('.stat-item');
var counterValues = Array.from(counters).map(function() { return 0; });
function forkliftLoop() {
if (!forkliftContainer) { requestAnimationFrame(forkliftLoop); return; }
var rect = forkliftContainer.getBoundingClientRect();
var scrolled = -rect.top;
var total = rect.height - window.innerHeight;
var progress = Math.max(0, Math.min(1, scrolled / total));
// Target frame from scroll progress
flTargetFrame = progress * (FL_TOTAL - 1);
// Lerp toward target
var diff = flTargetFrame - flCurrentFrame;
if (Math.abs(diff) > 0.05) {
flCurrentFrame += diff * FL_LERP;
} else {
flCurrentFrame = flTargetFrame;
}
// Render with crossfade
if (flCanvasW > 0 && flFrames.length > 0) {
var idxA = Math.floor(flCurrentFrame);
var idxB = Math.ceil(flCurrentFrame);
idxA = Math.max(0, Math.min(FL_TOTAL - 1, idxA));
idxB = Math.max(0, Math.min(FL_TOTAL - 1, idxB));
var blend = flCurrentFrame - Math.floor(flCurrentFrame);
var imgA = flFrames[idxA];
var imgB = flFrames[idxB];
if (imgA && imgA.complete && imgA.naturalWidth) {
flCtx.clearRect(0, 0, flCanvasW, flCanvasH);
if (blend > 0.01 && imgB && imgB.complete && imgB.naturalWidth && idxA !== idxB) {
flCtx.globalAlpha = 1;
flCtx.drawImage(imgA, 0, 0, flCanvasW, flCanvasH);
flCtx.globalAlpha = blend;
flCtx.drawImage(imgB, 0, 0, flCanvasW, flCanvasH);
flCtx.globalAlpha = 1;
} else {
flCtx.drawImage(imgA, 0, 0, flCanvasW, flCanvasH);
}
}
}
// Stats counters
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('vaultLoadingBarFill');
var loadingPct = document.getElementById('vaultLoadingPct');
var loadingScreen = document.getElementById('vaultLoadingScreen');
function preloadFrames() {
return new Promise(function(resolve) {
for (var i = 1; i <= TOTAL_FRAMES; i++) {
var img = new Image();
img.src = '/Themes/CarHaven/Content/frames/frame_' + String(i).padStart(4, '0') + '.jpg';
img.onload = img.onerror = function() {
loadedCount++;
var pct = Math.round((loadedCount / TOTAL_FRAMES) * 100);
if (loadingBarFill) loadingBarFill.style.width = pct + '%';
if (loadingPct) loadingPct.textContent = pct + '%';
if (loadedCount === TOTAL_FRAMES) resolve();
};
frames.push(img);
}
});
}
// =============================================
// INIT
// =============================================
preloadFrames().then(function() {
// Hide loading screen
if (loadingScreen) {
loadingScreen.classList.add('hidden');
setTimeout(function() { loadingScreen.style.display = 'none'; }, 700);
}
// Lenis smooth scroll
lenisInstance = 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.8,
touchMultiplier: 0.8,
});
gsap.registerPlugin(ScrollTrigger);
lenisInstance.on('scroll', ScrollTrigger.update);
gsap.ticker.add(function(time) { lenisInstance.raf(time * 1000); });
gsap.ticker.lagSmoothing(0);
// Snap-to-segment: listen to RAW user input, not Lenis scroll
// Lenis scroll events fire continuously during interpolation,
// which kills debounce timers. Wheel/touch events only fire
// when the user is actually interacting.
function onUserScrollInput() {
userScrolling = true;
clearTimeout(userScrollTimer);
userScrollTimer = setTimeout(function() {
userScrolling = false;
trySnap();
}, 120); // 120ms after last input = user stopped
}
function trySnap() {
if (isSnapping) return;
var progress = getVaultProgress();
if (progress <= 0 || progress >= 0.78) return;
for (var s = 0; s < segments.length - 1; s++) {
var seg = segments[s];
if (seg.type !== 'dial') continue;
var segProg = (progress - seg.start) / (seg.end - seg.start);
if (segProg > SNAP_TRIGGER && segProg < 0.92 && lastSnappedSeg < s) {
lastSnappedSeg = s;
isSnapping = true;
var nextStart = segments[s + 1].start;
var containerTop = vaultContainer.offsetTop;
var containerScroll = vaultContainer.offsetHeight - window.innerHeight;
var targetScroll = containerTop + nextStart * containerScroll;
lenisInstance.scrollTo(targetScroll, {
duration: SNAP_DURATION,
easing: function(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2,3)/2; },
onComplete: function() { isSnapping = false; }
});
return;
}
}
// Reset tracker if scrolled backwards
for (var r = lastSnappedSeg; r >= 0; r--) {
if (progress < segments[r].start + 0.02) {
lastSnappedSeg = r - 1;
}
}
}
window.addEventListener('wheel', onUserScrollInput, { passive: true });
window.addEventListener('touchmove', onUserScrollInput, { passive: true });
window.addEventListener('touchend', function() {
userScrolling = false;
setTimeout(trySnap, 80);
}, { passive: true });
// Vault canvas
setupCanvas();
window.addEventListener('resize', setupCanvas);
currentFrame = 0;
renderFrame();
requestAnimationFrame(mainLoop);
// Forklift canvas — preload then setup, loop runs immediately for stats
preloadForkliftFrames().then(function() {
setupForkliftCanvas();
window.addEventListener('resize', setupForkliftCanvas);
});
requestAnimationFrame(forkliftLoop);
// =============================================
// SCROLL REVEAL ANIMATIONS
// Uses GSAP ScrollTrigger (already loaded)
// =============================================
(function initScrollReveal() {
// --- Section titles ---
document.querySelectorAll('.product-grid .title, .home-page-product-grid .title').forEach(function(el) {
el.classList.add('sr-fade-up');
});
// --- Immersive features header ---
var fbHeader = document.querySelector('.fb-features-header');
if (fbHeader) fbHeader.classList.add('sr-fade-up');
// --- Immersive feature rows: image parallax + content reveal ---
document.querySelectorAll('.fb-feature').forEach(function(feature) {
var img = feature.querySelector('.fb-feature-img img');
var content = feature.querySelector('.fb-feature-content');
var align = feature.getAttribute('data-align');
// Parallax on image
if (img) {
gsap.fromTo(img,
{ yPercent: -10 },
{
yPercent: 10,
ease: 'none',
scrollTrigger: {
trigger: feature,
start: 'top bottom',
end: 'bottom top',
scrub: 1
}
}
);
}
// Content slide in from the side
if (content) {
var xFrom = align === 'right' ? 60 : -60;
gsap.from(content, {
x: xFrom,
opacity: 0,
duration: 1,
ease: 'power3.out',
scrollTrigger: {
trigger: feature,
start: 'top 75%',
toggleActions: 'play none none none'
}
});
}
});
// --- Category cards (stagger reveal) ---
var catCards = document.querySelectorAll('.fb-cat-card');
if (catCards.length > 0) {
catCards.forEach(function(el) { el.classList.add('sr-stagger-item'); });
gsap.to(catCards, {
opacity: 1,
y: 0,
duration: 0.7,
ease: 'power3.out',
stagger: 0.12,
scrollTrigger: {
trigger: '.fb-cat-grid',
start: 'top 85%',
toggleActions: 'play none none none'
}
});
}
// --- Category header ---
var catHeader = document.querySelector('.fb-categories-header');
if (catHeader) catHeader.classList.add('sr-fade-up');
// --- Product cards (staggered separately, not sr-fade-up) ---
document.querySelectorAll('.product-grid .item-box').forEach(function(el) {
el.classList.add('sr-stagger-item');
});
// --- Footer (staggered separately) ---
document.querySelectorAll('.footer-block').forEach(function(el) {
el.classList.add('sr-stagger-item');
});
// ---- Animate all sr-fade-up elements ----
gsap.utils.toArray('.sr-fade-up').forEach(function(el) {
gsap.to(el, {
opacity: 1,
y: 0,
duration: 0.8,
ease: 'power3.out',
scrollTrigger: {
trigger: el,
start: 'top 88%',
toggleActions: 'play none none none'
}
});
});
// ---- Animate sr-fade-left elements ----
gsap.utils.toArray('.sr-fade-left').forEach(function(el) {
gsap.to(el, {
opacity: 1,
x: 0,
duration: 0.9,
ease: 'power3.out',
scrollTrigger: {
trigger: el,
start: 'top 88%',
toggleActions: 'play none none none'
}
});
});
// ---- Animate sr-fade-right elements ----
gsap.utils.toArray('.sr-fade-right').forEach(function(el) {
gsap.to(el, {
opacity: 1,
x: 0,
duration: 0.9,
ease: 'power3.out',
scrollTrigger: {
trigger: el,
start: 'top 88%',
toggleActions: 'play none none none'
}
});
});
// ---- Stagger product cards within each grid ----
document.querySelectorAll('.product-grid').forEach(function(grid) {
var cards = grid.querySelectorAll('.item-box.sr-stagger-item');
if (cards.length === 0) return;
gsap.to(cards, {
opacity: 1,
y: 0,
duration: 0.7,
ease: 'power3.out',
stagger: 0.1,
scrollTrigger: {
trigger: grid,
start: 'top 85%',
toggleActions: 'play none none none'
}
});
});
// ---- Stagger footer blocks ----
var footerBlocks = document.querySelectorAll('.footer-block.sr-stagger-item');
if (footerBlocks.length > 0) {
gsap.to(footerBlocks, {
opacity: 1,
y: 0,
duration: 0.6,
ease: 'power2.out',
stagger: 0.12,
scrollTrigger: {
trigger: footerBlocks[0],
start: 'top 90%',
toggleActions: 'play none none none'
}
});
}
})();
});
})();
</script>