// ============================================================ // AMOR & ROMA — Hero: campo de humo/partículas orgánicas // Canvas 2D, reactivo al cursor, pasa detrás del texto. // ============================================================ (function () { "use strict"; const canvas = document.getElementById("hero-canvas"); if (!canvas) return; const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const ctx = canvas.getContext("2d"); let width, height, dpr; let pointer = { x: -9999, y: -9999, active: false }; let particles = []; const COUNT = reduceMotion ? 0 : Math.min(70, Math.floor(window.innerWidth / 16)); function themeColors() { const dark = document.documentElement.getAttribute("data-theme") === "dark"; return dark ? [ [186, 18, 77], // magenta [0, 109, 143], // teal web [223, 189, 185], // blush ] : [ [0, 63, 82], // teal deep [186, 18, 77], // magenta [223, 189, 185], // blush ]; } function resize() { const rect = canvas.parentElement.getBoundingClientRect(); dpr = Math.min(window.devicePixelRatio || 1, 2); width = rect.width; height = rect.height; canvas.width = width * dpr; canvas.height = height * dpr; canvas.style.width = width + "px"; canvas.style.height = height + "px"; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } function rand(min, max) { return Math.random() * (max - min) + min; } function makeParticle() { const colors = themeColors(); const c = colors[Math.floor(Math.random() * colors.length)]; return { x: rand(0, width), y: rand(0, height), baseX: 0, baseY: 0, vx: rand(-0.12, 0.12), vy: rand(-0.18, -0.05), r: rand(50, 130), alpha: rand(0.03, 0.09), color: c, wander: rand(0, Math.PI * 2), wanderSpeed: rand(0.002, 0.006), }; } function init() { resize(); particles = new Array(COUNT).fill(0).map(makeParticle); } function step() { ctx.clearRect(0, 0, width, height); particles.forEach((p) => { // wander drift (turbulence) p.wander += p.wanderSpeed; p.x += p.vx + Math.cos(p.wander) * 0.15; p.y += p.vy + Math.sin(p.wander) * 0.1; // cursor influence — smoke curls away/around pointer if (pointer.active) { const dx = p.x - pointer.x; const dy = p.y - pointer.y; const dist = Math.sqrt(dx * dx + dy * dy) || 1; const influence = Math.max(0, 220 - dist) / 220; if (influence > 0) { p.x += (dx / dist) * influence * 2.4; p.y += (dy / dist) * influence * 2.4; } } // wrap around edges softly if (p.y < -p.r) p.y = height + p.r; if (p.x < -p.r) p.x = width + p.r; if (p.x > width + p.r) p.x = -p.r; if (p.y > height + p.r) p.y = -p.r; const [r, g, b] = p.color; const gradient = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r); gradient.addColorStop(0, `rgba(${r},${g},${b},${p.alpha})`); gradient.addColorStop(1, `rgba(${r},${g},${b},0)`); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill(); }); if (!reduceMotion) requestAnimationFrame(step); } const parent = canvas.parentElement; parent.addEventListener("mousemove", (e) => { const rect = parent.getBoundingClientRect(); pointer.x = e.clientX - rect.left; pointer.y = e.clientY - rect.top; pointer.active = true; }); parent.addEventListener("mouseleave", () => { pointer.active = false; }); parent.addEventListener( "touchmove", (e) => { const rect = parent.getBoundingClientRect(); const t = e.touches[0]; if (!t) return; pointer.x = t.clientX - rect.left; pointer.y = t.clientY - rect.top; pointer.active = true; }, { passive: true } ); window.addEventListener("resize", () => { resize(); }); // Redraw once (or restart loop) if theme changes, to refresh palette const observer = new MutationObserver(() => { particles.forEach((p) => { const colors = themeColors(); p.color = colors[Math.floor(Math.random() * colors.length)]; }); }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] }); init(); if (reduceMotion) { // Static single soft render for reduced motion users ctx.clearRect(0, 0, width, height); } else { step(); } })();