767 lines
35 KiB
Plaintext
767 lines
35 KiB
Plaintext
@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>
|