// Supernova — slow, cinematic starfield + satisfying interactivity. // --------------------------------------------------------------- // Starfield (slow, gentle parallax drift) // --------------------------------------------------------------- (function () { const canvas = document.getElementById('starfield'); if (!canvas) return; const ctx = canvas.getContext('2d'); let stars = [], W, H; function resize() { W = canvas.width = innerWidth; H = canvas.height = innerHeight; } function makeStars() { stars = []; const n = Math.min(260, Math.floor(W * H / 6000)); for (let i = 0; i < n; i++) { stars.push({ x: Math.random() * W, y: Math.random() * H, r: Math.random() * 1.3 + 0.3, tw: Math.random() * Math.PI * 2, twSpeed: 0.4 + Math.random() * 0.9, drift: 0.04 + Math.random() * 0.18, hue: Math.random(), }); } } let t = 0; function frame() { t += 0.016; ctx.clearRect(0, 0, W, H); for (const s of stars) { s.y += s.drift; s.x += s.drift * 0.15; if (s.y > H + 4) { s.y = -4; s.x = Math.random() * W; } if (s.x > W + 4) s.x = -4; const tw = 0.55 + 0.45 * Math.sin(t * s.twSpeed + s.tw); const a = tw * (0.5 + 0.5 * s.r / 1.6); let col = s.hue < 0.72 ? `rgba(207,224,255,${a})` : s.hue < 0.86 ? `rgba(186,160,255,${a})` : s.hue < 0.95 ? `rgba(160,200,255,${a})` : `rgba(255,180,220,${a})`; ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fillStyle = col; ctx.fill(); } requestAnimationFrame(frame); } function shootingStar() { const x = Math.random() * W * 0.7 + W * 0.15, y = Math.random() * H * 0.4; const len = 90 + Math.random() * 60, ang = Math.PI * 0.75; let p = 0; const step = () => { p += 0.04; const cx = x + Math.cos(ang) * len * p, cy = y + Math.sin(ang) * len * p; const g = ctx.createLinearGradient(cx, cy, cx - Math.cos(ang) * len * 0.4, cy - Math.sin(ang) * len * 0.4); g.addColorStop(0, 'rgba(255,255,255,0.85)'); g.addColorStop(1, 'rgba(255,255,255,0)'); ctx.strokeStyle = g; ctx.lineWidth = 1.6; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx - Math.cos(ang) * len * 0.4, cy - Math.sin(ang) * len * 0.4); ctx.stroke(); if (p < 1) requestAnimationFrame(step); }; step(); setTimeout(shootingStar, 9000 + Math.random() * 12000); } resize(); makeStars(); requestAnimationFrame(frame); setTimeout(shootingStar, 4000); addEventListener('resize', () => { resize(); makeStars(); }); })(); // --------------------------------------------------------------- // Confetti celebration (gentle galaxy burst) // --------------------------------------------------------------- function celebrate() { const canvas = document.createElement('canvas'); canvas.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:9999'; document.body.appendChild(canvas); const ctx = canvas.getContext('2d'); canvas.width = innerWidth; canvas.height = innerHeight; const colors = ['#7b2ff7', '#2b6cff', '#ff2d95', '#ffffff', '#cfe0ff', '#7cffb0']; const parts = []; for (let i = 0; i < 150; i++) { parts.push({ x: Math.random() * canvas.width, y: -20 - Math.random() * canvas.height * 0.4, r: Math.random() * 4 + 2, vy: 1.4 + Math.random() * 2.2, vx: (Math.random() - 0.5) * 1.6, rot: Math.random() * Math.PI * 2, vr: (Math.random() - 0.5) * 0.07, color: colors[Math.floor(Math.random() * colors.length)], star: Math.random() < 0.45, }); } let frames = 0; function starPath(c, r) { c.beginPath(); for (let i = 0; i < 5; i++) { const a = (i * 4 * Math.PI) / 5 - Math.PI / 2; const a2 = a + (2 * Math.PI) / 5; c.lineTo(Math.cos(a) * r, Math.sin(a) * r); c.lineTo(Math.cos((a + a2) / 2) * r * 0.4, Math.sin((a + a2) / 2) * r * 0.4); } c.closePath(); c.fill(); } function step() { ctx.clearRect(0, 0, canvas.width, canvas.height); frames++; for (const p of parts) { p.x += p.vx; p.y += p.vy; p.rot += p.vr; p.vy += 0.015; ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot); ctx.globalAlpha = Math.max(0, 1 - frames / 250); ctx.fillStyle = p.color; if (p.star) starPath(ctx, p.r); else { ctx.beginPath(); ctx.arc(0, 0, p.r, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); } if (frames < 270 && parts.some(p => p.y < canvas.height + 40)) requestAnimationFrame(step); else canvas.remove(); } requestAnimationFrame(step); } // --------------------------------------------------------------- // Subtle 3D card tilt (slow, max 4deg) // --------------------------------------------------------------- (function () { const cards = document.querySelectorAll('.card'); cards.forEach(card => { card.addEventListener('mousemove', e => { const r = card.getBoundingClientRect(); const px = (e.clientX - r.left) / r.width - 0.5; const py = (e.clientY - r.top) / r.height - 0.5; card.style.transform = `perspective(900px) rotateX(${(-py * 4).toFixed(2)}deg) rotateY(${(px * 4).toFixed(2)}deg) translateY(-3px)`; }); card.addEventListener('mouseleave', () => { card.style.transform = ''; }); }); })(); // --------------------------------------------------------------- // Scroll reveal (fade + rise, gentle) // --------------------------------------------------------------- (function () { const els = document.querySelectorAll('.card, .stat, .seo-block, .section-title'); if (!('IntersectionObserver' in window)) return; const io = new IntersectionObserver(entries => { entries.forEach(en => { if (en.isIntersecting) { en.target.classList.add('revealed'); io.unobserve(en.target); } }); }, { threshold: 0.08 }); els.forEach(el => { el.classList.add('reveal'); io.observe(el); }); })(); // --------------------------------------------------------------- // Button ripple // --------------------------------------------------------------- (function () { document.addEventListener('click', e => { const btn = e.target.closest('.btn'); if (!btn) return; const r = btn.getBoundingClientRect(); const rip = document.createElement('span'); const size = Math.max(r.width, r.height) * 1.6; rip.style.cssText = `position:absolute;border-radius:50%;width:${size}px;height:${size}px;` + `left:${e.clientX - r.left - size / 2}px;top:${e.clientY - r.top - size / 2}px;` + `background:radial-gradient(circle,rgba(255,255,255,0.45),rgba(255,255,255,0));` + `transform:scale(0);animation:ripple .6s ease-out forwards;pointer-events:none`; btn.style.position = 'relative'; btn.style.overflow = 'hidden'; btn.appendChild(rip); setTimeout(() => rip.remove(), 650); }); })(); // --------------------------------------------------------------- // Quantity slider — animated price counter + value tween // --------------------------------------------------------------- (function () { const slider = document.getElementById('qty'); const priceEl = document.getElementById('live-price'); const qtyEl = document.getElementById('qty-val'); if (!slider || !priceEl) return; const serviceId = parseInt(slider.dataset.service, 10); let shown = 0; function render(target, qty) { const start = performance.now(), from = shown, dur = 320; function step(t) { const p = Math.min(1, (t - start) / dur); const eased = 1 - Math.pow(1 - p, 3); const val = from + (target - from) * eased; priceEl.innerHTML = `$${val.toFixed(2)} for ${qty.toLocaleString()} ×`; if (p < 1) requestAnimationFrame(step); else shown = target; } requestAnimationFrame(step); if (qtyEl) qtyEl.textContent = qty.toLocaleString(); } function update() { const q = parseInt(slider.value, 10); fetch(`/api/quote/${serviceId}/${q}`) .then(r => r.json()).then(d => render(d.retail, q)) .catch(() => {}); } slider.addEventListener('input', update); update(); })(); // --------------------------------------------------------------- // Order success celebration (detect ?launched=1) // --------------------------------------------------------------- (function () { if (new URLSearchParams(location.search).has('launched')) { const banner = document.getElementById('launch-banner'); if (banner) { banner.classList.add('show'); setTimeout(() => banner.classList.remove('show'), 5000); } celebrate(); } })(); // --------------------------------------------------------------- // Launch whoosh (Web Audio synth — subtle rising sweep) // --------------------------------------------------------------- function launchSound() { try { const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return; const ctx = new AC(); const dur = 0.55; const buf = ctx.createBuffer(1, ctx.sampleRate * dur, ctx.sampleRate); const d = buf.getChannelData(0); for (let i = 0; i < d.length; i++) { const t = i / d.length; d[i] = (Math.random() * 2 - 1) * Math.pow(t, 1.6) * (1 - t) * 0.9; } const src = ctx.createBufferSource(); src.buffer = buf; const filt = ctx.createBiquadFilter(); filt.type = 'bandpass'; filt.Q.value = 1.1; filt.frequency.setValueAtTime(280, ctx.currentTime); filt.frequency.exponentialRampToValueAtTime(2600, ctx.currentTime + dur); const gain = ctx.createGain(); gain.gain.setValueAtTime(0.0001, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.22, ctx.currentTime + dur * 0.28); gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + dur); src.connect(filt).connect(gain).connect(ctx.destination); src.start(); setTimeout(() => ctx.close(), dur * 1000 + 200); } catch (e) {} } (function () { const btn = document.querySelector('.btn.launch'); if (btn) btn.addEventListener('click', launchSound); })(); // --------------------------------------------------------------- // Orbital planet picker — drag to spin, slow auto-rotate // --------------------------------------------------------------- (function () { const ring = document.getElementById('orbit'); if (!ring) return; const planets = Array.from(ring.querySelectorAll('.planet-orb')); let angle = 0, vel = 0.06, dragging = false, lastX = 0; function layout() { const R = 175; planets.forEach(p => { const i = parseFloat(p.dataset.i); const a = ((i * 45 + angle) * Math.PI) / 180; p.style.transform = `translate(${(Math.cos(a) * R).toFixed(1)}px, ${(Math.sin(a) * R).toFixed(1)}px)`; }); } function tick() { if (!dragging) { vel += (0.06 - vel) * 0.03; angle += vel; } layout(); requestAnimationFrame(tick); } const down = x => { dragging = true; lastX = x; ring.style.cursor = 'grabbing'; }; const move = x => { if (!dragging) return; vel = (x - lastX) * 0.18; angle += vel; lastX = x; layout(); }; const up = () => { dragging = false; ring.style.cursor = 'grab'; }; ring.style.cursor = 'grab'; ring.addEventListener('mousedown', e => down(e.clientX)); window.addEventListener('mousemove', e => move(e.clientX)); window.addEventListener('mouseup', up); ring.addEventListener('touchstart', e => down(e.touches[0].clientX), { passive: true }); window.addEventListener('touchmove', e => move(e.touches[0].clientX), { passive: true }); window.addEventListener('touchend', up); requestAnimationFrame(tick); })(); // --------------------------------------------------------------- // Delivery ticker (dashboard live progress, poll every 45s) // --------------------------------------------------------------- (function () { const wrap = document.getElementById('ticker'); if (!wrap) return; const fmt = n => n.toLocaleString(); function bar(name, delivered, total, status) { const pct = total > 0 ? Math.min(100, (delivered / total) * 100) : 0; const label = status === 'completed' ? '✅ Done' : status === 'canceled' ? 'Cancelled' : status === 'inprogress' ? '⚡ Delivering…' : '⏳ Queued'; return `