inicio
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
// ============================================================
|
||||
// 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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user