jl-particle-interactive 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ import { ClickMode } from '../hooks/useParticleInteraction';
2
+ export declare class Particle {
3
+ x: number;
4
+ y: number;
5
+ vx: number;
6
+ vy: number;
7
+ targetX: number;
8
+ targetY: number;
9
+ baseColor: string;
10
+ opacity: number;
11
+ size: number;
12
+ sizeMultiplier: number;
13
+ friction: number;
14
+ ease: number;
15
+ easeMultiplier: number;
16
+ floatSpeed: number;
17
+ floatOffset: number;
18
+ constructor(w: number, h: number, color?: string | string[]);
19
+ update(time: number, isActive: boolean, mx?: number | null, my?: number | null, isMouseDown?: boolean, isMagnet?: boolean, clickMode?: ClickMode): void;
20
+ draw(ctx: CanvasRenderingContext2D, shape?: 'circle' | 'square'): void;
21
+ }
@@ -0,0 +1,10 @@
1
+ import { ReactNode, CSSProperties } from 'react';
2
+ export interface ParticleCanvasProps {
3
+ children?: ReactNode;
4
+ width?: string | number;
5
+ height?: string | number;
6
+ backgroundColor?: string;
7
+ className?: string;
8
+ style?: CSSProperties;
9
+ }
10
+ export default function ParticleCanvas({ children, width, height, backgroundColor, className, style, }: ParticleCanvasProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,14 @@
1
+ import { ClickMode } from '../hooks/useParticleInteraction';
2
+ import { ParticleShape } from '../types';
3
+ export interface TextParticleEngineProps {
4
+ text: string;
5
+ particleColor?: string | string[];
6
+ particleSize?: number;
7
+ particleDensity?: number;
8
+ particleEase?: number;
9
+ isMagnet?: boolean;
10
+ clickMode?: ClickMode;
11
+ particleShape?: ParticleShape;
12
+ backgroundColor?: string;
13
+ }
14
+ export default function TextParticleEngine({ text, particleColor, particleSize, particleDensity, particleEase, isMagnet, clickMode, particleShape, backgroundColor }: TextParticleEngineProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { RefObject } from 'react';
2
+ export type ClickMode = 'none' | 'attract' | 'repel';
3
+ export declare function useParticleInteraction(containerRef: RefObject<HTMLDivElement | null>): RefObject<{
4
+ x: number;
5
+ y: number;
6
+ isDown: boolean;
7
+ active: boolean;
8
+ }>;
9
+ export declare function getMagnetTarget(x: number, y: number, targetX: number, targetY: number, mx: number | null, my: number | null, isMouseDown: boolean, isMagnet: boolean, clickMode: ClickMode): {
10
+ x: number;
11
+ y: number;
12
+ };
@@ -0,0 +1,12 @@
1
+ import { default as React } from 'react';
2
+ import { Particle } from '../components/Particle';
3
+ declare function getPixelsForText(text: string, width: number, height: number): {
4
+ x: number;
5
+ y: number;
6
+ }[];
7
+ export declare function useTextParticles(text: string, particlesRef: React.MutableRefObject<Particle[]>, containerRef: React.RefObject<HTMLElement | null>): {
8
+ getPixelsForText: typeof getPixelsForText;
9
+ updateTextTargets: (char: string, w?: number, h?: number) => void;
10
+ textRef: React.RefObject<string>;
11
+ };
12
+ export {};
@@ -0,0 +1,8 @@
1
+ export { default as ParticleCanvas } from './components/ParticleCanvas';
2
+ export type { ParticleCanvasProps } from './components/ParticleCanvas';
3
+ export { default as TextParticleEngine } from './components/TextParticleEngine';
4
+ export type { TextParticleEngineProps } from './components/TextParticleEngine';
5
+ export { useParticleInteraction, getMagnetTarget } from './hooks/useParticleInteraction';
6
+ export type { ClickMode } from './hooks/useParticleInteraction';
7
+ export { useTextParticles } from './hooks/useTextParticles';
8
+ export type { ParticleShape, ColorMode } from './types';
@@ -0,0 +1,277 @@
1
+ var H = Object.defineProperty;
2
+ var j = (n, t, e) => t in n ? H(n, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : n[t] = e;
3
+ var y = (n, t, e) => j(n, typeof t != "symbol" ? t + "" : t, e);
4
+ import { jsx as L } from "react/jsx-runtime";
5
+ import { useRef as E, useEffect as z } from "react";
6
+ const G = {
7
+ position: "relative",
8
+ border: "1px solid rgba(255, 255, 255, 0.1)",
9
+ borderRadius: "1rem",
10
+ overflow: "hidden",
11
+ boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
12
+ transition: "all 300ms ease-out"
13
+ };
14
+ function _({
15
+ children: n,
16
+ width: t = "100%",
17
+ height: e = "60vh",
18
+ backgroundColor: r = "#050505",
19
+ className: a = "",
20
+ style: l
21
+ }) {
22
+ return /* @__PURE__ */ L(
23
+ "div",
24
+ {
25
+ className: a,
26
+ style: { ...G, width: t, height: e, backgroundColor: r, ...l },
27
+ children: n
28
+ }
29
+ );
30
+ }
31
+ function K(n) {
32
+ const t = E({ x: -1e3, y: -1e3, isDown: !1, active: !1 });
33
+ return z(() => {
34
+ const e = n.current;
35
+ if (!e) return;
36
+ const r = (D) => {
37
+ const u = e.getBoundingClientRect();
38
+ t.current.x = D.clientX - u.left, t.current.y = D.clientY - u.top, t.current.active = !0;
39
+ }, a = () => {
40
+ t.current.active = !1;
41
+ }, l = () => {
42
+ t.current.isDown = !0;
43
+ }, x = () => {
44
+ t.current.isDown = !1;
45
+ };
46
+ return e.addEventListener("pointermove", r), e.addEventListener("pointerleave", a), e.addEventListener("pointerdown", l), e.addEventListener("pointerup", x), e.style.touchAction = "none", () => {
47
+ e.removeEventListener("pointermove", r), e.removeEventListener("pointerleave", a), e.removeEventListener("pointerdown", l), e.removeEventListener("pointerup", x);
48
+ };
49
+ }, [n]), t;
50
+ }
51
+ function U(n, t, e, r, a, l, x, D, u) {
52
+ if (a === null || l === null || !D && u === "none")
53
+ return { x: e, y: r };
54
+ let g = e, h = r;
55
+ const i = a - n, d = l - t, f = i * i + d * d, p = Math.sqrt(f);
56
+ if (D && !x && f < 3e4) {
57
+ const v = (3e4 - f) / 3e4;
58
+ g += i * v * 0.15, h += d * v * 0.15;
59
+ }
60
+ if (x && u !== "none") {
61
+ if (u === "attract") {
62
+ if (f < 3e4) {
63
+ const v = (3e4 - f) / 3e4;
64
+ g += i * v * 0.8, h += d * v * 0.8;
65
+ }
66
+ } else if (u === "repel" && f < 5e4 && p > 0) {
67
+ const v = Math.pow(Math.max(0, 5e4 - f) / 5e4, 1.2);
68
+ g -= i / p * v * 400, h -= d / p * v * 400;
69
+ }
70
+ }
71
+ return { x: g, y: h };
72
+ }
73
+ class O {
74
+ constructor(t, e, r = "255, 255, 255") {
75
+ y(this, "x");
76
+ y(this, "y");
77
+ y(this, "vx");
78
+ y(this, "vy");
79
+ y(this, "targetX");
80
+ y(this, "targetY");
81
+ y(this, "baseColor");
82
+ y(this, "opacity");
83
+ y(this, "size");
84
+ y(this, "sizeMultiplier");
85
+ y(this, "friction");
86
+ y(this, "ease");
87
+ y(this, "easeMultiplier");
88
+ y(this, "floatSpeed");
89
+ y(this, "floatOffset");
90
+ this.x = Math.random() * t, this.y = Math.random() * e, this.targetX = this.x, this.targetY = this.y, this.vx = 0, this.vy = 0, this.size = Math.random() * 1.8 + 0.5, this.sizeMultiplier = 1, this.baseColor = Array.isArray(r) ? r[Math.floor(Math.random() * r.length)] : r, this.opacity = 0.4 + Math.random() * 0.6, this.friction = 0.82 + Math.random() * 0.1, this.ease = 0.03 + Math.random() * 0.05, this.easeMultiplier = 1, this.floatSpeed = Math.random() * 0.02 + 5e-3, this.floatOffset = Math.random() * Math.PI * 2;
91
+ }
92
+ update(t, e, r = null, a = null, l = !1, x = !0, D = "none") {
93
+ const { x: u, y: g } = U(
94
+ this.x,
95
+ this.y,
96
+ this.targetX,
97
+ this.targetY,
98
+ r,
99
+ a,
100
+ l,
101
+ x,
102
+ D
103
+ ), h = u - this.x, i = g - this.y, d = e ? 0 : Math.cos(t * 0.01 + this.y * 0.01) * 0.5, f = e ? 0 : Math.sin(t * 0.01 + this.x * 0.01) * 0.5;
104
+ this.vx += h * (this.ease * this.easeMultiplier) + d, this.vy += i * (this.ease * this.easeMultiplier) + f, this.vx *= this.friction, this.vy *= this.friction, this.x += this.vx, this.y += this.vy;
105
+ const p = e ? 0.2 : 2;
106
+ this.x += Math.cos(t * this.floatSpeed + this.floatOffset) * p, this.y += Math.sin(t * this.floatSpeed + this.floatOffset) * p;
107
+ }
108
+ draw(t, e = "circle") {
109
+ t.fillStyle = `rgba(${this.baseColor}, ${this.opacity})`;
110
+ const r = Math.max(0.1, this.size * this.sizeMultiplier);
111
+ e === "square" ? t.fillRect(this.x - r, this.y - r, r * 2, r * 2) : (t.beginPath(), t.arc(this.x, this.y, r, 0, Math.PI * 2), t.fill());
112
+ }
113
+ }
114
+ function A(n, t, e) {
115
+ if (t <= 0 || e <= 0) return [];
116
+ const r = document.createElement("canvas");
117
+ r.width = t, r.height = e;
118
+ const a = r.getContext("2d", { willReadFrequently: !0 });
119
+ if (!a) return [];
120
+ a.clearRect(0, 0, t, e);
121
+ let l = Math.min(t, e) * 0.65;
122
+ a.font = `bold ${l}px "Georgia", serif`;
123
+ const x = a.measureText(n);
124
+ x.width > t * 0.9 && (l = l * (t * 0.9) / x.width, a.font = `bold ${l}px "Georgia", serif`), a.fillStyle = "white", a.textAlign = "center", a.textBaseline = "middle", a.fillText(n, t / 2, e / 2.05);
125
+ const u = a.getImageData(0, 0, t, e).data, g = [], h = window.innerWidth < 600 ? 6 : 8;
126
+ for (let i = 0; i < e; i += h)
127
+ for (let d = 0; d < t; d += h) {
128
+ const f = (i * t + d) * 4;
129
+ u[f + 3] > 128 && g.push({
130
+ x: d + (Math.random() - 0.5) * 4,
131
+ y: i + (Math.random() - 0.5) * 4
132
+ });
133
+ }
134
+ return g;
135
+ }
136
+ function J(n, t, e) {
137
+ const r = E(n);
138
+ return z(() => {
139
+ r.current = n;
140
+ }, [n]), { getPixelsForText: A, updateTextTargets: (l, x, D) => {
141
+ var S, X;
142
+ const u = x || ((S = e.current) == null ? void 0 : S.offsetWidth) || window.innerWidth, g = D || ((X = e.current) == null ? void 0 : X.offsetHeight) || window.innerHeight;
143
+ if (!l) {
144
+ t.current.forEach((o) => {
145
+ const M = 50 + Math.random() * (u - 100), b = 50 + Math.random() * (g - 100);
146
+ (Math.abs(M - o.x) > 20 || Math.abs(b - o.y) > 20) && (o.vx += (Math.random() - 0.5) * 20, o.vy += (Math.random() - 0.5) * 20), o.targetX = M, o.targetY = b;
147
+ });
148
+ return;
149
+ }
150
+ const h = A(l, u, g);
151
+ if (h.length === 0) return;
152
+ const i = u * 0.15, d = h.map((s) => ({ pt: s, key: s.x + (Math.random() - 0.5) * i }));
153
+ d.sort((s, o) => s.key - o.key);
154
+ const f = d.map((s) => s.pt), p = t.current.map((s, o) => ({ i: o, key: s.x + (Math.random() - 0.5) * i }));
155
+ p.sort((s, o) => s.key - o.key);
156
+ const P = p.map((s) => s.i), v = Math.ceil(Math.sqrt(P.length));
157
+ for (let s = 0; s < P.length; s += v) {
158
+ const o = Math.min(s + v, P.length), M = P.slice(s, o), b = [];
159
+ for (let c = s; c < o; c++)
160
+ b.push(f[c % f.length]);
161
+ M.sort((c, w) => t.current[c].y - t.current[w].y), b.sort((c, w) => c.y - w.y);
162
+ for (let c = 0; c < M.length; c++) {
163
+ const w = M[c], m = t.current[w], R = b[c], T = R.x - m.x, C = R.y - m.y;
164
+ if (Math.abs(T) > 20 || Math.abs(C) > 20) {
165
+ m.vx += (Math.random() - 0.5) * 20, m.vy += (Math.random() - 0.5) * 20;
166
+ const I = (Math.random() > 0.5 ? 1 : -1) * (Math.random() * 10 + 5);
167
+ m.vx += Math.sign(C) * I, m.vy -= Math.sign(T) * I;
168
+ }
169
+ m.targetX = R.x, m.targetY = R.y;
170
+ }
171
+ }
172
+ }, textRef: r };
173
+ }
174
+ const N = (n) => {
175
+ if (!n || typeof n != "string") return "0, 0, 0";
176
+ try {
177
+ let t = 0, e = 0, r = 0;
178
+ return n.length === 4 ? (t = parseInt(n[1] + n[1], 16), e = parseInt(n[2] + n[2], 16), r = parseInt(n[3] + n[3], 16)) : n.length === 7 && (t = parseInt(n.substring(1, 3), 16), e = parseInt(n.substring(3, 5), 16), r = parseInt(n.substring(5, 7), 16)), `${t}, ${e}, ${r}`;
179
+ } catch {
180
+ return "0, 0, 0";
181
+ }
182
+ };
183
+ function tt({
184
+ text: n,
185
+ particleColor: t = "255, 255, 255",
186
+ particleSize: e = 1,
187
+ particleDensity: r = 1,
188
+ particleEase: a = 1,
189
+ isMagnet: l = !0,
190
+ clickMode: x = "none",
191
+ particleShape: D = "circle",
192
+ backgroundColor: u = "#050505"
193
+ }) {
194
+ const g = E(null), h = E(null), i = E([]), d = E(0), f = E(0), p = K(h), P = E({ isMagnet: l, clickMode: x, particleShape: D, backgroundColor: u });
195
+ P.current = { isMagnet: l, clickMode: x, particleShape: D, backgroundColor: u };
196
+ const { updateTextTargets: v, textRef: S } = J(n, i, h), X = (s, o, M = 1) => {
197
+ const b = window.innerWidth < 600 ? 1500 : 3e3, c = Math.floor(b * M), w = [];
198
+ for (let m = 0; m < c; m++) {
199
+ const R = new O(s, o, t);
200
+ R.sizeMultiplier = e, R.easeMultiplier = a, w.push(R);
201
+ }
202
+ i.current = w;
203
+ };
204
+ return z(() => {
205
+ i.current.length > 0 && i.current.forEach((s) => {
206
+ s.baseColor = Array.isArray(t) ? t[Math.floor(Math.random() * t.length)] : t;
207
+ });
208
+ }, [t]), z(() => {
209
+ i.current.length > 0 && i.current.forEach((s) => {
210
+ s.sizeMultiplier = e;
211
+ });
212
+ }, [e]), z(() => {
213
+ i.current.length > 0 && i.current.forEach((s) => {
214
+ s.easeMultiplier = a;
215
+ });
216
+ }, [a]), z(() => {
217
+ if (i.current.length > 0 && g.current && h.current) {
218
+ const s = window.innerWidth < 600 ? 1500 : 3e3, o = Math.floor(s * r), M = i.current.length;
219
+ if (o > M) {
220
+ const b = h.current.getBoundingClientRect();
221
+ for (let c = 0; c < o - M; c++) {
222
+ const w = new O(b.width, b.height, t);
223
+ w.sizeMultiplier = e, i.current.push(w);
224
+ }
225
+ v(n);
226
+ } else o < M && i.current.splice(o);
227
+ }
228
+ }, [r]), z(() => {
229
+ const s = h.current, o = g.current;
230
+ if (!s || !o) return;
231
+ const M = (m) => {
232
+ for (const R of m) {
233
+ const { width: T, height: C } = R.contentRect, I = window.devicePixelRatio || 1;
234
+ o.width = T * I, o.height = C * I, o.style.width = `${T}px`, o.style.height = `${C}px`;
235
+ const Y = o.getContext("2d");
236
+ Y && Y.scale(I, I), i.current.length === 0 && X(T, C, r), v(n, T, C);
237
+ }
238
+ }, b = new ResizeObserver(M);
239
+ b.observe(s);
240
+ const c = o.getContext("2d"), w = () => {
241
+ f.current++;
242
+ const m = s.getBoundingClientRect(), { isMagnet: R, clickMode: T, particleShape: C, backgroundColor: I } = P.current, Y = N(I);
243
+ c.fillStyle = `rgba(${Y}, 0.25)`, c.fillRect(0, 0, m.width, m.height), c.globalCompositeOperation = "screen";
244
+ const W = S.current !== "", q = p.current.active ? p.current.x : null, B = p.current.active ? p.current.y : null, F = p.current.isDown;
245
+ for (let $ = 0; $ < i.current.length; $++) {
246
+ const k = i.current[$];
247
+ k.update(f.current, !!W, q, B, F, R, T), k.draw(c, C);
248
+ }
249
+ c.globalCompositeOperation = "source-over", d.current = requestAnimationFrame(w);
250
+ };
251
+ return w(), () => {
252
+ b.disconnect(), cancelAnimationFrame(d.current);
253
+ };
254
+ }, []), z(() => {
255
+ v(n);
256
+ }, [n]), /* @__PURE__ */ L(
257
+ "div",
258
+ {
259
+ ref: h,
260
+ style: { position: "absolute", inset: 0, zIndex: 0, overflow: "hidden" },
261
+ children: /* @__PURE__ */ L(
262
+ "canvas",
263
+ {
264
+ ref: g,
265
+ style: { display: "block", width: "100%", height: "100%" }
266
+ }
267
+ )
268
+ }
269
+ );
270
+ }
271
+ export {
272
+ _ as ParticleCanvas,
273
+ tt as TextParticleEngine,
274
+ U as getMagnetTarget,
275
+ K as useParticleInteraction,
276
+ J as useTextParticles
277
+ };
@@ -0,0 +1 @@
1
+ (function(f,M){typeof exports=="object"&&typeof module<"u"?M(exports,require("react/jsx-runtime"),require("react")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react"],M):(f=typeof globalThis<"u"?globalThis:f||self,M(f.JlParticleInteractive={},f.ReactJsxRuntime,f.React))})(this,(function(f,M,u){"use strict";var Q=Object.defineProperty;var V=(f,M,u)=>M in f?Q(f,M,{enumerable:!0,configurable:!0,writable:!0,value:u}):f[M]=u;var v=(f,M,u)=>V(f,typeof M!="symbol"?M+"":M,u);const B={position:"relative",border:"1px solid rgba(255, 255, 255, 0.1)",borderRadius:"1rem",overflow:"hidden",boxShadow:"0 25px 50px -12px rgba(0, 0, 0, 0.25)",transition:"all 300ms ease-out"};function F({children:r,width:t="100%",height:e="60vh",backgroundColor:n="#050505",className:a="",style:l}){return M.jsx("div",{className:a,style:{...B,width:t,height:e,backgroundColor:n,...l},children:r})}function O(r){const t=u.useRef({x:-1e3,y:-1e3,isDown:!1,active:!1});return u.useEffect(()=>{const e=r.current;if(!e)return;const n=E=>{const d=e.getBoundingClientRect();t.current.x=E.clientX-d.left,t.current.y=E.clientY-d.top,t.current.active=!0},a=()=>{t.current.active=!1},l=()=>{t.current.isDown=!0},b=()=>{t.current.isDown=!1};return e.addEventListener("pointermove",n),e.addEventListener("pointerleave",a),e.addEventListener("pointerdown",l),e.addEventListener("pointerup",b),e.style.touchAction="none",()=>{e.removeEventListener("pointermove",n),e.removeEventListener("pointerleave",a),e.removeEventListener("pointerdown",l),e.removeEventListener("pointerup",b)}},[r]),t}function k(r,t,e,n,a,l,b,E,d){if(a===null||l===null||!E&&d==="none")return{x:e,y:n};let m=e,h=n;const i=a-r,g=l-t,p=i*i+g*g,y=Math.sqrt(p);if(E&&!b&&p<3e4){const w=(3e4-p)/3e4;m+=i*w*.15,h+=g*w*.15}if(b&&d!=="none"){if(d==="attract"){if(p<3e4){const w=(3e4-p)/3e4;m+=i*w*.8,h+=g*w*.8}}else if(d==="repel"&&p<5e4&&y>0){const w=Math.pow(Math.max(0,5e4-p)/5e4,1.2);m-=i/y*w*400,h-=g/y*w*400}}return{x:m,y:h}}class q{constructor(t,e,n="255, 255, 255"){v(this,"x");v(this,"y");v(this,"vx");v(this,"vy");v(this,"targetX");v(this,"targetY");v(this,"baseColor");v(this,"opacity");v(this,"size");v(this,"sizeMultiplier");v(this,"friction");v(this,"ease");v(this,"easeMultiplier");v(this,"floatSpeed");v(this,"floatOffset");this.x=Math.random()*t,this.y=Math.random()*e,this.targetX=this.x,this.targetY=this.y,this.vx=0,this.vy=0,this.size=Math.random()*1.8+.5,this.sizeMultiplier=1,this.baseColor=Array.isArray(n)?n[Math.floor(Math.random()*n.length)]:n,this.opacity=.4+Math.random()*.6,this.friction=.82+Math.random()*.1,this.ease=.03+Math.random()*.05,this.easeMultiplier=1,this.floatSpeed=Math.random()*.02+.005,this.floatOffset=Math.random()*Math.PI*2}update(t,e,n=null,a=null,l=!1,b=!0,E="none"){const{x:d,y:m}=k(this.x,this.y,this.targetX,this.targetY,n,a,l,b,E),h=d-this.x,i=m-this.y,g=e?0:Math.cos(t*.01+this.y*.01)*.5,p=e?0:Math.sin(t*.01+this.x*.01)*.5;this.vx+=h*(this.ease*this.easeMultiplier)+g,this.vy+=i*(this.ease*this.easeMultiplier)+p,this.vx*=this.friction,this.vy*=this.friction,this.x+=this.vx,this.y+=this.vy;const y=e?.2:2;this.x+=Math.cos(t*this.floatSpeed+this.floatOffset)*y,this.y+=Math.sin(t*this.floatSpeed+this.floatOffset)*y}draw(t,e="circle"){t.fillStyle=`rgba(${this.baseColor}, ${this.opacity})`;const n=Math.max(.1,this.size*this.sizeMultiplier);e==="square"?t.fillRect(this.x-n,this.y-n,n*2,n*2):(t.beginPath(),t.arc(this.x,this.y,n,0,Math.PI*2),t.fill())}}function j(r,t,e){if(t<=0||e<=0)return[];const n=document.createElement("canvas");n.width=t,n.height=e;const a=n.getContext("2d",{willReadFrequently:!0});if(!a)return[];a.clearRect(0,0,t,e);let l=Math.min(t,e)*.65;a.font=`bold ${l}px "Georgia", serif`;const b=a.measureText(r);b.width>t*.9&&(l=l*(t*.9)/b.width,a.font=`bold ${l}px "Georgia", serif`),a.fillStyle="white",a.textAlign="center",a.textBaseline="middle",a.fillText(r,t/2,e/2.05);const d=a.getImageData(0,0,t,e).data,m=[],h=window.innerWidth<600?6:8;for(let i=0;i<e;i+=h)for(let g=0;g<t;g+=h){const p=(i*t+g)*4;d[p+3]>128&&m.push({x:g+(Math.random()-.5)*4,y:i+(Math.random()-.5)*4})}return m}function A(r,t,e){const n=u.useRef(r);return u.useEffect(()=>{n.current=r},[r]),{getPixelsForText:j,updateTextTargets:(l,b,E)=>{var X,Y;const d=b||((X=e.current)==null?void 0:X.offsetWidth)||window.innerWidth,m=E||((Y=e.current)==null?void 0:Y.offsetHeight)||window.innerHeight;if(!l){t.current.forEach(o=>{const R=50+Math.random()*(d-100),P=50+Math.random()*(m-100);(Math.abs(R-o.x)>20||Math.abs(P-o.y)>20)&&(o.vx+=(Math.random()-.5)*20,o.vy+=(Math.random()-.5)*20),o.targetX=R,o.targetY=P});return}const h=j(l,d,m);if(h.length===0)return;const i=d*.15,g=h.map(s=>({pt:s,key:s.x+(Math.random()-.5)*i}));g.sort((s,o)=>s.key-o.key);const p=g.map(s=>s.pt),y=t.current.map((s,o)=>({i:o,key:s.x+(Math.random()-.5)*i}));y.sort((s,o)=>s.key-o.key);const I=y.map(s=>s.i),w=Math.ceil(Math.sqrt(I.length));for(let s=0;s<I.length;s+=w){const o=Math.min(s+w,I.length),R=I.slice(s,o),P=[];for(let c=s;c<o;c++)P.push(p[c%p.length]);R.sort((c,T)=>t.current[c].y-t.current[T].y),P.sort((c,T)=>c.y-T.y);for(let c=0;c<R.length;c++){const T=R[c],x=t.current[T],D=P[c],C=D.x-x.x,S=D.y-x.y;if(Math.abs(C)>20||Math.abs(S)>20){x.vx+=(Math.random()-.5)*20,x.vy+=(Math.random()-.5)*20;const z=(Math.random()>.5?1:-1)*(Math.random()*10+5);x.vx+=Math.sign(S)*z,x.vy-=Math.sign(C)*z}x.targetX=D.x,x.targetY=D.y}}},textRef:n}}const H=r=>{if(!r||typeof r!="string")return"0, 0, 0";try{let t=0,e=0,n=0;return r.length===4?(t=parseInt(r[1]+r[1],16),e=parseInt(r[2]+r[2],16),n=parseInt(r[3]+r[3],16)):r.length===7&&(t=parseInt(r.substring(1,3),16),e=parseInt(r.substring(3,5),16),n=parseInt(r.substring(5,7),16)),`${t}, ${e}, ${n}`}catch{return"0, 0, 0"}};function G({text:r,particleColor:t="255, 255, 255",particleSize:e=1,particleDensity:n=1,particleEase:a=1,isMagnet:l=!0,clickMode:b="none",particleShape:E="circle",backgroundColor:d="#050505"}){const m=u.useRef(null),h=u.useRef(null),i=u.useRef([]),g=u.useRef(0),p=u.useRef(0),y=O(h),I=u.useRef({isMagnet:l,clickMode:b,particleShape:E,backgroundColor:d});I.current={isMagnet:l,clickMode:b,particleShape:E,backgroundColor:d};const{updateTextTargets:w,textRef:X}=A(r,i,h),Y=(s,o,R=1)=>{const P=window.innerWidth<600?1500:3e3,c=Math.floor(P*R),T=[];for(let x=0;x<c;x++){const D=new q(s,o,t);D.sizeMultiplier=e,D.easeMultiplier=a,T.push(D)}i.current=T};return u.useEffect(()=>{i.current.length>0&&i.current.forEach(s=>{s.baseColor=Array.isArray(t)?t[Math.floor(Math.random()*t.length)]:t})},[t]),u.useEffect(()=>{i.current.length>0&&i.current.forEach(s=>{s.sizeMultiplier=e})},[e]),u.useEffect(()=>{i.current.length>0&&i.current.forEach(s=>{s.easeMultiplier=a})},[a]),u.useEffect(()=>{if(i.current.length>0&&m.current&&h.current){const s=window.innerWidth<600?1500:3e3,o=Math.floor(s*n),R=i.current.length;if(o>R){const P=h.current.getBoundingClientRect();for(let c=0;c<o-R;c++){const T=new q(P.width,P.height,t);T.sizeMultiplier=e,i.current.push(T)}w(r)}else o<R&&i.current.splice(o)}},[n]),u.useEffect(()=>{const s=h.current,o=m.current;if(!s||!o)return;const R=x=>{for(const D of x){const{width:C,height:S}=D.contentRect,z=window.devicePixelRatio||1;o.width=C*z,o.height=S*z,o.style.width=`${C}px`,o.style.height=`${S}px`;const $=o.getContext("2d");$&&$.scale(z,z),i.current.length===0&&Y(C,S,n),w(r,C,S)}},P=new ResizeObserver(R);P.observe(s);const c=o.getContext("2d"),T=()=>{p.current++;const x=s.getBoundingClientRect(),{isMagnet:D,clickMode:C,particleShape:S,backgroundColor:z}=I.current,$=H(z);c.fillStyle=`rgba(${$}, 0.25)`,c.fillRect(0,0,x.width,x.height),c.globalCompositeOperation="screen";const J=X.current!=="",K=y.current.active?y.current.x:null,U=y.current.active?y.current.y:null,N=y.current.isDown;for(let L=0;L<i.current.length;L++){const W=i.current[L];W.update(p.current,!!J,K,U,N,D,C),W.draw(c,S)}c.globalCompositeOperation="source-over",g.current=requestAnimationFrame(T)};return T(),()=>{P.disconnect(),cancelAnimationFrame(g.current)}},[]),u.useEffect(()=>{w(r)},[r]),M.jsx("div",{ref:h,style:{position:"absolute",inset:0,zIndex:0,overflow:"hidden"},children:M.jsx("canvas",{ref:m,style:{display:"block",width:"100%",height:"100%"}})})}f.ParticleCanvas=F,f.TextParticleEngine=G,f.getMagnetTarget=k,f.useParticleInteraction=O,f.useTextParticles=A,Object.defineProperty(f,Symbol.toStringTag,{value:"Module"})}));
@@ -0,0 +1,2 @@
1
+ export type ColorMode = 'single' | 'palette';
2
+ export type ParticleShape = 'circle' | 'square';
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "jl-particle-interactive",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Interactive particle text animation components for React",
6
+ "author": "JL",
7
+ "license": "MIT",
8
+ "type": "module",
9
+ "main": "./dist/jl-particle-interactive.umd.cjs",
10
+ "module": "./dist/jl-particle-interactive.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/jl-particle-interactive.js",
16
+ "require": "./dist/jl-particle-interactive.umd.cjs"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "scripts": {
23
+ "build": "vite build",
24
+ "lint": "tsc --noEmit"
25
+ },
26
+ "keywords": [
27
+ "react",
28
+ "particles",
29
+ "animation",
30
+ "canvas",
31
+ "interactive",
32
+ "text"
33
+ ],
34
+ "peerDependencies": {
35
+ "react": ">=18.0.0",
36
+ "react-dom": ">=18.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/react": "^19.0.0",
40
+ "@types/react-dom": "^19.0.0",
41
+ "@vitejs/plugin-react": "^5.0.4",
42
+ "typescript": "~5.8.2",
43
+ "vite": "^6.2.3",
44
+ "vite-plugin-dts": "^4.5.0"
45
+ }
46
+ }