v12-ui
Version:
A React component library with a focus on utility-first design and accessibility.
172 lines (171 loc) • 6.16 kB
JavaScript
import { jsx as p } from "react/jsx-runtime";
import { useRef as w, useEffect as P } from "react";
import { useDataTheme as b } from "../Hooks/useDataTheme.js";
import { cn as I } from "../utils/utils.js";
class C {
canvas;
ctx;
particles = [];
mouse = null;
animationId = null;
dpr;
config;
constructor(t, s) {
this.canvas = t;
const i = t.getContext("2d");
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.generateParticles(), this.animate = this.animate.bind(this), this.animate();
}
setupCanvas() {
const t = this.canvas.getBoundingClientRect();
this.canvas.width = t.width * this.dpr, this.canvas.height = t.height * this.dpr, this.canvas.style.width = `${t.width}px`, this.canvas.style.height = `${t.height}px`, this.ctx.scale(this.dpr, this.dpr), this.ctx.imageSmoothingEnabled = !0, this.ctx.imageSmoothingQuality && (this.ctx.imageSmoothingQuality = "high");
}
createTextMask() {
return this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height), this.ctx.font = `bold ${this.config.fontSize || 50}px ${this.config.fontFamily || "sans-serif"}`, this.ctx.textAlign = "center", this.ctx.textBaseline = "middle", this.ctx.fillStyle = `${this.config.color || "#fff"}`, this.ctx.fillText(this.config.text, this.canvas.width / 2, this.canvas.height / 2), this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
}
extractPositions(t) {
const s = [], { data: i, width: a, height: f } = t, o = Math.max(1, 4);
let e = 0;
for (let n = 0; n < i.length; n += 4) {
const [l] = i.slice(n, n + 4);
l > 50 && e++;
}
const c = Math.max(
1,
Math.floor(Math.sqrt(e / (this.config.particles || 6e3)))
), h = Math.min(o, c);
for (let n = 0; n < f; n += h)
for (let l = 0; l < a; l += h) {
const d = (n * a + l) * 4, [x] = i.slice(d, d + 4);
x > 50 && s.push({
x: l / this.dpr,
y: n / this.dpr
});
}
return s;
}
generateParticles() {
const t = this.createTextMask(), s = this.extractPositions(t);
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((a) => ({
tx: a.x,
// posición final (píxel real)
ty: a.y,
// posición inicial: fuera del canvas
x: a.x + (Math.random() - 0.5) * i,
y: a.y + (Math.random() - 0.5) * i,
vx: 0,
vy: 0,
phase: Math.random() * Math.PI * 2,
// 0…2π
age: 0,
isImmune: Math.random() < 0.05
// ≈ 5 % no se repelen
}));
}
updateParticle(t) {
const s = t.tx - t.x, i = t.ty - t.y;
if (this.mouse && this.config.attractMode) {
const r = this.mouse.x - t.x, o = this.mouse.y - t.y, e = Math.hypot(r, o);
if (e < this.config.repulsion && e > 0) {
const h = 10 * Math.max(0, 1 - e / this.config.repulsion);
t.vx += r / e * h, t.vy += o / e * h;
}
} else if (this.mouse && !this.config.attractMode) {
const r = this.mouse.x - t.x, o = this.mouse.y - t.y, e = Math.hypot(r, o);
if (e < this.config.repulsion && e > 0) {
let c;
this.config.trace ? t.isImmune ? c = (this.config.repulsion - e) / this.config.repulsion * 0.1 : c = Math.max(0, 1 - e / this.config.repulsion) : c = Math.max(0, 1 - e / this.config.repulsion);
const h = 20 * c;
t.vx -= r / e * h, t.vy -= o / e * h;
}
}
const a = 0.04, f = 0.73;
t.vx += s * a, t.vy += i * a, t.vx *= f, t.vy *= f, t.x += t.vx, t.y += t.vy;
}
drawParticles() {
const t = this.canvas.width / this.dpr, s = this.canvas.height / this.dpr;
this.ctx.clearRect(0, 0, t, s);
for (const i of this.particles) {
this.updateParticle(i), i.age += 1;
const a = (Math.sin(i.phase + i.age * 0.15) + 1) / 2;
this.ctx.fillStyle = this.config.color || "#fff", this.config.glow && (this.ctx.globalAlpha = a), 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 = (t) => {
const s = this.canvas.getBoundingClientRect();
this.mouse = {
x: (t.clientX - s.left) * (this.canvas.width / s.width),
y: (t.clientY - s.top) * (this.canvas.height / s.height)
};
};
handleMouseLeave = () => {
this.mouse = null;
};
animate() {
this.drawParticles(), this.canvas.addEventListener("mousemove", this.handleMouseMove), this.canvas.addEventListener("mouseleave", this.handleMouseLeave), this.animationId = requestAnimationFrame(this.animate);
}
destroy() {
this.canvas.removeEventListener("mousemove", this.handleMouseMove), this.canvas.removeEventListener("mouseleave", this.handleMouseLeave), this.animationId && cancelAnimationFrame(this.animationId);
}
}
function A({
text: m = "Magic Text",
particles: t = 500,
dotSize: s = 0.9,
repulsion: i = 50,
friction: a = 0.82,
returnSpeed: f = 0.01,
fontFamily: r = "sans-serif",
fontSize: o = 50,
color: e,
glow: c = !0,
trace: h = !0,
attractMode: n = !1,
className: l,
...d
}) {
const { theme: x } = b(), g = e || (x === "dark" ? "#fff" : "#000");
console.log("Theme:", x, "Color detected:", g);
const v = w(null);
return P(() => {
const u = v.current;
if (!u) return;
if (!m) {
console.warn("MagicText: text needed");
return;
}
const M = {
text: m,
particles: t,
dotSize: s,
repulsion: i,
friction: a,
returnSpeed: f,
fontFamily: r,
fontSize: o,
color: g,
glow: c,
trace: h,
attractMode: n
}, y = new C(u, M);
return () => {
y && y.destroy();
};
}, [m, t, s, i, a, f, r, o, x, e, c, h, n]), /* @__PURE__ */ p(
"canvas",
{
ref: v,
...d,
className: I("block mx-auto overflow-visible z-0 w-fit h-fit", l)
}
);
}
export {
A as MagicText
};