react-particle-physics 1.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,1035 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ LineSystem: () => LineSystem,
34
+ ParticleSystem: () => ParticleSystem,
35
+ detectPerformanceTier: () => detectPerformanceTier,
36
+ getPresetSettings: () => getPresetSettings,
37
+ sampleLogoToParticles: () => sampleLogoToParticles
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/ParticleSystem.tsx
42
+ var import_react = require("react");
43
+ var THREE = __toESM(require("three"));
44
+ var import_fiber = require("@react-three/fiber");
45
+
46
+ // src/sampleLogoToParticles.ts
47
+ var createSeed = (x, y) => {
48
+ const n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
49
+ return n - Math.floor(n);
50
+ };
51
+ async function sampleLogoToParticles(imageUrl, targetCount, options = {}) {
52
+ const {
53
+ maxTextureSize = 1024,
54
+ mode = "alpha",
55
+ alphaThreshold = 0.04,
56
+ lumaThreshold = 0.64,
57
+ edgeThreshold = 0.08
58
+ } = options;
59
+ const image = await loadImage(imageUrl);
60
+ const scale = Math.min(1, maxTextureSize / Math.max(image.width, image.height));
61
+ const width = Math.max(1, Math.round(image.width * scale));
62
+ const height = Math.max(1, Math.round(image.height * scale));
63
+ const canvas = document.createElement("canvas");
64
+ canvas.width = width;
65
+ canvas.height = height;
66
+ const context = canvas.getContext("2d", { willReadFrequently: true });
67
+ if (!context) {
68
+ throw new Error("Unable to create a 2D canvas context.");
69
+ }
70
+ context.clearRect(0, 0, width, height);
71
+ context.drawImage(image, 0, 0, width, height);
72
+ const imageData = context.getImageData(0, 0, width, height).data;
73
+ const luminance = new Float32Array(width * height);
74
+ for (let i = 0; i < width * height; i += 1) {
75
+ const offset = i * 4;
76
+ const r = imageData[offset] / 255;
77
+ const g = imageData[offset + 1] / 255;
78
+ const b = imageData[offset + 2] / 255;
79
+ luminance[i] = r * 0.2126 + g * 0.7152 + b * 0.0722;
80
+ }
81
+ const reservoir = [];
82
+ let visibleCount = 0;
83
+ const pixelScore = (x, y) => {
84
+ const idx = y * width + x;
85
+ const offset = idx * 4;
86
+ const alpha = imageData[offset + 3] / 255;
87
+ const luma = luminance[idx];
88
+ if (mode === "alpha") {
89
+ return alpha > alphaThreshold ? alpha : 0;
90
+ }
91
+ if (mode === "luma") {
92
+ return luma > lumaThreshold ? luma : 0;
93
+ }
94
+ if (mode === "hybrid") {
95
+ const alphaMask = alpha > alphaThreshold ? alpha : 0;
96
+ const lumaMask = luma > lumaThreshold ? luma : 0;
97
+ return Math.max(alphaMask, lumaMask);
98
+ }
99
+ const left = luminance[y * width + Math.max(0, x - 1)];
100
+ const right = luminance[y * width + Math.min(width - 1, x + 1)];
101
+ const up = luminance[Math.max(0, y - 1) * width + x];
102
+ const down = luminance[Math.min(height - 1, y + 1) * width + x];
103
+ const gradient = Math.abs(right - left) + Math.abs(down - up);
104
+ return gradient > edgeThreshold ? gradient : 0;
105
+ };
106
+ for (let y = 0; y < height; y += 1) {
107
+ for (let x = 0; x < width; x += 1) {
108
+ const index = (y * width + x) * 4;
109
+ const alpha = imageData[index + 3] / 255;
110
+ const score = pixelScore(x, y);
111
+ if (score <= 0) {
112
+ continue;
113
+ }
114
+ visibleCount += 1;
115
+ const brightness = (imageData[index] + imageData[index + 1] + imageData[index + 2]) / (255 * 3);
116
+ const point = {
117
+ x,
118
+ y,
119
+ uvX: (x + 0.5) / width,
120
+ uvY: 1 - (y + 0.5) / height,
121
+ r: imageData[index] / 255,
122
+ g: imageData[index + 1] / 255,
123
+ b: imageData[index + 2] / 255,
124
+ alpha: Math.max(0.2, Math.min(1, score * 1.4 + alpha * 0.4 + brightness * 0.2)),
125
+ seed: createSeed(x, y)
126
+ };
127
+ if (reservoir.length < targetCount) {
128
+ reservoir.push(point);
129
+ } else {
130
+ const randomIndex = Math.floor(Math.random() * visibleCount);
131
+ if (randomIndex < targetCount) {
132
+ reservoir[randomIndex] = point;
133
+ }
134
+ }
135
+ }
136
+ }
137
+ if (reservoir.length === 0) {
138
+ throw new Error("The provided image did not contain any visible pixels to sample.");
139
+ }
140
+ const count = reservoir.length;
141
+ const position = new Float32Array(count * 3);
142
+ const uv = new Float32Array(count * 2);
143
+ const color = new Float32Array(count * 3);
144
+ const seed = new Float32Array(count);
145
+ const opacity = new Float32Array(count);
146
+ const fitScale = 0.96;
147
+ const imageAspect = width / height;
148
+ for (let i = 0; i < count; i += 1) {
149
+ const point = reservoir[i];
150
+ const normalizedX = ((point.x + 0.5) / width - 0.5) * 2;
151
+ const normalizedY = ((point.y + 0.5) / height - 0.5) * -2;
152
+ position[i * 3] = normalizedX * fitScale * imageAspect;
153
+ position[i * 3 + 1] = normalizedY * fitScale;
154
+ position[i * 3 + 2] = (point.seed - 0.5) * 0.02;
155
+ uv[i * 2] = point.uvX;
156
+ uv[i * 2 + 1] = point.uvY;
157
+ color[i * 3] = point.r;
158
+ color[i * 3 + 1] = point.g;
159
+ color[i * 3 + 2] = point.b;
160
+ seed[i] = point.seed;
161
+ opacity[i] = point.alpha;
162
+ }
163
+ return {
164
+ position,
165
+ uv,
166
+ color,
167
+ seed,
168
+ opacity,
169
+ count,
170
+ width,
171
+ height
172
+ };
173
+ }
174
+ async function loadImage(imageUrl) {
175
+ return await new Promise((resolve, reject) => {
176
+ const image = new Image();
177
+ image.crossOrigin = "anonymous";
178
+ image.onload = () => resolve(image);
179
+ image.onerror = () => reject(new Error(`Unable to load image at ${imageUrl}`));
180
+ image.src = imageUrl;
181
+ });
182
+ }
183
+
184
+ // src/particleShaders.ts
185
+ var particleVertexShader = `
186
+ precision highp float;
187
+
188
+ uniform float uTime;
189
+ uniform float uPixelRatio;
190
+ uniform float uParticleSize;
191
+ uniform float uInteractionRadius;
192
+ uniform float uDisplacementStrength;
193
+ uniform float uTrailStrength;
194
+ uniform float uDestructTime;
195
+ uniform vec2 uPointer;
196
+ uniform sampler2D uTrailTexture;
197
+
198
+ attribute vec2 aUv;
199
+ attribute float aSeed;
200
+ attribute float aOpacity;
201
+ attribute vec3 aOriginalColor;
202
+
203
+ varying float vOpacity;
204
+ varying float vSeed;
205
+ varying float vTrail;
206
+ varying vec3 vOriginalColor;
207
+
208
+ float hash11(float p) {
209
+ p = fract(p * 0.1031);
210
+ p *= p + 33.33;
211
+ p *= p + p;
212
+ return fract(p);
213
+ }
214
+
215
+ void main() {
216
+ vec3 transformed = position;
217
+ vec2 delta = aUv - uPointer;
218
+ float distanceToPointer = length(delta);
219
+ float influence = 1.0 - smoothstep(0.0, uInteractionRadius, distanceToPointer);
220
+ influence = pow(influence, 1.75);
221
+
222
+ vec2 safeDelta = delta / max(distanceToPointer, 0.0001);
223
+ vec2 repel = safeDelta * influence * uDisplacementStrength;
224
+
225
+ vec4 trailSample = texture2D(uTrailTexture, aUv);
226
+ vec2 trail = trailSample.rg * 2.0 - 1.0;
227
+ float trailEnergy = trailSample.a;
228
+
229
+ float noise = hash11(aSeed + uTime * 0.2);
230
+ vec2 ambient = vec2(sin(uTime * 0.5 + aSeed * 12.1), cos(uTime * 0.37 + aSeed * 4.9)) * 0.0015;
231
+
232
+ transformed.xy += repel;
233
+ transformed.xy += trail * (uTrailStrength * (0.2 + trailEnergy));
234
+ transformed.xy += ambient * (0.5 + noise);
235
+ transformed.z += trailEnergy * 0.04;
236
+
237
+ if (uDestructTime > 0.0) {
238
+ vec3 outward = normalize(transformed + vec3(hash11(aSeed) - 0.5, hash11(aSeed * 2.0) - 0.5, hash11(aSeed * 3.0) - 0.5) * 0.5);
239
+ float speed = (2.0 + hash11(aSeed * 5.0) * 8.0) * uDestructTime + uDestructTime * uDestructTime * 15.0;
240
+ transformed += outward * speed;
241
+ transformed.x += sin(uDestructTime * 10.0 + aSeed * 20.0) * uDestructTime * 0.5;
242
+ transformed.y += cos(uDestructTime * 12.0 + aSeed * 25.0) * uDestructTime * 0.5;
243
+ }
244
+
245
+ vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.0);
246
+ float perspectiveScale = 1.0 / max(0.6, -mvPosition.z);
247
+ gl_PointSize = uParticleSize * uPixelRatio * perspectiveScale * (0.72 + aOpacity * 0.55);
248
+ gl_Position = projectionMatrix * mvPosition;
249
+
250
+ vOpacity = aOpacity;
251
+ vSeed = aSeed;
252
+ vTrail = trailEnergy;
253
+ vOriginalColor = aOriginalColor;
254
+ }
255
+ `;
256
+ var particleFragmentShader = `
257
+ precision highp float;
258
+
259
+ uniform float uTime;
260
+ uniform vec3 uColor;
261
+ uniform float uGlowIntensity;
262
+ uniform int uColorMode;
263
+ uniform float uOpacityVariation;
264
+ uniform float uDestructTime;
265
+
266
+ varying float vOpacity;
267
+ varying float vSeed;
268
+ varying float vTrail;
269
+ varying vec3 vOriginalColor;
270
+
271
+ float hash11(float p) {
272
+ p = fract(p * 0.1031);
273
+ p *= p + 33.33;
274
+ p *= p + p;
275
+ return fract(p);
276
+ }
277
+
278
+ vec3 seedToColor(float s) {
279
+ return vec3(
280
+ 0.5 + 0.5 * sin(s * 6.2831 * 1.0 + 0.0),
281
+ 0.5 + 0.5 * sin(s * 6.2831 * 1.5 + 2.094),
282
+ 0.5 + 0.5 * sin(s * 6.2831 * 2.0 + 4.188)
283
+ );
284
+ }
285
+
286
+ void main() {
287
+ vec2 centered = gl_PointCoord - 0.5;
288
+ float dist = length(centered);
289
+ float soft = 1.0 - smoothstep(0.22, 0.5, dist);
290
+ float halo = 1.0 - smoothstep(0.0, 0.5, dist);
291
+ float sparkle = mix(0.88, 1.08, hash11(vSeed * 37.0 + floor(uTime * 4.0)));
292
+
293
+ float alpha = soft * soft * mix(1.0, 0.55 + vOpacity * 0.45, uOpacityVariation);
294
+ alpha *= mix(0.9, 1.12, vTrail);
295
+
296
+ vec3 baseColor = uColor;
297
+ if (uColorMode == 1) {
298
+ baseColor = seedToColor(vSeed);
299
+ } else if (uColorMode == 2) {
300
+ baseColor = vOriginalColor;
301
+ }
302
+
303
+ float glow = pow(halo, max(0.1, 2.0 - uGlowIntensity)) * uGlowIntensity * 0.6;
304
+ vec3 color = baseColor * (0.72 + halo * 0.32 + glow) * sparkle;
305
+
306
+ if (uDestructTime > 0.0) {
307
+ alpha *= max(0.0, 1.0 - uDestructTime * 0.4);
308
+ }
309
+
310
+ gl_FragColor = vec4(color, alpha);
311
+ }
312
+ `;
313
+ var trailVertexShader = `
314
+ precision highp float;
315
+
316
+ varying vec2 vUv;
317
+
318
+ void main() {
319
+ vUv = uv;
320
+ gl_Position = vec4(position.xy, 0.0, 1.0);
321
+ }
322
+ `;
323
+ var trailFragmentShader = `
324
+ precision highp float;
325
+
326
+ uniform sampler2D uPreviousTrail;
327
+ uniform vec2 uPointer;
328
+ uniform vec2 uPreviousPointer;
329
+ uniform float uDecay;
330
+ uniform float uBrushSize;
331
+ uniform float uBrushStrength;
332
+ uniform float uTime;
333
+
334
+ varying vec2 vUv;
335
+
336
+ float lineMask(vec2 p, vec2 a, vec2 b, float radius) {
337
+ vec2 pa = p - a;
338
+ vec2 ba = b - a;
339
+ float h = clamp(dot(pa, ba) / max(dot(ba, ba), 0.0001), 0.0, 1.0);
340
+ return 1.0 - smoothstep(radius * 0.72, radius, length(pa - ba * h));
341
+ }
342
+
343
+ void main() {
344
+ vec4 texColor = texture2D(uPreviousTrail, vUv);
345
+
346
+ // Decode flow from 0..1 to -1..1
347
+ vec2 prevFlow = texColor.rg * 2.0 - 1.0;
348
+
349
+ // Apply decay
350
+ prevFlow *= uDecay;
351
+ float prevEnergy = texColor.a * uDecay;
352
+
353
+ float pointerGlow = 1.0 - smoothstep(uBrushSize, uBrushSize * 1.7, length(vUv - uPointer));
354
+ float trailGlow = lineMask(vUv, uPreviousPointer, uPointer, uBrushSize * 0.75);
355
+ float pulse = 0.9 + 0.1 * sin(uTime * 2.0);
356
+
357
+ vec2 motion = uPointer - uPreviousPointer;
358
+ // Scale motion by a constant so faster movements create larger flow,
359
+ // without jumping to 100% force on tiny directional changes.
360
+ vec2 flow = motion * 50.0 * (pointerGlow + trailGlow);
361
+
362
+ vec2 newFlow = clamp(prevFlow + flow * uBrushStrength, -1.0, 1.0);
363
+ float newEnergy = max(prevEnergy, max(pointerGlow, trailGlow) * pulse * uBrushStrength);
364
+
365
+ // Encode flow back to 0..1
366
+ gl_FragColor = vec4(newFlow * 0.5 + 0.5, newEnergy, newEnergy);
367
+ }
368
+ `;
369
+
370
+ // src/ParticleSystem.tsx
371
+ var import_jsx_runtime = require("react/jsx-runtime");
372
+ function ParticleSystem({
373
+ image,
374
+ particleCount = 5e4,
375
+ interactionRadius = 0.25,
376
+ displacementStrength = 0.15,
377
+ particleSize = 2,
378
+ trailStrength = 0.18,
379
+ sampleMode = "edge",
380
+ alphaThreshold = 0.04,
381
+ lumaThreshold = 0.64,
382
+ edgeThreshold = 0.08,
383
+ particleColor = "#f5f7fb",
384
+ colorMode = "base",
385
+ glowIntensity = 0.4,
386
+ opacityVariation = 0.5,
387
+ isDestructing = false,
388
+ onDestructComplete,
389
+ onPointsRef,
390
+ onLoadStatus
391
+ }) {
392
+ const [particleData, setParticleData] = (0, import_react.useState)(null);
393
+ const [error, setError] = (0, import_react.useState)(null);
394
+ (0, import_react.useEffect)(() => {
395
+ let mounted = true;
396
+ setParticleData(null);
397
+ setError(null);
398
+ onLoadStatus?.(true, null);
399
+ sampleLogoToParticles(image, particleCount, {
400
+ mode: sampleMode,
401
+ alphaThreshold,
402
+ lumaThreshold,
403
+ edgeThreshold
404
+ }).then((data) => {
405
+ if (mounted) {
406
+ setParticleData(data);
407
+ onLoadStatus?.(false, null);
408
+ }
409
+ }).catch((loadError) => {
410
+ if (mounted) {
411
+ const errMsg = loadError instanceof Error ? loadError.message : "Unable to load particles.";
412
+ setError(errMsg);
413
+ onLoadStatus?.(false, errMsg);
414
+ }
415
+ });
416
+ return () => {
417
+ mounted = false;
418
+ };
419
+ }, [alphaThreshold, edgeThreshold, image, lumaThreshold, particleCount, sampleMode, onLoadStatus]);
420
+ const pointerTarget = (0, import_react.useRef)(new THREE.Vector2(-10, -10));
421
+ const pointerCurrent = (0, import_react.useRef)(new THREE.Vector2(-10, -10));
422
+ const pointerPrevious = (0, import_react.useRef)(new THREE.Vector2(-10, -10));
423
+ const isPointerActive = (0, import_react.useRef)(false);
424
+ const particleGeometry = (0, import_react.useMemo)(() => new THREE.BufferGeometry(), []);
425
+ const particleMaterial = (0, import_react.useMemo)(() => createParticleMaterial(), []);
426
+ const trail = (0, import_react.useMemo)(() => createTrailState(), []);
427
+ const particleMesh = (0, import_react.useRef)(null);
428
+ const { gl, size } = (0, import_fiber.useThree)();
429
+ (0, import_react.useEffect)(() => {
430
+ trail.resize(size.width, size.height);
431
+ }, [gl, size.height, size.width, trail]);
432
+ (0, import_react.useEffect)(() => {
433
+ if (!particleData || error) return;
434
+ particleGeometry.setAttribute("position", new THREE.BufferAttribute(particleData.position, 3));
435
+ particleGeometry.setAttribute("aUv", new THREE.BufferAttribute(particleData.uv, 2));
436
+ particleGeometry.setAttribute("aOriginalColor", new THREE.BufferAttribute(particleData.color, 3));
437
+ particleGeometry.setAttribute("aSeed", new THREE.BufferAttribute(particleData.seed, 1));
438
+ particleGeometry.setAttribute("aOpacity", new THREE.BufferAttribute(particleData.opacity, 1));
439
+ particleGeometry.computeBoundingSphere();
440
+ }, [error, particleData, particleGeometry]);
441
+ (0, import_react.useEffect)(() => {
442
+ if (particleData && particleMesh.current) {
443
+ onPointsRef?.(particleMesh.current);
444
+ }
445
+ return () => onPointsRef?.(null);
446
+ }, [particleData, onPointsRef]);
447
+ const destructStartTime = (0, import_react.useRef)(null);
448
+ (0, import_fiber.useFrame)(({ clock, gl: gl2 }, delta) => {
449
+ if (isPointerActive.current && particleData) {
450
+ if (pointerCurrent.current.x < -5) {
451
+ pointerCurrent.current.copy(pointerTarget.current);
452
+ pointerPrevious.current.copy(pointerTarget.current);
453
+ } else {
454
+ const smoothing = 1 - Math.exp(-delta * 12);
455
+ pointerCurrent.current.lerp(pointerTarget.current, smoothing);
456
+ }
457
+ } else {
458
+ pointerTarget.current.set(-10, -10);
459
+ pointerCurrent.current.set(-10, -10);
460
+ pointerPrevious.current.set(-10, -10);
461
+ }
462
+ particleMaterial.uniforms.uTime.value = clock.elapsedTime;
463
+ particleMaterial.uniforms.uPointer.value.copy(pointerCurrent.current);
464
+ particleMaterial.uniforms.uPixelRatio.value = Math.min(window.devicePixelRatio || 1, 2);
465
+ particleMaterial.uniforms.uInteractionRadius.value = interactionRadius;
466
+ particleMaterial.uniforms.uDisplacementStrength.value = displacementStrength;
467
+ particleMaterial.uniforms.uParticleSize.value = particleSize;
468
+ particleMaterial.uniforms.uTrailStrength.value = trailStrength;
469
+ particleMaterial.uniforms.uGlowIntensity.value = glowIntensity;
470
+ particleMaterial.uniforms.uColorMode.value = colorMode === "original" ? 2 : colorMode === "random" ? 1 : 0;
471
+ particleMaterial.uniforms.uColor.value.set(particleColor);
472
+ particleMaterial.uniforms.uOpacityVariation.value = opacityVariation;
473
+ trail.uniforms.uPointer.value.copy(pointerCurrent.current);
474
+ trail.uniforms.uPreviousPointer.value.copy(pointerPrevious.current);
475
+ trail.uniforms.uTime.value = clock.elapsedTime;
476
+ trail.uniforms.uDecay.value = Math.pow(0.85, delta * 60);
477
+ trail.uniforms.uPreviousTrail.value = trail.read.texture;
478
+ gl2.setRenderTarget(trail.write);
479
+ gl2.render(trail.scene, trail.camera);
480
+ gl2.setRenderTarget(null);
481
+ trail.swap();
482
+ particleMaterial.uniforms.uTrailTexture.value = trail.read.texture;
483
+ pointerPrevious.current.copy(pointerCurrent.current);
484
+ if (particleMesh.current) {
485
+ particleMesh.current.rotation.z = Math.sin(clock.elapsedTime * 0.06) * 8e-3;
486
+ }
487
+ if (isDestructing) {
488
+ if (destructStartTime.current === null) destructStartTime.current = clock.elapsedTime;
489
+ const destructTime = clock.elapsedTime - destructStartTime.current;
490
+ particleMaterial.uniforms.uDestructTime.value = destructTime;
491
+ if (destructTime > 2.5 && onDestructComplete) onDestructComplete();
492
+ } else {
493
+ destructStartTime.current = null;
494
+ particleMaterial.uniforms.uDestructTime.value = 0;
495
+ }
496
+ });
497
+ if (!particleData || error) return null;
498
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
499
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
500
+ "points",
501
+ {
502
+ ref: particleMesh,
503
+ geometry: particleGeometry,
504
+ material: particleMaterial,
505
+ frustumCulled: false,
506
+ scale: [1.6, 1.6, 1]
507
+ }
508
+ ),
509
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
510
+ "mesh",
511
+ {
512
+ visible: false,
513
+ onPointerOver: () => {
514
+ isPointerActive.current = true;
515
+ },
516
+ onPointerOut: () => {
517
+ isPointerActive.current = false;
518
+ },
519
+ onPointerMove: (e) => {
520
+ if (!particleData) return;
521
+ isPointerActive.current = true;
522
+ const imageAspect = particleData.width / particleData.height;
523
+ const meshScale = 1.6;
524
+ const fitScale = 0.96;
525
+ const localX = e.point.x / meshScale;
526
+ const localY = e.point.y / meshScale;
527
+ const uvX = (localX / (fitScale * imageAspect) + 1) * 0.5;
528
+ const uvY = (localY / fitScale + 1) * 0.5;
529
+ pointerTarget.current.set(uvX, uvY);
530
+ },
531
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("planeGeometry", { args: [100, 100] })
532
+ }
533
+ )
534
+ ] });
535
+ }
536
+ function createParticleMaterial() {
537
+ return new THREE.ShaderMaterial({
538
+ transparent: true,
539
+ depthWrite: false,
540
+ depthTest: false,
541
+ uniforms: {
542
+ uTime: { value: 0 },
543
+ uPixelRatio: { value: 1 },
544
+ uParticleSize: { value: 2 },
545
+ uInteractionRadius: { value: 0.25 },
546
+ uDisplacementStrength: { value: 0.15 },
547
+ uTrailStrength: { value: 0.18 },
548
+ uPointer: { value: new THREE.Vector2(0.5, 0.5) },
549
+ uTrailTexture: { value: null },
550
+ uColor: { value: new THREE.Color("#f5f7fb") },
551
+ uGlowIntensity: { value: 0.4 },
552
+ uColorMode: { value: 0 },
553
+ uOpacityVariation: { value: 0.5 },
554
+ uDestructTime: { value: 0 }
555
+ },
556
+ vertexShader: particleVertexShader,
557
+ fragmentShader: particleFragmentShader,
558
+ blending: THREE.AdditiveBlending
559
+ });
560
+ }
561
+ function createTrailState() {
562
+ const scene = new THREE.Scene();
563
+ const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
564
+ const uniforms = {
565
+ uPreviousTrail: { value: null },
566
+ uPointer: { value: new THREE.Vector2(0.5, 0.5) },
567
+ uPreviousPointer: { value: new THREE.Vector2(0.5, 0.5) },
568
+ uDecay: { value: 0.985 },
569
+ uBrushSize: { value: 0.08 },
570
+ uBrushStrength: { value: 0.55 },
571
+ uTime: { value: 0 }
572
+ };
573
+ const material = new THREE.ShaderMaterial({
574
+ transparent: false,
575
+ depthWrite: false,
576
+ depthTest: false,
577
+ uniforms,
578
+ vertexShader: trailVertexShader,
579
+ fragmentShader: trailFragmentShader
580
+ });
581
+ const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);
582
+ scene.add(quad);
583
+ let write = new THREE.WebGLRenderTarget(512, 512, {
584
+ depthBuffer: false,
585
+ stencilBuffer: false,
586
+ generateMipmaps: false,
587
+ minFilter: THREE.LinearFilter,
588
+ magFilter: THREE.LinearFilter,
589
+ format: THREE.RGBAFormat,
590
+ type: THREE.UnsignedByteType
591
+ });
592
+ let read = write.clone();
593
+ const resize = (width, height) => {
594
+ const devicePixelRatio = Math.min(window.devicePixelRatio || 1, 2);
595
+ const resolution = Math.max(384, Math.min(1024, Math.round(Math.max(width, height) * devicePixelRatio)));
596
+ write.setSize(resolution, resolution);
597
+ read.setSize(resolution, resolution);
598
+ uniforms.uPreviousTrail.value = read.texture;
599
+ };
600
+ const swap = () => {
601
+ const temp = write;
602
+ write = read;
603
+ read = temp;
604
+ uniforms.uPreviousTrail.value = read.texture;
605
+ };
606
+ uniforms.uPreviousTrail.value = read.texture;
607
+ return { scene, camera, uniforms, get write() {
608
+ return write;
609
+ }, get read() {
610
+ return read;
611
+ }, resize, swap };
612
+ }
613
+
614
+ // src/LineSystem.tsx
615
+ var import_react2 = require("react");
616
+ var THREE2 = __toESM(require("three"));
617
+ var import_fiber2 = require("@react-three/fiber");
618
+
619
+ // src/lineShaders.ts
620
+ var lineVertexShader = `
621
+ precision highp float;
622
+
623
+ uniform float uTime;
624
+ uniform float uAmplitude;
625
+ uniform float uLineWidth;
626
+ uniform float uPixelRatio;
627
+ uniform float uInteractionRadius;
628
+ uniform float uDisplacementStrength;
629
+ uniform float uDestructTime;
630
+ uniform vec2 uPointer;
631
+
632
+ attribute float aBrightness;
633
+ attribute vec2 aUv;
634
+ attribute float aSeed;
635
+ attribute vec3 aOriginalColor;
636
+
637
+ varying float vBrightness;
638
+ varying float vSeed;
639
+ varying vec3 vOriginalColor;
640
+ varying vec2 vUv;
641
+
642
+ float hash11(float p) {
643
+ p = fract(p * 0.1031);
644
+ p *= p + 33.33;
645
+ p *= p + p;
646
+ return fract(p);
647
+ }
648
+
649
+ void main() {
650
+ vec3 transformed = position;
651
+
652
+ // Pointer repulsion (same as particle system)
653
+ vec2 delta = aUv - uPointer;
654
+ float distanceToPointer = length(delta);
655
+ float influence = 1.0 - smoothstep(0.0, uInteractionRadius, distanceToPointer);
656
+ influence = pow(influence, 1.75);
657
+ vec2 safeDelta = delta / max(distanceToPointer, 0.0001);
658
+ vec2 repel = safeDelta * influence * uDisplacementStrength;
659
+ transformed.xy += repel;
660
+
661
+ // Subtle ambient motion
662
+ float noise = hash11(aSeed + uTime * 0.15);
663
+ vec2 ambient = vec2(
664
+ sin(uTime * 0.3 + aSeed * 8.0),
665
+ cos(uTime * 0.25 + aSeed * 5.0)
666
+ ) * 0.001;
667
+ transformed.xy += ambient * (0.5 + noise);
668
+
669
+ // Self-destruct
670
+ if (uDestructTime > 0.0) {
671
+ vec3 outward = normalize(transformed + vec3(
672
+ hash11(aSeed) - 0.5,
673
+ hash11(aSeed * 2.0) - 0.5,
674
+ hash11(aSeed * 3.0) - 0.5
675
+ ) * 0.5);
676
+ float speed = (2.0 + hash11(aSeed * 5.0) * 8.0) * uDestructTime + uDestructTime * uDestructTime * 15.0;
677
+ transformed += outward * speed;
678
+ }
679
+
680
+ vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.0);
681
+ float perspectiveScale = 1.0 / max(0.6, -mvPosition.z);
682
+ gl_PointSize = uLineWidth * uPixelRatio * perspectiveScale;
683
+ gl_Position = projectionMatrix * mvPosition;
684
+
685
+ vBrightness = aBrightness;
686
+ vSeed = aSeed;
687
+ vOriginalColor = aOriginalColor;
688
+ vUv = aUv;
689
+ }
690
+ `;
691
+ var lineFragmentShader = `
692
+ precision highp float;
693
+
694
+ uniform float uTime;
695
+ uniform vec3 uColor;
696
+ uniform float uGlowIntensity;
697
+ uniform int uColorMode;
698
+ uniform float uOpacityVariation;
699
+ uniform float uDestructTime;
700
+
701
+ varying float vBrightness;
702
+ varying float vSeed;
703
+ varying vec3 vOriginalColor;
704
+ varying vec2 vUv;
705
+
706
+ float hash11(float p) {
707
+ p = fract(p * 0.1031);
708
+ p *= p + 33.33;
709
+ p *= p + p;
710
+ return fract(p);
711
+ }
712
+
713
+ vec3 seedToColor(float s) {
714
+ return vec3(
715
+ 0.5 + 0.5 * sin(s * 6.2831 * 1.0 + 0.0),
716
+ 0.5 + 0.5 * sin(s * 6.2831 * 1.5 + 2.094),
717
+ 0.5 + 0.5 * sin(s * 6.2831 * 2.0 + 4.188)
718
+ );
719
+ }
720
+
721
+ void main() {
722
+ // Soft circular point for smooth line appearance
723
+ vec2 centered = gl_PointCoord - 0.5;
724
+ float dist = length(centered);
725
+ float soft = 1.0 - smoothstep(0.3, 0.5, dist);
726
+
727
+ float alpha = vBrightness * mix(1.0, 0.55 + vBrightness * 0.45, uOpacityVariation);
728
+ alpha *= soft;
729
+ float sparkle = mix(0.88, 1.08, hash11(vSeed * 37.0 + floor(uTime * 4.0)));
730
+
731
+ vec3 baseColor = uColor;
732
+ if (uColorMode == 1) {
733
+ baseColor = seedToColor(vSeed);
734
+ } else if (uColorMode == 2) {
735
+ baseColor = vOriginalColor;
736
+ }
737
+
738
+ float glow = uGlowIntensity * 0.4;
739
+ vec3 color = baseColor * (0.72 + vBrightness * 0.32 + glow) * sparkle;
740
+
741
+ if (uDestructTime > 0.0) {
742
+ alpha *= max(0.0, 1.0 - uDestructTime * 0.4);
743
+ }
744
+
745
+ gl_FragColor = vec4(color, alpha);
746
+ }
747
+ `;
748
+
749
+ // src/LineSystem.tsx
750
+ var import_jsx_runtime2 = require("react/jsx-runtime");
751
+ async function sampleImageToLines(imageUrl, lineCount, amplitude, sampleMode = "alpha", edgeThreshold = 0.075) {
752
+ const image = await loadImg(imageUrl);
753
+ const maxTextureSize = 1024;
754
+ const scale = Math.min(1, maxTextureSize / Math.max(image.width, image.height));
755
+ const width = Math.max(1, Math.round(image.width * scale));
756
+ const height = Math.max(1, Math.round(image.height * scale));
757
+ const canvas = document.createElement("canvas");
758
+ canvas.width = width;
759
+ canvas.height = height;
760
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
761
+ if (!ctx) throw new Error("Unable to create 2D context");
762
+ ctx.clearRect(0, 0, width, height);
763
+ ctx.drawImage(image, 0, 0, width, height);
764
+ const imageData = ctx.getImageData(0, 0, width, height).data;
765
+ const luminance = new Float32Array(width * height);
766
+ for (let i = 0; i < width * height; i++) {
767
+ const off = i * 4;
768
+ luminance[i] = imageData[off] / 255 * 0.2126 + imageData[off + 1] / 255 * 0.7152 + imageData[off + 2] / 255 * 0.0722;
769
+ }
770
+ const imageAspect = width / height;
771
+ const fitScale = 0.96;
772
+ const lines = [];
773
+ const actualLineCount = Math.min(lineCount, width);
774
+ const step = width / actualLineCount;
775
+ for (let i = 0; i < actualLineCount; i++) {
776
+ const x = Math.floor(i * step);
777
+ const positions = [];
778
+ const brightness = [];
779
+ const uvs = [];
780
+ const seeds = [];
781
+ const colors = [];
782
+ for (let y = 0; y < height; y++) {
783
+ const idx = (y * width + x) * 4;
784
+ const r = imageData[idx] / 255;
785
+ const g = imageData[idx + 1] / 255;
786
+ const b = imageData[idx + 2] / 255;
787
+ const a = imageData[idx + 3] / 255;
788
+ const luma = r * 0.2126 + g * 0.7152 + b * 0.0722;
789
+ let score = 0;
790
+ const alphaThreshold = 0.04;
791
+ const lumaThreshold = 0.64;
792
+ if (sampleMode === "alpha") {
793
+ score = a > alphaThreshold ? a : 0;
794
+ } else if (sampleMode === "luma") {
795
+ score = luma > lumaThreshold ? luma : 0;
796
+ } else if (sampleMode === "hybrid") {
797
+ score = Math.max(a > alphaThreshold ? a : 0, luma > lumaThreshold ? luma : 0);
798
+ } else {
799
+ const left = luminance[y * width + Math.max(0, x - 1)];
800
+ const right = luminance[y * width + Math.min(width - 1, x + 1)];
801
+ const up = luminance[Math.max(0, y - 1) * width + x];
802
+ const down = luminance[Math.min(height - 1, y + 1) * width + x];
803
+ const gradient = Math.abs(right - left) + Math.abs(down - up);
804
+ score = gradient > edgeThreshold ? gradient : 0;
805
+ }
806
+ const bright = score > 0 ? luma * a : 0;
807
+ const baseX = ((x + 0.5) / width - 0.5) * 2 * fitScale * imageAspect;
808
+ const displaceX = baseX + bright * amplitude;
809
+ const posY = ((y + 0.5) / height - 0.5) * -2 * fitScale;
810
+ const seedVal = Math.abs(Math.sin(x * 12.9898 + y * 78.233) * 43758.5453) % 1;
811
+ positions.push(displaceX, posY, seedVal * 5e-3);
812
+ brightness.push(bright);
813
+ uvs.push((x + 0.5) / width, 1 - (y + 0.5) / height);
814
+ seeds.push(seedVal);
815
+ colors.push(r, g, b);
816
+ }
817
+ if (positions.length > 0) {
818
+ lines.push({
819
+ positions: new Float32Array(positions),
820
+ brightness: new Float32Array(brightness),
821
+ uvs: new Float32Array(uvs),
822
+ seeds: new Float32Array(seeds),
823
+ colors: new Float32Array(colors),
824
+ vertexCount: positions.length / 3
825
+ });
826
+ }
827
+ }
828
+ return { lines, width, height };
829
+ }
830
+ function loadImg(imageUrl) {
831
+ return new Promise((resolve, reject) => {
832
+ const image = new Image();
833
+ image.crossOrigin = "anonymous";
834
+ image.onload = () => resolve(image);
835
+ image.onerror = () => reject(new Error(`Unable to load image at ${imageUrl}`));
836
+ image.src = imageUrl;
837
+ });
838
+ }
839
+ function LineSystem({
840
+ image,
841
+ lineCount = 100,
842
+ lineWidth = 1.5,
843
+ lineAmplitude = 0.15,
844
+ interactionRadius = 0.25,
845
+ displacementStrength = 0.15,
846
+ sampleMode = "alpha",
847
+ edgeThreshold = 0.075,
848
+ particleColor = "#f5f7fb",
849
+ colorMode = "base",
850
+ glowIntensity = 0.4,
851
+ opacityVariation = 0.5,
852
+ isDestructing = false,
853
+ onDestructComplete,
854
+ onLoadStatus
855
+ }) {
856
+ const [lineData, setLineData] = (0, import_react2.useState)(null);
857
+ (0, import_react2.useEffect)(() => {
858
+ let mounted = true;
859
+ setLineData(null);
860
+ onLoadStatus?.(true, null);
861
+ sampleImageToLines(image, lineCount, lineAmplitude, sampleMode, edgeThreshold).then((data) => {
862
+ if (mounted) {
863
+ setLineData(data);
864
+ onLoadStatus?.(false, null);
865
+ }
866
+ }).catch((err) => {
867
+ if (mounted) {
868
+ const msg = err instanceof Error ? err.message : "Failed to sample lines.";
869
+ onLoadStatus?.(false, msg);
870
+ }
871
+ });
872
+ return () => {
873
+ mounted = false;
874
+ };
875
+ }, [image, lineCount, lineAmplitude, sampleMode, edgeThreshold, onLoadStatus]);
876
+ const groupRef = (0, import_react2.useRef)(null);
877
+ const pointerTarget = (0, import_react2.useRef)(new THREE2.Vector2(-10, -10));
878
+ const pointerCurrent = (0, import_react2.useRef)(new THREE2.Vector2(-10, -10));
879
+ const isPointerActive = (0, import_react2.useRef)(false);
880
+ const destructStartTime = (0, import_react2.useRef)(null);
881
+ const linesRef = (0, import_react2.useRef)([]);
882
+ const { scene } = (0, import_fiber2.useThree)();
883
+ const material = (0, import_react2.useMemo)(() => {
884
+ return new THREE2.ShaderMaterial({
885
+ transparent: true,
886
+ depthWrite: false,
887
+ depthTest: false,
888
+ uniforms: {
889
+ uTime: { value: 0 },
890
+ uAmplitude: { value: 0.15 },
891
+ uLineWidth: { value: 1.5 },
892
+ uPixelRatio: { value: 1 },
893
+ uInteractionRadius: { value: 0.25 },
894
+ uDisplacementStrength: { value: 0.15 },
895
+ uDestructTime: { value: 0 },
896
+ uPointer: { value: new THREE2.Vector2(-10, -10) },
897
+ uColor: { value: new THREE2.Color("#f5f7fb") },
898
+ uGlowIntensity: { value: 0.4 },
899
+ uColorMode: { value: 0 },
900
+ uOpacityVariation: { value: 0.5 }
901
+ },
902
+ vertexShader: lineVertexShader,
903
+ fragmentShader: lineFragmentShader,
904
+ blending: THREE2.AdditiveBlending
905
+ });
906
+ }, []);
907
+ (0, import_react2.useEffect)(() => {
908
+ const group = groupRef.current;
909
+ if (!group || !lineData) return;
910
+ linesRef.current.forEach((line) => {
911
+ group.remove(line);
912
+ line.geometry.dispose();
913
+ });
914
+ linesRef.current = [];
915
+ lineData.lines.forEach((lineInfo) => {
916
+ const geom = new THREE2.BufferGeometry();
917
+ geom.setAttribute("position", new THREE2.BufferAttribute(lineInfo.positions, 3));
918
+ geom.setAttribute("aBrightness", new THREE2.BufferAttribute(lineInfo.brightness, 1));
919
+ geom.setAttribute("aUv", new THREE2.BufferAttribute(lineInfo.uvs, 2));
920
+ geom.setAttribute("aSeed", new THREE2.BufferAttribute(lineInfo.seeds, 1));
921
+ geom.setAttribute("aOriginalColor", new THREE2.BufferAttribute(lineInfo.colors, 3));
922
+ const lineObj = new THREE2.Points(geom, material);
923
+ lineObj.frustumCulled = false;
924
+ group.add(lineObj);
925
+ linesRef.current.push(lineObj);
926
+ });
927
+ return () => {
928
+ linesRef.current.forEach((line) => {
929
+ group.remove(line);
930
+ line.geometry.dispose();
931
+ });
932
+ linesRef.current = [];
933
+ };
934
+ }, [lineData, material, scene]);
935
+ (0, import_fiber2.useFrame)(({ clock }, delta) => {
936
+ if (isPointerActive.current) {
937
+ if (pointerCurrent.current.x < -5) {
938
+ pointerCurrent.current.copy(pointerTarget.current);
939
+ } else {
940
+ const smoothing = 1 - Math.exp(-delta * 12);
941
+ pointerCurrent.current.lerp(pointerTarget.current, smoothing);
942
+ }
943
+ } else {
944
+ pointerTarget.current.set(-10, -10);
945
+ pointerCurrent.current.set(-10, -10);
946
+ }
947
+ const colorModeValue = colorMode === "original" ? 2 : colorMode === "random" ? 1 : 0;
948
+ material.uniforms.uTime.value = clock.elapsedTime;
949
+ material.uniforms.uPointer.value.copy(pointerCurrent.current);
950
+ material.uniforms.uInteractionRadius.value = interactionRadius;
951
+ material.uniforms.uDisplacementStrength.value = displacementStrength;
952
+ material.uniforms.uLineWidth.value = lineWidth;
953
+ material.uniforms.uPixelRatio.value = Math.min(window.devicePixelRatio || 1, 2);
954
+ material.uniforms.uColor.value.set(particleColor);
955
+ material.uniforms.uGlowIntensity.value = glowIntensity;
956
+ material.uniforms.uColorMode.value = colorModeValue;
957
+ material.uniforms.uOpacityVariation.value = opacityVariation;
958
+ if (isDestructing) {
959
+ if (destructStartTime.current === null) destructStartTime.current = clock.elapsedTime;
960
+ const destructTime = clock.elapsedTime - destructStartTime.current;
961
+ material.uniforms.uDestructTime.value = destructTime;
962
+ if (destructTime > 2.5 && onDestructComplete) onDestructComplete();
963
+ } else {
964
+ destructStartTime.current = null;
965
+ material.uniforms.uDestructTime.value = 0;
966
+ }
967
+ if (groupRef.current) {
968
+ groupRef.current.rotation.z = Math.sin(clock.elapsedTime * 0.06) * 8e-3;
969
+ }
970
+ });
971
+ if (!lineData) return null;
972
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
973
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("group", { ref: groupRef, scale: [1.6, 1.6, 1] }),
974
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
975
+ "mesh",
976
+ {
977
+ visible: false,
978
+ onPointerOver: () => {
979
+ isPointerActive.current = true;
980
+ },
981
+ onPointerOut: () => {
982
+ isPointerActive.current = false;
983
+ },
984
+ onPointerMove: (e) => {
985
+ isPointerActive.current = true;
986
+ const imageAspect = lineData.width / lineData.height;
987
+ const meshScale = 1.6;
988
+ const fitScale = 0.96;
989
+ const localX = e.point.x / meshScale;
990
+ const localY = e.point.y / meshScale;
991
+ const uvX = (localX / (fitScale * imageAspect) + 1) * 0.5;
992
+ const uvY = (localY / fitScale + 1) * 0.5;
993
+ pointerTarget.current.set(uvX, uvY);
994
+ },
995
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("planeGeometry", { args: [100, 100] })
996
+ }
997
+ )
998
+ ] });
999
+ }
1000
+
1001
+ // src/performance.ts
1002
+ function detectPerformanceTier() {
1003
+ const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
1004
+ typeof navigator !== "undefined" ? navigator.userAgent : ""
1005
+ );
1006
+ if (isMobile) return "low";
1007
+ const nav = typeof navigator !== "undefined" ? navigator : {};
1008
+ const memory = nav.deviceMemory || 4;
1009
+ const cores = nav.hardwareConcurrency || 4;
1010
+ if (memory < 4 || cores <= 2) return "low";
1011
+ if (memory <= 8 || cores <= 4) return "medium";
1012
+ if (memory <= 16 || cores <= 8) return "high";
1013
+ return "ultra";
1014
+ }
1015
+ function getPresetSettings(tier) {
1016
+ switch (tier) {
1017
+ case "low":
1018
+ return { particleCount: 15e3, particleSize: 3, lineCount: 50 };
1019
+ case "medium":
1020
+ return { particleCount: 3e4, particleSize: 2.5, lineCount: 100 };
1021
+ case "ultra":
1022
+ return { particleCount: 1e5, particleSize: 1.5, lineCount: 300 };
1023
+ case "high":
1024
+ default:
1025
+ return { particleCount: 5e4, particleSize: 2, lineCount: 200 };
1026
+ }
1027
+ }
1028
+ // Annotate the CommonJS export names for ESM import in node:
1029
+ 0 && (module.exports = {
1030
+ LineSystem,
1031
+ ParticleSystem,
1032
+ detectPerformanceTier,
1033
+ getPresetSettings,
1034
+ sampleLogoToParticles
1035
+ });