v12-ui
Version:
A React component library with a focus on utility-first design and accessibility.
223 lines (222 loc) • 8.92 kB
JavaScript
import { jsx as L } from "react/jsx-runtime";
import { useRef as w, useEffect as E } from "react";
import { cn as y } from "../utils/utils.js";
class p {
canvas;
ctx;
particles = [];
mouse = null;
animationId = null;
dpr;
config;
imageElement = null;
constructor(e, i) {
this.canvas = e;
const a = e.getContext("2d", { willReadFrequently: !0 });
if (!a) throw new Error("No se pudo obtener el contexto 2D del canvas");
this.ctx = a, this.dpr = window.devicePixelRatio || 1, this.config = i, this.setupCanvas(), this.initializeEffect();
}
async initializeEffect() {
try {
this.config.imageUrl ? (console.log("MagicLogo: Cargando imagen desde URL:", this.config.imageUrl), await this.loadImage(this.config.imageUrl)) : this.config.svgContent ? (console.log("MagicLogo: Cargando SVG como imagen"), await this.loadSVGAsImage(this.config.svgContent)) : this.config.imageElement && (console.log("MagicLogo: Cargando elemento de imagen existente"), await this.loadImageElement(this.config.imageElement)), console.log("MagicLogo: Imagen cargada exitosamente, generando partículas..."), this.generateParticles(), this.setupEventListeners(), this.animate = this.animate.bind(this), this.animate();
} catch (e) {
console.error("MagicLogo: Error al inicializar el efecto:", e);
}
}
async loadImage(e) {
return await new Promise((i, a) => {
const t = new Image(), n = () => {
this.imageElement = t, t.removeEventListener("load", n), t.removeEventListener("error", s), i();
}, s = (o) => {
t.removeEventListener("load", n), t.removeEventListener("error", s), a(new Error(`Error al cargar la imagen desde URL: ${e} - ${o.type}`));
};
t.addEventListener("load", n), t.addEventListener("error", s), t.crossOrigin = "anonymous", t.src = e, setTimeout(() => {
t.removeEventListener("load", n), t.removeEventListener("error", s), a(new Error(`Timeout: La imagen desde URL ${e} tardó demasiado en cargar`));
}, 1e4);
});
}
async loadImageElement(e) {
return await new Promise((i, a) => {
if (e.complete && e.naturalWidth > 0)
this.imageElement = e, i();
else {
const t = () => {
this.imageElement = e, e.removeEventListener("load", t), e.removeEventListener("error", n), i();
}, n = (s) => {
e.removeEventListener("load", t), e.removeEventListener("error", n), a(new Error(`Error al cargar la imagen: ${s.type}`));
};
e.addEventListener("load", t), e.addEventListener("error", n), setTimeout(() => {
e.removeEventListener("load", t), e.removeEventListener("error", n), a(new Error("Timeout: La imagen tardó demasiado en cargar"));
}, 1e4);
}
});
}
async loadSVGAsImage(e) {
return await new Promise((i, a) => {
const t = new Blob([e], { type: "image/svg+xml" }), n = URL.createObjectURL(t), s = new Image(), o = () => {
this.imageElement = s, URL.revokeObjectURL(n), s.removeEventListener("load", o), s.removeEventListener("error", r), i();
}, r = (l) => {
URL.revokeObjectURL(n), s.removeEventListener("load", o), s.removeEventListener("error", r), a(new Error(`Error al cargar SVG como imagen: ${l.type}`));
};
s.addEventListener("load", o), s.addEventListener("error", r), s.crossOrigin = "anonymous", s.src = n, setTimeout(() => {
URL.revokeObjectURL(n), s.removeEventListener("load", o), s.removeEventListener("error", r), a(new Error("Timeout: El SVG tardó demasiado en cargar"));
}, 1e4);
});
}
setupCanvas() {
const e = this.canvas.getBoundingClientRect();
this.canvas.width = e.width * this.dpr, this.canvas.height = e.height * this.dpr, this.canvas.style.width = `${e.width}px`, this.canvas.style.height = `${e.height}px`, this.ctx.scale(this.dpr, this.dpr), this.ctx.imageSmoothingEnabled = !0, this.ctx.imageSmoothingQuality && (this.ctx.imageSmoothingQuality = "high");
}
createImageMask() {
return this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height), this.imageElement && this.drawImageToCanvas(this.imageElement), this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
}
drawImageToCanvas(e) {
const i = this.canvas.width / this.dpr, a = this.canvas.height / this.dpr, n = Math.min(i / e.naturalWidth, a / e.naturalHeight) * 0.8, s = e.naturalWidth * n, o = e.naturalHeight * n, r = (i - s) / 2, l = (a - o) / 2;
this.ctx.drawImage(e, r, l, s, o);
}
extractPositions(e) {
const i = [], { data: a, width: t, height: n } = e, o = Math.max(1, 4);
let r = 0;
for (let c = 0; c < a.length; c += 4) {
const [h, g, d, v] = a.slice(c, c + 4);
v > 50 && (h < 220 || g < 220 || d < 220) && r++;
}
const l = Math.max(
1,
Math.floor(Math.sqrt(r / (this.config.particles || 6e3)))
), m = Math.min(o, l);
for (let c = 0; c < n; c += m)
for (let h = 0; h < t; h += m) {
const g = (c * t + h) * 4, [d, v, u, x] = a.slice(g, g + 4);
x > 50 && (d < 220 || v < 220 || u < 220) && i.push({
x: h / this.dpr,
y: c / this.dpr,
color: `rgb(${d},${v},${u})`
});
}
return i;
}
generateParticles() {
if (!this.imageElement) {
console.warn("No hay imagen cargada para generar partículas");
return;
}
const e = this.createImageMask(), i = this.extractPositions(e);
if (i.length === 0) {
console.warn("No se encontraron píxeles visibles en la imagen");
return;
}
const a = Math.max(this.canvas.width, this.canvas.height) * 2;
this.particles = i.map((t, n) => ({
tx: t.x,
// posición final (píxel real)
ty: t.y,
// posición inicial: fuera del canvas
x: t.x + (Math.random() - 0.5) * a,
y: t.y + (Math.random() - 0.5) * a,
vx: 0,
vy: 0,
color: t.color,
phase: Math.random() * Math.PI * 2,
// 0…2π
age: 0,
order: n
}));
}
updateParticle(e) {
const i = e.tx - e.x, a = e.ty - e.y;
if (this.mouse) {
const s = this.mouse.x - e.x, o = this.mouse.y - e.y, r = Math.hypot(s, o), m = e.order * 0.08, h = (r - m) * 0.1;
r > 0 && (e.vx += s / r * h, e.vy += o / r * h);
}
const t = 0.04, n = 0.73;
e.vx += i * t, e.vy += a * t, e.vx *= n, e.vy *= n, e.x += e.vx, e.y += e.vy;
}
drawParticles() {
const e = this.canvas.width / this.dpr, i = this.canvas.height / this.dpr;
this.ctx.clearRect(0, 0, e, i);
for (const a of this.particles) {
this.updateParticle(a), a.age += 1;
const t = (Math.sin(a.phase + a.age * 0.15) + 1) / 2;
this.ctx.fillStyle = this.config.color || a.color || "#fff", this.config.glow && (this.ctx.globalAlpha = t), this.ctx.beginPath(), this.ctx.arc(a.x, a.y, this.config.dotSize, 0, Math.PI * 2), this.ctx.fill();
}
this.ctx.globalAlpha = 1;
}
handleMouseMove = (e) => {
const i = this.canvas.getBoundingClientRect();
this.mouse = {
x: (e.clientX - i.left) * (this.canvas.width / this.dpr / i.width),
y: (e.clientY - i.top) * (this.canvas.height / this.dpr / i.height)
};
};
handleMouseLeave = () => {
this.mouse = null;
};
animate() {
this.drawParticles(), this.animationId = requestAnimationFrame(this.animate);
}
setupEventListeners() {
this.canvas.addEventListener("mousemove", this.handleMouseMove), this.canvas.addEventListener("mouseleave", this.handleMouseLeave);
}
destroy() {
this.canvas.removeEventListener("mousemove", this.handleMouseMove), this.canvas.removeEventListener("mouseleave", this.handleMouseLeave), this.animationId && cancelAnimationFrame(this.animationId);
}
}
function R({
imageUrl: f,
imageElement: e,
svgContent: i,
particles: a = 750,
dotSize: t = 0.9,
repulsion: n = 80,
friction: s = 0.82,
returnSpeed: o = 0.01,
color: r,
glow: l = !0,
className: m,
...c
}) {
const h = w(null);
return E(() => {
const g = h.current;
if (!g) return;
if (e && (!e.complete || e.naturalWidth === 0)) {
console.warn("MagicLogo: imageElement no está completamente cargado");
return;
}
let d;
if (f != null)
d = { imageUrl: f };
else if (e != null)
d = { imageElement: e };
else if (i != null)
d = { svgContent: i };
else {
console.warn("MagicLogo: falta una fuente de imagen");
return;
}
const v = {
particles: a,
dotSize: t,
repulsion: n,
friction: s,
returnSpeed: o,
color: r,
glow: l,
...d
}, u = new p(g, v);
return () => {
u && u.destroy();
};
}, [f, e, i, a, t, n, s, o, r, l]), /* @__PURE__ */ L(
"canvas",
{
ref: h,
...c,
className: y("block mx-auto overflow-visible z-0 w-auto h-auto", m)
}
);
}
export {
R as MagicMouseFollower
};