/** * landing.js — Pixel-Perfect inspired interactions for Nexus Autoparts * * Features: * - Animated canvas grid background (grid lines, stars at intersections, glowing tiles) * - Scroll-reveal via IntersectionObserver * - Counter animation on stat cards * - Typewriter effect * - Infinite brand marquee loader * - Theme toggle with icon swap */ (function () { 'use strict'; // ═══════════════════════════════════════════════════════════════════════════ // THEME TOGGLE // ═══════════════════════════════════════════════════════════════════════════ var themeIcon = document.getElementById('themeIcon'); function updateThemeIcon() { var theme = document.documentElement.getAttribute('data-theme'); if (themeIcon) themeIcon.innerHTML = theme === 'industrial' ? '☾' : '☀'; } window.toggleTheme = function () { var html = document.documentElement; var current = html.getAttribute('data-theme'); var next = current === 'industrial' ? 'modern' : 'industrial'; html.setAttribute('data-theme', next); localStorage.setItem('nexus-theme', next); updateThemeIcon(); // Canvas will pick up new CSS vars on next frame }; updateThemeIcon(); // ═══════════════════════════════════════════════════════════════════════════ // CANVAS ANIMATED GRID // ═══════════════════════════════════════════════════════════════════════════ var canvas = document.getElementById('heroCanvas'); if (canvas) { var ctx = canvas.getContext('2d'); var CELL = 80; var MAX_GLOWS = 6; var glows = []; var frame = 0; function resizeCanvas() { var hero = canvas.parentElement; canvas.width = hero.offsetWidth; canvas.height = hero.offsetHeight; } function getColor(varName) { return getComputedStyle(document.documentElement).getPropertyValue(varName).trim(); } function drawStar(x, y, outer, inner, color) { ctx.beginPath(); for (var i = 0; i < 4; i++) { var angle = (Math.PI / 2) * i; ctx.lineTo(x + Math.cos(angle) * outer, y + Math.sin(angle) * outer); ctx.lineTo(x + Math.cos(angle + Math.PI / 4) * inner, y + Math.sin(angle + Math.PI / 4) * inner); } ctx.closePath(); ctx.fillStyle = color; ctx.fill(); } function spawnGlow() { var cols = Math.floor(canvas.width / CELL); var rows = Math.floor(canvas.height / CELL); glows.push({ col: Math.floor(Math.random() * cols), row: Math.floor(Math.random() * rows), life: 0, maxLife: 90 + Math.random() * 60, // 1.5-2.5s at 60fps }); } function animateGrid() { frame++; ctx.clearRect(0, 0, canvas.width, canvas.height); var gridColor = getColor('--canvas-grid-color') || 'rgba(255,255,255,0.06)'; var starColor = getColor('--canvas-star-color') || 'rgba(245,166,35,0.3)'; var glowColor = getColor('--canvas-glow-color') || 'rgba(245,166,35,0.08)'; var cols = Math.floor(canvas.width / CELL) + 1; var rows = Math.floor(canvas.height / CELL) + 1; // Draw grid lines ctx.strokeStyle = gridColor; ctx.lineWidth = 1; for (var c = 0; c <= cols; c++) { var x = c * CELL; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke(); } for (var r = 0; r <= rows; r++) { var y = r * CELL; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke(); } // Draw stars at intersections (every other one for performance) for (c = 0; c <= cols; c += 2) { for (r = 0; r <= rows; r += 2) { drawStar(c * CELL, r * CELL, 6, 3, starColor); } } // Spawn glowing tiles if (glows.length < MAX_GLOWS && Math.random() < 0.03) { spawnGlow(); } // Draw glowing tiles for (var i = glows.length - 1; i >= 0; i--) { var g = glows[i]; g.life++; var progress = g.life / g.maxLife; var opacity = progress < 0.5 ? progress * 2 : (1 - progress) * 2; ctx.fillStyle = glowColor; ctx.globalAlpha = opacity; ctx.fillRect(g.col * CELL + 1, g.row * CELL + 1, CELL - 2, CELL - 2); // Subtle shadow glow ctx.shadowColor = glowColor; ctx.shadowBlur = 20; ctx.fillRect(g.col * CELL + 1, g.row * CELL + 1, CELL - 2, CELL - 2); ctx.shadowBlur = 0; ctx.globalAlpha = 1; if (g.life >= g.maxLife) glows.splice(i, 1); } requestAnimationFrame(animateGrid); } resizeCanvas(); animateGrid(); var resizeTimeout; window.addEventListener('resize', function () { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(resizeCanvas, 150); }); } // ═══════════════════════════════════════════════════════════════════════════ // SCROLL REVEAL — IntersectionObserver // ═══════════════════════════════════════════════════════════════════════════ var revealEls = document.querySelectorAll('.nx-reveal, .nx-reveal-scale'); if ('IntersectionObserver' in window) { var observer = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { entry.target.classList.add('is-visible'); observer.unobserve(entry.target); } }); }, { threshold: 0.15, rootMargin: '0px 0px -40px 0px' }); revealEls.forEach(function (el) { observer.observe(el); }); } else { // Fallback: show all immediately revealEls.forEach(function (el) { el.classList.add('is-visible'); }); } // ═══════════════════════════════════════════════════════════════════════════ // COUNTER ANIMATION — Stat cards // ═══════════════════════════════════════════════════════════════════════════ function animateCounter(el, target, format, suffix, duration) { var start = 0; var startTime = null; function step(timestamp) { if (!startTime) startTime = timestamp; var progress = Math.min((timestamp - startTime) / duration, 1); // Ease out cubic var eased = 1 - Math.pow(1 - progress, 3); var current = Math.floor(eased * target); if (format.indexOf('M') !== -1) { // e.g. "1.5M" or "15.8M" var decimals = target >= 10e6 ? 1 : 1; el.textContent = (current / 1e6).toFixed(decimals) + 'M' + suffix; } else if (format.indexOf('K') !== -1) { // e.g. "85K" or "304K" el.textContent = Math.floor(current / 1e3) + 'K' + suffix; } else { el.textContent = current + suffix; } if (progress < 1) requestAnimationFrame(step); } requestAnimationFrame(step); } // Trigger counters when stats become visible var statCards = document.querySelectorAll('.stat-card'); if (statCards.length && 'IntersectionObserver' in window) { var statsObserver = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { var numEl = entry.target.querySelector('.number'); if (numEl && !numEl._animated) { numEl._animated = true; var target = parseInt(numEl.getAttribute('data-target'), 10) || 0; var suffix = numEl.getAttribute('data-suffix') || ''; var format = numEl.getAttribute('data-format') || 'num'; animateCounter(numEl, target, format, suffix, 2000); } statsObserver.unobserve(entry.target); } }); }, { threshold: 0.5 }); statCards.forEach(function (card) { statsObserver.observe(card); }); } // ═══════════════════════════════════════════════════════════════════════════ // TYPEWRITER EFFECT // ═══════════════════════════════════════════════════════════════════════════ var typewriterEl = document.getElementById('typewriterText'); if (typewriterEl) { var phrases = [ 'POS + Inventario + CFDI 4.0 + Contabilidad', 'Catalogo TecDoc: 1.5M+ partes, 304K aftermarket', '15.8M cross-references OEM ↔ aftermarket', 'Chatbot IA con voz, foto y diagnostico', 'WhatsApp Business integrado', 'Busca por VIN, placas o numero de parte', 'Marketplace B2B: bodegas ↔ talleres', 'PWA + Android + modo kiosko + offline', ]; var phraseIdx = 0; var charIdx = 0; var isDeleting = false; var typingSpeed = 50; function typeStep() { var current = phrases[phraseIdx]; if (!isDeleting) { typewriterEl.textContent = current.substring(0, charIdx + 1); charIdx++; if (charIdx >= current.length) { isDeleting = true; setTimeout(typeStep, 2000); // Pause before deleting return; } setTimeout(typeStep, typingSpeed); } else { typewriterEl.textContent = current.substring(0, charIdx); charIdx--; if (charIdx < 0) { isDeleting = false; charIdx = 0; phraseIdx = (phraseIdx + 1) % phrases.length; setTimeout(typeStep, 400); return; } setTimeout(typeStep, 30); } } // Start after hero reveals setTimeout(typeStep, 1200); } // ═══════════════════════════════════════════════════════════════════════════ // BRAND MARQUEE — Load from API + duplicate for seamless loop // ═══════════════════════════════════════════════════════════════════════════ var marqueeContainer = document.getElementById('brandsMarquee'); function buildMarquee(brands) { var html = ''; brands.forEach(function (b) { html += '' + escHtml(b.name_brand) + ''; }); // Duplicate for seamless loop marqueeContainer.innerHTML = html + html; } function escHtml(s) { var d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; } // Fallback brands in case API fails var fallbackBrands = [ 'Toyota', 'Nissan', 'Ford', 'Volkswagen', 'Honda', 'Chevrolet', 'Hyundai', 'Kia', 'Mazda', 'BMW', 'Mercedes-Benz', 'Audi', 'Renault', 'Jeep', 'Dodge', 'Ram', 'Subaru', 'Mitsubishi', 'Suzuki', 'Peugeot', 'Volvo', 'Fiat', 'Chrysler', 'Acura', 'Infiniti', 'Lexus', 'Lincoln', 'Buick', 'GMC', 'Cadillac', 'Porsche', 'Mini', 'Seat', 'Alfa Romeo', 'Land Rover', 'Jaguar' ]; fetch('/api/catalog/brands') .then(function (r) { return r.json(); }) .then(function (brands) { if (brands && brands.length > 0) { buildMarquee(brands); } else { buildMarquee(fallbackBrands.map(function (n, i) { return { id_brand: i, name_brand: n }; })); } }) .catch(function () { buildMarquee(fallbackBrands.map(function (n, i) { return { id_brand: i, name_brand: n }; })); }); // ── Live hero stats from API ── // The landing has 4 stat cards with data-format tags that identify them. // The counter animation runs on whatever data-target is set at observe // time, so we update all 4 targets BEFORE the IntersectionObserver fires. fetch('/api/catalog/stats') .then(function (r) { return r.json(); }) .then(function (d) { // Maps data-format (the card's identifier) → the JSON key to pull. var statMap = [ { format: '1.5M', key: 'parts' }, // OEM parts { format: '304K', key: 'aftermarket_parts' }, // Aftermarket { format: '15.8M', key: 'cross_references' }, // Cross-refs { format: 'num', key: 'brands' }, // Brand count ]; statMap.forEach(function (s) { var el = document.querySelector('[data-format="' + s.format + '"]'); var value = d[s.key]; if (el && typeof value === 'number' && value > 0) { el.setAttribute('data-target', value); // If the animation already ran (cached, instant DOM), snap to the // real value so users see the live number instead of the stale // hardcoded default. if (el._animated) { if (s.format.indexOf('M') !== -1) { el.textContent = (value / 1e6).toFixed(1) + 'M+'; } else if (s.format.indexOf('K') !== -1) { el.textContent = Math.floor(value / 1e3) + 'K+'; } else { el.textContent = String(value); } } } }); }) .catch(function () { /* fallback: hardcoded data-target values stay */ }); // ═══════════════════════════════════════════════════════════════════════════ // SMOOTH SCROLL for nav links // ═══════════════════════════════════════════════════════════════════════════ document.querySelectorAll('.header-nav a[href^="#"]').forEach(function (a) { a.addEventListener('click', function (e) { e.preventDefault(); var target = document.querySelector(a.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }); // ═══════════════════════════════════════════════════════════════════════════ // HEADER — Solid on scroll // ═══════════════════════════════════════════════════════════════════════════ var header = document.querySelector('.site-header'); if (header) { window.addEventListener('scroll', function () { if (window.scrollY > 80) { header.style.background = 'var(--glass-bg-strong)'; } else { header.style.background = 'var(--glass-bg)'; } }, { passive: true }); } })();