返回 presentation-ai
sparkles.tsx
根目录 / src / components / ui / sparkles.tsx
1 "use client";
2 import { cn } from "@/lib/utils";
3 import {
4 type IOptions,
5 type RecursivePartial,
6 type SingleOrMultiple,
7 } from "@tsparticles/engine";
8 import Particles, { initParticlesEngine } from "@tsparticles/react";
9 import { loadSlim } from "@tsparticles/slim";
10 import { memo, useEffect, useId, useMemo, useRef, useState } from "react";
11
12 type ParticlesProps = {
13 id?: string;
14 className?: string;
15 background?: string;
16 particleSize?: number;
17 minSize?: number;
18 maxSize?: number;
19 speed?: number;
20 particleColor?: string;
21 particleDensity?: number;
22 };
23
24 let particlesEnginePromise: Promise<void> | null = null;
25
26 function ensureParticlesEngine(): Promise<void> {
27 if (!particlesEnginePromise) {
28 particlesEnginePromise = initParticlesEngine(async (engine) => {
29 await loadSlim(engine);
30 });
31 }
32
33 return particlesEnginePromise;
34 }
35
36 const SparklesCoreBase = (props: ParticlesProps) => {
37 const {
38 id,
39 className,
40 background,
41 minSize,
42 maxSize,
43 speed,
44 particleColor,
45 particleDensity,
46 } = props;
47 const [init, setInit] = useState(false);
48 const isMountedRef = useRef(false);
49
50 useEffect(() => {
51 isMountedRef.current = true;
52 return () => {
53 isMountedRef.current = false;
54 };
55 }, []);
56
57 useEffect(() => {
58 void ensureParticlesEngine().then(() => {
59 if (!isMountedRef.current) return;
60 setInit(true);
61 });
62 }, []);
63
64 const generatedId = useId();
65 const options = useMemo<RecursivePartial<IOptions>>(
66 () => ({
67 background: {
68 color: {
69 value: background || "#0d47a1",
70 },
71 },
72 fullScreen: {
73 enable: false,
74 zIndex: 1,
75 },
76
77 fpsLimit: 120,
78 interactivity: {
79 events: {
80 onClick: {
81 enable: true,
82 mode: "push",
83 },
84 onHover: {
85 enable: false,
86 mode: "repulse",
87 },
88 resize: {
89 enable: true,
90 },
91 },
92 modes: {
93 push: {
94 quantity: 4,
95 },
96 repulse: {
97 distance: 200,
98 duration: 0.4,
99 },
100 },
101 },
102 particles: {
103 bounce: {
104 horizontal: {
105 value: 1,
106 },
107 vertical: {
108 value: 1,
109 },
110 },
111 collisions: {
112 absorb: {
113 speed: 2,
114 },
115 bounce: {
116 horizontal: {
117 value: 1,
118 },
119 vertical: {
120 value: 1,
121 },
122 },
123 enable: false,
124 maxSpeed: 50,
125 mode: "bounce",
126 overlap: {
127 enable: true,
128 retries: 0,
129 },
130 },
131 color: {
132 value: particleColor || "#ffffff",
133 animation: {
134 h: {
135 count: 0,
136 enable: false,
137 speed: 1,
138 decay: 0,
139 delay: 0,
140 sync: true,
141 offset: 0,
142 },
143 s: {
144 count: 0,
145 enable: false,
146 speed: 1,
147 decay: 0,
148 delay: 0,
149 sync: true,
150 offset: 0,
151 },
152 l: {
153 count: 0,
154 enable: false,
155 speed: 1,
156 decay: 0,
157 delay: 0,
158 sync: true,
159 offset: 0,
160 },
161 },
162 },
163 effect: {
164 close: true,
165 fill: true,
166 options: {},
167 type: {} as SingleOrMultiple<string> | undefined,
168 },
169 groups: {},
170 move: {
171 angle: {
172 offset: 0,
173 value: 90,
174 },
175 attract: {
176 distance: 200,
177 enable: false,
178 rotate: {
179 x: 3000,
180 y: 3000,
181 },
182 },
183 center: {
184 x: 50,
185 y: 50,
186 mode: "percent",
187 radius: 0,
188 },
189 decay: 0,
190 distance: {},
191 direction: "none",
192 drift: 0,
193 enable: true,
194 gravity: {
195 acceleration: 9.81,
196 enable: false,
197 inverse: false,
198 maxSpeed: 50,
199 },
200 path: {
201 clamp: true,
202 delay: {
203 value: 0,
204 },
205 enable: false,
206 options: {},
207 },
208 outModes: {
209 default: "out",
210 },
211 random: false,
212 size: false,
213 speed: {
214 min: 0.1,
215 max: 1,
216 },
217 spin: {
218 acceleration: 0,
219 enable: false,
220 },
221 straight: false,
222 trail: {
223 enable: false,
224 length: 10,
225 fill: {},
226 },
227 vibrate: false,
228 warp: false,
229 },
230 number: {
231 density: {
232 enable: true,
233 width: 400,
234 height: 400,
235 },
236 limit: {
237 mode: "delete",
238 value: 0,
239 },
240 value: particleDensity || 120,
241 },
242 opacity: {
243 value: {
244 min: 0.1,
245 max: 1,
246 },
247 animation: {
248 count: 0,
249 enable: true,
250 speed: speed || 4,
251 decay: 0,
252 delay: 0,
253 sync: false,
254 mode: "auto",
255 startValue: "random",
256 destroy: "none",
257 },
258 },
259 reduceDuplicates: false,
260 shadow: {
261 blur: 0,
262 color: {
263 value: "#000",
264 },
265 enable: false,
266 offset: {
267 x: 0,
268 y: 0,
269 },
270 },
271 shape: {
272 close: true,
273 fill: true,
274 options: {},
275 type: "circle",
276 },
277 size: {
278 value: {
279 min: minSize || 1,
280 max: maxSize || 3,
281 },
282 animation: {
283 count: 0,
284 enable: false,
285 speed: 5,
286 decay: 0,
287 delay: 0,
288 sync: false,
289 mode: "auto",
290 startValue: "random",
291 destroy: "none",
292 },
293 },
294 stroke: {
295 width: 0,
296 },
297 zIndex: {
298 value: 0,
299 opacityRate: 1,
300 sizeRate: 1,
301 velocityRate: 1,
302 },
303 destroy: {
304 bounds: {},
305 mode: "none",
306 split: {
307 count: 1,
308 factor: {
309 value: 3,
310 },
311 rate: {
312 value: {
313 min: 4,
314 max: 9,
315 },
316 },
317 sizeOffset: true,
318 },
319 },
320 roll: {
321 darken: {
322 enable: false,
323 value: 0,
324 },
325 enable: false,
326 enlighten: {
327 enable: false,
328 value: 0,
329 },
330 mode: "vertical",
331 speed: 25,
332 },
333 tilt: {
334 value: 0,
335 animation: {
336 enable: false,
337 speed: 0,
338 decay: 0,
339 sync: false,
340 },
341 direction: "clockwise",
342 enable: false,
343 },
344 twinkle: {
345 lines: {
346 enable: false,
347 frequency: 0.05,
348 opacity: 1,
349 },
350 particles: {
351 enable: false,
352 frequency: 0.05,
353 opacity: 1,
354 },
355 },
356 wobble: {
357 distance: 5,
358 enable: false,
359 speed: {
360 angle: 50,
361 move: 10,
362 },
363 },
364 life: {
365 count: 0,
366 delay: {
367 value: 0,
368 sync: false,
369 },
370 duration: {
371 value: 0,
372 sync: false,
373 },
374 },
375 rotate: {
376 value: 0,
377 animation: {
378 enable: false,
379 speed: 0,
380 decay: 0,
381 sync: false,
382 },
383 direction: "clockwise",
384 path: false,
385 },
386 orbit: {
387 animation: {
388 count: 0,
389 enable: false,
390 speed: 1,
391 decay: 0,
392 delay: 0,
393 sync: false,
394 },
395 enable: false,
396 opacity: 1,
397 rotation: {
398 value: 45,
399 },
400 width: 1,
401 },
402 links: {
403 blink: false,
404 color: {
405 value: "#fff",
406 },
407 consent: false,
408 distance: 100,
409 enable: false,
410 frequency: 1,
411 opacity: 1,
412 shadow: {
413 blur: 5,
414 color: {
415 value: "#000",
416 },
417 enable: false,
418 },
419 triangles: {
420 enable: false,
421 frequency: 1,
422 },
423 width: 1,
424 warp: false,
425 },
426 repulse: {
427 value: 0,
428 enabled: false,
429 distance: 1,
430 duration: 1,
431 factor: 1,
432 speed: 1,
433 },
434 },
435 detectRetina: true,
436 }),
437 [background, maxSize, minSize, particleColor, particleDensity, speed],
438 );
439 return (
440 <div className={cn(className)}>
441 {init && (
442 <Particles
443 id={id || generatedId}
444 className={cn("h-full w-full")}
445 options={options}
446 />
447 )}
448 </div>
449 );
450 };
451
452 export const SparklesCore = memo(function SparklesCoreMemo(
453 props: ParticlesProps,
454 ) {
455 return <SparklesCoreBase {...props} />;
456 });
457
457 lines Plain Text