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