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