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