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

767 lines
35 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-BASED SCROLL ANIMATION
//
// 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;
var SNAP_TRIGGER = 0.005;
var SNAP_DURATION = 1.6;
var isSnapping = false;
var lastSnappedSeg = -1;
var userScrolling = false;
var userScrollTimer = null;
// =============================================
// CANVAS SETUP
// =============================================
var canvas = document.getElementById('vaultCanvas');
if (!canvas) return;
var ctx = canvas.getContext('2d');
var canvasW = 0, canvasH = 0;
function setupCanvas() {
var sample = null;
for (var s = 0; s < frames.length; s++) {
if (frames[s] && frames[s].complete && frames[s].naturalWidth) { sample = frames[s]; break; }
}
if (!sample) 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, 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') {
return seg.dir === 1 ? DIAL_START + segProgress * (DIAL_END - DIAL_START)
: DIAL_END - segProgress * (DIAL_END - DIAL_START);
}
return OPEN_START + segProgress * (OPEN_END - OPEN_START);
}
function isReady(i) { return frames[i] && frames[i].complete && frames[i].naturalWidth; }
function findNearest(idx) {
idx = Math.max(0, Math.min(TOTAL_FRAMES - 1, Math.round(idx)));
if (isReady(idx)) return idx;
for (var d = 1; d < TOTAL_FRAMES; d++) {
if (idx - d >= 0 && isReady(idx - d)) return idx - d;
if (idx + d < TOTAL_FRAMES && isReady(idx + d)) return idx + d;
}
return -1;
}
function renderFrame() {
if (canvasW === 0) return;
var idxA = Math.floor(currentFrame), 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);
if (!isReady(idxA)) { idxA = findNearest(idxA); if (idxA < 0) return; }
var imgA = frames[idxA];
if (imgA && imgA.complete && imgA.naturalWidth) {
ctx.clearRect(0, 0, canvasW, canvasH);
if (CROSSFADE && blend > 0.01 && isReady(idxB) && idxA !== idxB) {
ctx.globalAlpha = 1;
ctx.drawImage(imgA, 0, 0, canvasW, canvasH);
ctx.globalAlpha = blend;
ctx.drawImage(frames[idxB], 0, 0, canvasW, canvasH);
ctx.globalAlpha = 1;
} else {
ctx.drawImage(imgA, 0, 0, canvasW, canvasH);
}
}
}
// =============================================
// MAIN LOOP
// =============================================
function mainLoop() {
var progress = getVaultProgress();
var totalScroll = document.documentElement.scrollHeight - window.innerHeight;
progressBar.style.width = (window.scrollY / totalScroll) * 100 + '%';
scrollHint.style.opacity = Math.max(0, 1 - progress * 8);
targetFrame = calcTargetFrame(progress);
var diff = targetFrame - currentFrame;
if (Math.abs(diff) > 0.05) { currentFrame += diff * LERP_SPEED; }
else { currentFrame = targetFrame; }
renderFrame();
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'; }
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 tp = (progress - seg.start) / (seg.end - seg.start);
var isMobile = window.innerWidth <= 768;
var xP = isMobile ? 'translateX(-50%) ' : '';
if (tp < 0 || tp > 1) { block.style.opacity = '0'; block.style.transform = xP + 'translateY(50px)'; }
else {
var alpha = tp < 0.60 ? tp / 0.60 : tp < 0.85 ? 1 : 1 - (tp - 0.85) / 0.15;
block.style.opacity = alpha;
block.style.transform = xP + 'translateY(' + ((1 - Math.min(1, tp / 0.60)) * 50) + 'px)';
}
});
if (progress >= 1) {
var pastVault = window.scrollY - (vaultContainer.offsetTop + vaultContainer.offsetHeight);
if (pastVault > 200) { vaultLayer.style.opacity = Math.max(0, 1 - (pastVault - 200) / 300); }
} else { vaultLayer.style.opacity = '1'; }
var rect = vaultContainer.getBoundingClientRect();
textOverlay.style.display = (rect.top < window.innerHeight && rect.bottom > 0) ? 'flex' : 'none';
requestAnimationFrame(mainLoop);
}
// =============================================
// FORKLIFT CANVAS ANIMATION (frame-based)
// =============================================
var FL_TOTAL = 120;
var FL_LERP = 0.06;
var flFrames = [];
var flLoaded = 0;
var flCurrentFrame = 0;
var flCanvas = document.getElementById('forkliftCanvas');
var flCtx = flCanvas ? flCanvas.getContext('2d') : null;
var flCanvasW = 0, flCanvasH = 0;
function setupForkliftCanvas() {
var sample = null;
for (var s = 0; s < flFrames.length; s++) {
if (flFrames[s] && flFrames[s].complete && flFrames[s].naturalWidth) { sample = flFrames[s]; break; }
}
if (!sample || !flCanvas) return;
var dpr = window.devicePixelRatio || 1;
var container = flCanvas.parentElement;
var displayW = container ? container.clientWidth : 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() {
for (var i = 0; i < FL_TOTAL; i++) flFrames.push(null);
return new Promise(function(resolve) {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/Themes/CarHaven/Content/forklift-frames/forklift-bundle.jpg', true);
xhr.responseType = 'arraybuffer';
xhr.onload = function() {
if (xhr.status !== 200) { resolve(); return; }
var buf = xhr.response;
var view = new DataView(buf);
var count = view.getUint32(0, true);
var decoded = 0;
for (var i = 0; i < count; i++) {
var offset = view.getUint32(4 + i * 8, true);
var size = view.getUint32(4 + i * 8 + 4, true);
var blob = new Blob([new Uint8Array(buf, offset, size)], { type: 'image/jpeg' });
var img = new Image();
img.src = URL.createObjectURL(blob);
flFrames[i] = img;
img.onload = function() { decoded++; flLoaded = decoded; if (decoded >= 5) resolve(); };
}
};
xhr.onerror = function() { resolve(); };
xhr.send();
});
}
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));
var flTarget = progress * (FL_TOTAL - 1);
var diff = flTarget - flCurrentFrame;
if (Math.abs(diff) > 0.05) { flCurrentFrame += diff * FL_LERP; } else { flCurrentFrame = flTarget; }
if (flCanvasW > 0) {
var idxA = Math.max(0, Math.min(FL_TOTAL - 1, Math.floor(flCurrentFrame)));
var idxB = Math.max(0, Math.min(FL_TOTAL - 1, Math.ceil(flCurrentFrame)));
var blend = flCurrentFrame - Math.floor(flCurrentFrame);
var imgA = flFrames[idxA], imgB = flFrames[idxB];
// Fallback
if (!imgA || !imgA.complete || !imgA.naturalWidth) {
for (var fd = 1; fd < FL_TOTAL; fd++) {
if (idxA - fd >= 0 && flFrames[idxA - fd] && flFrames[idxA - fd].complete) { imgA = flFrames[idxA - fd]; break; }
if (idxA + fd < FL_TOTAL && flFrames[idxA + fd] && flFrames[idxA + fd].complete) { imgA = flFrames[idxA + fd]; break; }
}
}
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); }
}
}
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);
}
// =============================================
// SINGLE-FILE FRAME LOADER
//
// Instead of 323 individual HTTP requests (each
// going through the full ASP.NET middleware),
// we fetch ONE binary bundle file. Format:
// 4 bytes: uint32 LE frame count
// N * 8 bytes: (uint32 offset, uint32 size) per frame
// Then all JPG data concatenated
//
// One request → ~50MB on gigabit = ~1 second.
// =============================================
var loadingBarFill = document.getElementById('vaultLoadingBarFill');
var loadingPct = document.getElementById('vaultLoadingPct');
var loadingScreen = document.getElementById('vaultLoadingScreen');
function preloadFrames() {
for (var i = 0; i < TOTAL_FRAMES; i++) frames.push(null);
return new Promise(function(resolve) {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/Themes/CarHaven/Content/frames/vault-bundle.jpg', true);
xhr.responseType = 'arraybuffer';
xhr.onprogress = function(e) {
if (e.lengthComputable) {
var pct = Math.round((e.loaded / e.total) * 100);
if (loadingBarFill) loadingBarFill.style.width = pct + '%';
if (loadingPct) loadingPct.textContent = pct + '%';
}
};
xhr.onload = function() {
if (xhr.status !== 200) { console.error('Failed to load vault frames bundle'); resolve(); return; }
var buf = xhr.response;
var view = new DataView(buf);
var count = view.getUint32(0, true);
// Parse header: offset + size for each frame
var headerSize = 4 + count * 8;
var decoded = 0;
for (var i = 0; i < count; i++) {
var offset = view.getUint32(4 + i * 8, true);
var size = view.getUint32(4 + i * 8 + 4, true);
var blob = new Blob([new Uint8Array(buf, offset, size)], { type: 'image/jpeg' });
var img = new Image();
img.src = URL.createObjectURL(blob);
frames[i] = img;
// Resolve early after first 15 frames decode
img.onload = (function(idx) {
return function() {
decoded++;
loadedCount = decoded;
if (decoded >= 15) resolve();
};
})(i);
}
};
xhr.onerror = function() {
console.error('Network error loading vault frames bundle');
resolve();
};
xhr.send();
});
}
// =============================================
// INIT
// =============================================
preloadFrames().then(function() {
if (loadingScreen) {
loadingScreen.classList.add('hidden');
setTimeout(function() { loadingScreen.style.display = 'none'; }, 700);
}
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);
function onUserScrollInput() {
userScrolling = true; clearTimeout(userScrollTimer);
userScrollTimer = setTimeout(function() { userScrolling = false; trySnap(); }, 120);
}
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 containerScroll = vaultContainer.offsetHeight - window.innerHeight;
lenisInstance.scrollTo(vaultContainer.offsetTop + segments[s + 1].start * containerScroll, {
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;
}
}
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 });
setupCanvas(); window.addEventListener('resize', setupCanvas);
currentFrame = 0; renderFrame();
requestAnimationFrame(mainLoop);
preloadForkliftFrames().then(function() {
setupForkliftCanvas(); window.addEventListener('resize', setupForkliftCanvas);
});
requestAnimationFrame(forkliftLoop);
// =============================================
// SCROLL REVEAL ANIMATIONS
// =============================================
(function initScrollReveal() {
document.querySelectorAll('.product-grid .title, .home-page-product-grid .title').forEach(function(el) { el.classList.add('sr-fade-up'); });
var fbHeader = document.querySelector('.fb-features-header');
if (fbHeader) fbHeader.classList.add('sr-fade-up');
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');
if (img) { gsap.fromTo(img, { yPercent: -10 }, { yPercent: 10, ease: 'none', scrollTrigger: { trigger: feature, start: 'top bottom', end: 'bottom top', scrub: 1 } }); }
if (content) { gsap.from(content, { x: align === 'right' ? 60 : -60, opacity: 0, duration: 1, ease: 'power3.out', scrollTrigger: { trigger: feature, start: 'top 75%', toggleActions: 'play none none none' } }); }
});
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' } });
}
var catHeader = document.querySelector('.fb-categories-header');
if (catHeader) catHeader.classList.add('sr-fade-up');
document.querySelectorAll('.product-grid .item-box').forEach(function(el) { el.classList.add('sr-stagger-item'); });
document.querySelectorAll('.footer-block').forEach(function(el) { el.classList.add('sr-stagger-item'); });
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' } }); });
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' } }); });
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' } }); });
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' } });
});
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>