particle-image 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.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # ParticleImage
2
+
3
+ A high-performance 3D particle morphing library built with Three.js. Transform particles into any image (Path, Base64, or SVG) with smooth GPGPU-powered animations.
4
+
5
+ **English | [中文](./README.zh_CN.md)**
6
+
7
+ https://github.com/user-attachments/assets/aeac5d4f-d23a-4751-bc38-550211f4c67e
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install particle-image
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```javascript
18
+ import { ParticleImage } from 'particle-image';
19
+
20
+ const canvas = document.getElementById('canvas');
21
+ const effect = new ParticleImage(canvas, {
22
+ theme: 'dark', // 'dark' | 'light' (default: 'dark')
23
+ color: '#aecbfa', // Base particle color
24
+ density: 200, // Number of particles (default: 150)
25
+ particlesScale: 0.6, // Scale of the morphed image (default: 0.5)
26
+ cameraZoom: 3.5, // Camera distance (default: 3.5)
27
+ duration: 0.8 // Animation duration in seconds (default: 0.6)
28
+ });
29
+
30
+ // Morph to an image
31
+ await effect.render('./path/to/image.png');
32
+
33
+ // Scatter particles back to background state
34
+ effect.scatter();
35
+
36
+ // Clean up resources
37
+ effect.destroy();
38
+ ```
39
+
40
+ ## Options
41
+
42
+ | Property | Type | Default | Description |
43
+ | :--- | :--- | :--- | :--- |
44
+ | `theme` | `string` | `'dark'` | Visual theme (`'dark'` or `'light'`). Affects background and auto-default color. |
45
+ | `color` | `string` | *Varies* | Hex color for particles in scattered/background state. Defaults to light blue for dark theme, dark grey for light theme. |
46
+ | `density` | `number` | `150` | Higher values result in more particles and sharper images. |
47
+ | `particlesScale` | `number` | `0.5` | The target size of the image relative to the canvas. |
48
+ | `cameraZoom` | `number` | `3.5` | Perspective zoom level. Lower values create more wide-angle distortion. |
49
+ | `duration` | `number` | `0.6` | Transition time for `render()` and `scatter()` animations. |
50
+
51
+ ## API
52
+
53
+ ### `render(imageSource)`
54
+ - **Arguments**: `imageSource` (String: URL path, Base64 data, or raw SVG string).
55
+ - **Returns**: `Promise<void>`
56
+ - **Description**: Triggers the particle morphing animation towards the provided image. Uses an internal LRU cache to store processed data for instant repeated calls.
57
+
58
+ ### `scatter()`
59
+ - **Description**: Resets particles back to their original wandering background state.
60
+
61
+ ### `destroy()`
62
+ - **Description**: Stops the animation loop and releases all memory and GPU resources used by the instance. Use this when the component is unmounted to prevent memory leaks.
63
+
64
+ ## License
65
+
66
+ MIT
@@ -0,0 +1,66 @@
1
+ # ParticleImage
2
+
3
+ 基于 Three.js 开发的高性能 3D 粒子变形特效库。支持将粒子平滑地聚合形成特定的图像(支持 路径、Base64 或 SVG 源码),并带有高性能的着色器动画效果。
4
+
5
+ **[English](./README.md) | 中文**
6
+
7
+ https://github.com/user-attachments/assets/aeac5d4f-d23a-4751-bc38-550211f4c67e
8
+
9
+ ## 安装
10
+
11
+ ```bash
12
+ npm install particle-image
13
+ ```
14
+
15
+ ## 基础用法
16
+
17
+ ```javascript
18
+ import { ParticleImage } from 'particle-image';
19
+
20
+ const canvas = document.getElementById('canvas');
21
+ const effect = new ParticleImage(canvas, {
22
+ theme: 'dark', // 'dark' | 'light' (默认: 'dark')
23
+ color: '#aecbfa', // 粒子基础颜色
24
+ density: 200, // 粒子密度/数量 (默认: 150)
25
+ particlesScale: 0.6, // 聚合图像的缩放比例 (默认: 0.5)
26
+ cameraZoom: 3.5, // 摄像机焦距/透视距离 (默认: 3.5)
27
+ duration: 0.8 // 动画切换时长,单位秒 (默认: 0.6)
28
+ });
29
+
30
+ // 变形为指定图片
31
+ await effect.render('./path/to/image.png');
32
+
33
+ // 将粒子散开回到背景随机漫游状态
34
+ effect.scatter();
35
+
36
+ // 销毁实例并释放内存
37
+ effect.destroy();
38
+ ```
39
+
40
+ ## 配置项 (Options)
41
+
42
+ | 属性 | 类型 | 默认值 | 说明 |
43
+ | :--- | :--- | :--- | :--- |
44
+ | `theme` | `string` | `'dark'` | 视觉主题 (`'dark'` 或 `'light'`)。影响背景色及默认粒子颜色。 |
45
+ | `color` | `string` | *根据主题* | 粒子在随机漫游状态下的 Hex 颜色值。深色主题默认为浅蓝,浅色主题默认为深灰。 |
46
+ | `density` | `number` | `150` | 粒子密集度。值越大粒子越多,形成的图像越清晰。 |
47
+ | `particlesScale` | `number` | `0.5` | 聚合图像相对于画布的大小比例。 |
48
+ | `cameraZoom` | `number` | `3.5` | 透视缩放。值越小广角畸变越强,值越大画面越趋于平整。 |
49
+ | `duration` | `number` | `0.6` | `render()` 和 `scatter()` 状态切换动画的持续时间。 |
50
+
51
+ ## API 接口
52
+
53
+ ### `render(imageSource)`
54
+ - **参数**: `imageSource` (String: URL 路径, Base64 数据, 或原生 SVG 字符串)。
55
+ - **返回值**: `Promise<void>`
56
+ - **说明**: 触发粒子聚合动画。内置 LRU 缓存,相同图片的重复调用将通过缓存瞬时完成。
57
+
58
+ ### `scatter()`
59
+ - **说明**: 触发粒子散开动画,使其回到初始的随机分布背景状态。
60
+
61
+ ### `destroy()`
62
+ - **说明**: 停止动画循环并彻底释放实例占用的所有显存与内存资源。建议在组件销毁时调用,以确保没有内存泄漏。
63
+
64
+ ## 开源协议
65
+
66
+ MIT
@@ -0,0 +1,73 @@
1
+
2
+ export interface ParticleImageOptions {
3
+ /**
4
+ * Visual theme of the background and default particle color.
5
+ * @default 'dark'
6
+ */
7
+ theme?: 'dark' | 'light';
8
+
9
+ /**
10
+ * Base hex color for particles in scattered or transition state.
11
+ * Defaults to light blue for dark theme, and dark grey for light theme.
12
+ */
13
+ color?: string;
14
+
15
+ /**
16
+ * Scale of the morphed image relative to the canvas.
17
+ * @default 0.5
18
+ */
19
+ particlesScale?: number;
20
+
21
+ /**
22
+ * Particle density. Higher values result in more particles and sharper images.
23
+ * @default 150
24
+ */
25
+ density?: number;
26
+
27
+ /**
28
+ * Perspective zoom level for the 3D camera.
29
+ * @default 3.5
30
+ */
31
+ cameraZoom?: number;
32
+
33
+ /**
34
+ * Duration of the morphing and scattering animations in seconds.
35
+ * @default 0.6
36
+ */
37
+ duration?: number;
38
+ }
39
+
40
+ /**
41
+ * ParticleImage is a high-performance 3D particle morphing library built with Three.js.
42
+ */
43
+ export class ParticleImage {
44
+ /**
45
+ * Creates an instance of ParticleImage.
46
+ * @param canvas The HTMLCanvasElement to render on.
47
+ * @param options Configuration options for the particle effect.
48
+ */
49
+ constructor(canvas: HTMLCanvasElement, options?: ParticleImageOptions);
50
+
51
+ /**
52
+ * The current progress of the morph animation (0 to 1).
53
+ */
54
+ progress: number;
55
+
56
+ /**
57
+ * Triggers the morphing animation to the specified image.
58
+ * Fits the image within the canvas while maintaining aspect ratio.
59
+ * @param imageSource URL path, Base64 string, or raw SVG string.
60
+ * @returns A promise that resolves when the image processing is complete and animation starts.
61
+ */
62
+ render(imageSource: string): Promise<void>;
63
+
64
+ /**
65
+ * Triggers the scattering animation, returning particles to their wandering background state.
66
+ */
67
+ scatter(): void;
68
+
69
+ /**
70
+ * Stops the animation loop and releases all memory and GPU resources.
71
+ */
72
+ destroy(): void;
73
+ }
@@ -0,0 +1,600 @@
1
+ import * as s from "three";
2
+ import x from "gsap";
3
+ import T from "poisson-disk-sampling";
4
+ const f = `
5
+ // MATHS
6
+ vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); }
7
+ vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
8
+ float permute(float x){return floor(mod(((x*34.0)+1.0)*x, 289.0));}
9
+
10
+ vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
11
+ float taylorInvSqrt(float r){return 1.79284291400159 - 0.85373472095314 * r;}
12
+
13
+ // SIMPLEX NOISES
14
+ // Simplex 2D noise
15
+ float snoise(vec2 v){
16
+ const vec4 C = vec4(0.211324865405187, 0.366025403784439,
17
+ -0.577350269189626, 0.024390243902439);
18
+ vec2 i = floor(v + dot(v, C.yy) );
19
+ vec2 x0 = v - i + dot(i, C.xx);
20
+ vec2 i1;
21
+ i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
22
+ vec4 x12 = x0.xyxy + C.xxzz;
23
+ x12.xy -= i1;
24
+ i = mod(i, 289.0);
25
+ vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
26
+ + i.x + vec3(0.0, i1.x, 1.0 ));
27
+ vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy),
28
+ dot(x12.zw,x12.zw)), 0.0);
29
+ m = m*m ;
30
+ m = m*m ;
31
+ vec3 x = 2.0 * fract(p * C.www) - 1.0;
32
+ vec3 h = abs(x) - 0.5;
33
+ vec3 ox = floor(x + 0.5);
34
+ vec3 a0 = x - ox;
35
+ m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
36
+ vec3 g;
37
+ g.x = a0.x * x0.x + h.x * x0.y;
38
+ g.yz = a0.yz * x12.xz + h.yz * x12.yw;
39
+ return 130.0 * dot(m, g);
40
+ }
41
+
42
+ // Simplex 3D Noise
43
+ float snoise(vec3 v){
44
+ const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
45
+ const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
46
+
47
+ vec3 i = floor(v + dot(v, C.yyy) );
48
+ vec3 x0 = v - i + dot(i, C.xxx) ;
49
+
50
+ vec3 g = step(x0.yzx, x0.xyz);
51
+ vec3 l = 1.0 - g;
52
+ vec3 i1 = min( g.xyz, l.zxy );
53
+ vec3 i2 = max( g.xyz, l.zxy );
54
+
55
+ vec3 x1 = x0 - i1 + 1.0 * C.xxx;
56
+ vec3 x2 = x0 - i2 + 2.0 * C.xxx;
57
+ vec3 x3 = x0 - 1. + 3.0 * C.xxx;
58
+
59
+ i = mod(i, 289.0 );
60
+ vec4 p = permute( permute( permute(
61
+ i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
62
+ + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
63
+ + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
64
+
65
+ float n_ = 1.0/7.0;
66
+ vec3 ns = n_ * D.wyz - D.xzx;
67
+
68
+ vec4 j = p - 49.0 * floor(p * ns.z *ns.z);
69
+
70
+ vec4 x_ = floor(j * ns.z);
71
+ vec4 y_ = floor(j - 7.0 * x_ );
72
+
73
+ vec4 x = x_ *ns.x + ns.yyyy;
74
+ vec4 y = y_ *ns.x + ns.yyyy;
75
+ vec4 h = 1.0 - abs(x) - abs(y);
76
+
77
+ vec4 b0 = vec4( x.xy, y.xy );
78
+ vec4 b1 = vec4( x.zw, y.zw );
79
+
80
+ vec4 s0 = floor(b0)*2.0 + 1.0;
81
+ vec4 s1 = floor(b1)*2.0 + 1.0;
82
+ vec4 sh = -step(h, vec4(0.0));
83
+
84
+ vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
85
+ vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
86
+
87
+ vec3 p0 = vec3(a0.xy,h.x);
88
+ vec3 p1 = vec3(a0.zw,h.y);
89
+ vec3 p2 = vec3(a1.xy,h.z);
90
+ vec3 p3 = vec3(a1.zw,h.w);
91
+
92
+ vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
93
+ p0 *= norm.x;
94
+ p1 *= norm.y;
95
+ p2 *= norm.z;
96
+ p3 *= norm.w;
97
+
98
+ vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
99
+ m = m * m;
100
+ return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
101
+ dot(p2,x2), dot(p3,x3) ) );
102
+ }
103
+
104
+ vec4 grad4(float j, vec4 ip){
105
+ const vec4 ones = vec4(1.0, 1.0, 1.0, -1.0);
106
+ vec4 p,s;
107
+
108
+ p.xyz = floor( fract (vec3(j) * ip.xyz) * 7.0) * ip.z - 1.0;
109
+ p.w = 1.5 - dot(abs(p.xyz), ones.xyz);
110
+ s = vec4(lessThan(p, vec4(0.0)));
111
+ p.xyz = p.xyz + (s.xyz*2.0 - 1.0) * s.www;
112
+
113
+ return p;
114
+ }
115
+
116
+ float snoise(vec4 v){
117
+ const vec2 C = vec2( 0.138196601125010504, 0.309016994374947451);
118
+ vec4 i = floor(v + dot(v, C.yyyy) );
119
+ vec4 x0 = v - i + dot(i, C.xxxx);
120
+
121
+ vec4 i0;
122
+ vec3 isX = step( x0.yzw, x0.xxx );
123
+ vec3 isYZ = step( x0.zww, x0.yyz );
124
+ i0.x = isX.x + isX.y + isX.z;
125
+ i0.yzw = 1.0 - isX;
126
+ i0.y += isYZ.x + isYZ.y;
127
+ i0.zw += 1.0 - isYZ.xy;
128
+ i0.z += isYZ.z;
129
+ i0.w += 1.0 - isYZ.z;
130
+
131
+ vec4 i3 = clamp( i0, 0.0, 1.0 );
132
+ vec4 i2 = clamp( i0-1.0, 0.0, 1.0 );
133
+ vec4 i1 = clamp( i0-2.0, 0.0, 1.0 );
134
+
135
+ vec4 x1 = x0 - i1 + 1.0 * C.xxxx;
136
+ vec4 x2 = x0 - i2 + 2.0 * C.xxxx;
137
+ vec4 x3 = x0 - i3 + 3.0 * C.xxxx;
138
+ vec4 x4 = x0 - 1.0 + 4.0 * C.xxxx;
139
+
140
+ i = mod(i, 289.0);
141
+ float j0 = permute( permute( permute( permute(i.w) + i.z) + i.y) + i.x);
142
+ vec4 j1 = permute( permute( permute( permute (
143
+ i.w + vec4(i1.w, i2.w, i3.w, 1.0 ))
144
+ + i.z + vec4(i1.z, i2.z, i3.z, 1.0 ))
145
+ + i.y + vec4(i1.y, i2.y, i3.y, 1.0 ))
146
+ + i.x + vec4(i1.x, i2.x, i3.x, 1.0 ));
147
+
148
+ vec4 ip = vec4(1.0/294.0, 1.0/49.0, 1.0/7.0, 0.0) ;
149
+
150
+ vec4 p0 = grad4(j0, ip);
151
+ vec4 p1 = grad4(j1.x, ip);
152
+ vec4 p2 = grad4(j1.y, ip);
153
+ vec4 p3 = grad4(j1.z, ip);
154
+ vec4 p4 = grad4(j1.w, ip);
155
+
156
+ vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
157
+ p0 *= norm.x;
158
+ p1 *= norm.y;
159
+ p2 *= norm.z;
160
+ p3 *= norm.w;
161
+ p4 *= taylorInvSqrt(dot(p4,p4));
162
+
163
+ vec3 m0 = max(0.6 - vec3(dot(x0,x0), dot(x1,x1), dot(x2,x2)), 0.0);
164
+ vec2 m1 = max(0.6 - vec2(dot(x3,x3), dot(x4,x4) ), 0.0);
165
+ m0 = m0 * m0;
166
+ m1 = m1 * m1;
167
+ return 49.0 * ( dot(m0*m0, vec3( dot( p0, x0 ), dot( p1, x1 ), dot( p2, x2 )))
168
+ + dot(m1*m1, vec2( dot( p3, x3 ), dot( p4, x4 ) ) ) ) ;
169
+ }
170
+ `, S = `
171
+ precision highp float;
172
+ uniform sampler2D uPosition;
173
+ uniform sampler2D uPosRefs;
174
+ uniform sampler2D uPosNearest;
175
+
176
+ uniform vec2 uMousePos;
177
+ uniform float uTime;
178
+ uniform float uDeltaTime;
179
+ uniform float uProgress;
180
+ uniform float uSize;
181
+
182
+ vec2 hash( vec2 p ){
183
+ p = vec2( dot(p,vec2(2127.1,81.17)), dot(p,vec2(1269.5,283.37)) );
184
+ return fract(sin(p)*43758.5453);
185
+ }
186
+
187
+ void main() {
188
+ vec2 simTexCoords = gl_FragCoord.xy / vec2(uSize, uSize);
189
+ vec4 pFrame = texture2D(uPosition, simTexCoords);
190
+
191
+ float scale = pFrame.z;
192
+ float velocity = pFrame.w;
193
+ vec2 refPos = texture2D(uPosRefs, simTexCoords).xy;
194
+ vec2 nearestPos = texture2D(uPosNearest, simTexCoords).xy;
195
+ float seed = hash(simTexCoords).x;
196
+ float seed2 = hash(simTexCoords).y;
197
+
198
+ float time = uTime * .5;
199
+ float lifeEnd = 3. + sin(seed2 * 100.) * 1.;
200
+ float lifeTime = mod((seed * 100.) + time, lifeEnd);
201
+
202
+ vec2 disp = vec2(0., 0.);
203
+ vec2 pos = pFrame.xy;
204
+
205
+ vec2 targetPos = mix(refPos, nearestPos, uProgress);
206
+
207
+ vec2 direction = normalize(targetPos - pos);
208
+ float dist = length(targetPos - pos);
209
+
210
+ float moveSpeed = mix(0.015, 0.03, uProgress);
211
+ float distStrength = smoothstep(0.0, 0.2, dist);
212
+
213
+ if(dist > 0.002){
214
+ pos += direction * moveSpeed * distStrength;
215
+ }
216
+
217
+ if(lifeTime < .01){
218
+ pos = refPos;
219
+ pFrame.xy = refPos;
220
+ scale = 0.;
221
+ }
222
+
223
+ float targetScale = smoothstep(.01, 0.5, lifeTime) - smoothstep(0.5, 1., lifeTime/lifeEnd);
224
+ targetScale += smoothstep(0.1, 0., smoothstep(0.001, .1, dist)) * 1.5 * uProgress;
225
+
226
+ float scaleDiff = targetScale - scale;
227
+ scaleDiff *= .1;
228
+ scale += scaleDiff;
229
+
230
+ float distRadius = 0.15;
231
+ vec2 finalPos = pos + (disp * smoothstep(0.001, distRadius, dist));
232
+ vec2 diff = finalPos - pFrame.xy;
233
+ diff *= .2;
234
+
235
+ velocity = smoothstep(distRadius, .001, dist) * uProgress;
236
+
237
+ vec4 frame = vec4(pFrame.xy + diff, scale, velocity);
238
+ gl_FragColor = frame;
239
+ }
240
+ `, C = `
241
+ precision highp float;
242
+ attribute vec4 seeds;
243
+
244
+ uniform sampler2D uPosition;
245
+ uniform float uTime;
246
+ uniform float uParticleScale;
247
+ uniform float uPixelRatio;
248
+ uniform int uColorScheme;
249
+ uniform float uProgress;
250
+ uniform float uPulseProgress;
251
+
252
+ varying vec4 vSeeds;
253
+ varying float vVelocity;
254
+ varying vec2 vLocalPos;
255
+ varying vec2 vScreenPos;
256
+ varying float vScale;
257
+ varying vec2 vUv;
258
+
259
+ // NOISE_SHADER_CHUNK will be prepended manually
260
+
261
+ void main() {
262
+ vUv = uv;
263
+
264
+ vec4 pos = texture2D(uPosition, uv);
265
+ vSeeds = seeds;
266
+
267
+ float noiseX = snoise(vec3( vec2(pos.xy * 10.), uTime * .2 + 100.));
268
+ float noiseY = snoise(vec3( vec2(pos.xy * 10.), uTime * .2));
269
+
270
+ float noiseX2 = snoise(vec3( vec2(pos.xy * .5), uTime * .15 + 45.));
271
+ float noiseY2 = snoise(vec3( vec2(pos.xy * .5), uTime * .15 + 87.));
272
+
273
+ float cDist = length(pos.xy) * 1.;
274
+ float progress = uPulseProgress;
275
+ float t = smoothstep(progress - .25, progress, cDist) - smoothstep(progress, progress + .25, cDist);
276
+ t *= smoothstep(1., .0, cDist);
277
+ pos.xy *= 1. + (t * .02);
278
+
279
+ float dist = smoothstep(0., 0.9, pos.w);
280
+ dist = mix(0., dist, uProgress);
281
+
282
+ pos.y += noiseY * 0.005 * dist;
283
+ pos.x += noiseX * 0.005 * dist;
284
+ pos.y += noiseY2 * 0.02;
285
+ pos.x += noiseX2 * 0.02;
286
+
287
+ vVelocity = pos.w;
288
+ vScale = pos.z;
289
+ vLocalPos = pos.xy;
290
+ vec4 viewSpace = modelViewMatrix * vec4(vec3(pos.xy, 0.), 1.0);
291
+
292
+ gl_Position = projectionMatrix * viewSpace;
293
+ vScreenPos = gl_Position.xy;
294
+
295
+ float minScale = .25;
296
+ minScale += float(uColorScheme) * .75;
297
+
298
+ gl_PointSize = ((vScale * 7.) * (uPixelRatio * 0.5) * uParticleScale) + (minScale * uPixelRatio);
299
+ }
300
+ `, R = `
301
+ precision highp float;
302
+
303
+ varying vec4 vSeeds;
304
+ varying vec2 vScreenPos;
305
+ varying vec2 vLocalPos;
306
+ varying float vScale;
307
+ varying float vVelocity;
308
+ varying vec2 vUv;
309
+
310
+ uniform vec3 uColor;
311
+
312
+ uniform vec2 uMousePos;
313
+ uniform vec2 uRez;
314
+
315
+ uniform float uAlpha;
316
+ uniform float uTime;
317
+ uniform sampler2D uColorTex;
318
+ uniform float uProgress;
319
+
320
+ uniform int uColorScheme;
321
+
322
+ // NOISE_SHADER_CHUNK will be prepended manually
323
+
324
+ #define PI 3.1415926535897932384626433832795
325
+
326
+ void main() {
327
+ float uBorderSize = 0.2;
328
+ float ratio = uRez.x / uRez.y;
329
+
330
+ vec2 uv = gl_PointCoord.xy - 0.5;
331
+ uv.y *= -1.;
332
+
333
+ float h = 0.5;
334
+ vec3 gradientColor = mix(uColor, uColor * 0.8, vVelocity);
335
+
336
+ vec3 imgColor = texture2D(uColorTex, vUv).rgb;
337
+ vec3 color = mix(gradientColor, imgColor, uProgress);
338
+
339
+ float dist = length(uv);
340
+
341
+ float dr = .5;
342
+ float t = smoothstep(dr+(uBorderSize + .0001), dr-uBorderSize, dist);
343
+ t = clamp(t, 0., 1.);
344
+
345
+ float disc = smoothstep(.5, .45, dist);
346
+
347
+ float a = uAlpha * disc * smoothstep(0.1, 0.2, vScale);
348
+
349
+ if(a < 0.01){
350
+ discard;
351
+ }
352
+
353
+ color = clamp(color, 0., 1.);
354
+ // For light mode, we want the particles to be darker when scattered
355
+ // To improve dot visibility in light mode, we use uColor directly when not hovering
356
+ vec3 finalCol = mix(color, uColor, float(uColorScheme) * (1.0 - uProgress));
357
+
358
+ gl_FragColor = vec4(finalCol, clamp(a, 0., 1.));
359
+
360
+ #ifdef SRGB_TRANSFER
361
+ gl_FragColor = sRGBTransferOETF( gl_FragColor );
362
+ #endif
363
+ }
364
+ `, y = `(function(){"use strict";self.onmessage=function(w){const{imageData:t,pointsBase:r,density:q,width:g,height:p}=w.data,m=g/2,x=p/2,a=[],M=2;for(let n=0;n<t.height;n+=M)for(let e=0;e<t.width;e+=M){const l=Math.round(e),d=Math.round(n),c=(l+d*t.width)*4,s=t.data[c]/255,i=t.data[c+1]/255,h=t.data[c+2]/255,o=t.data[c+3]/255,y=(1-(.2126*s+.7152*i+.0722*h))*o;y>.1&&Math.random()<y*1.5&&a.push([e,n])}if(a.length===0)for(let n=0;n<r.length;n++)a.push([Math.random()*g,Math.random()*p]);const f=[],u=[],b=r.length,D=a.length;for(let n=0;n<b;n++){const e=r[n][0],l=r[n][1];let d=-1,c=1/0;for(let s=0;s<D;s++){const i=a[s][0]-e,h=a[s][1]-l,o=i*i+h*h;o<c&&(c=o,d=s)}if(d!==-1){const s=a[d];f.push(s[0]-m,s[1]-x);const i=Math.round(s[0]),h=Math.round(s[1]),o=(i+h*t.width)*4;u.push(t.data[o]/255,t.data[o+1]/255,t.data[o+2]/255,t.data[o+3]/255)}else f.push(e-m,l-x),u.push(0,0,0,0)}self.postMessage({nearestPoints:f,nearestColors:u})}})();
365
+ `, g = typeof self < "u" && self.Blob && new Blob(["(self.URL || self.webkitURL).revokeObjectURL(self.location.href);", y], { type: "text/javascript;charset=utf-8" });
366
+ function D(p) {
367
+ let e;
368
+ try {
369
+ if (e = g && (self.URL || self.webkitURL).createObjectURL(g), !e) throw "";
370
+ const t = new Worker(e, {
371
+ name: p?.name
372
+ });
373
+ return t.addEventListener("error", () => {
374
+ (self.URL || self.webkitURL).revokeObjectURL(e);
375
+ }), t;
376
+ } catch {
377
+ return new Worker(
378
+ "data:text/javascript;charset=utf-8," + encodeURIComponent(y),
379
+ {
380
+ name: p?.name
381
+ }
382
+ );
383
+ }
384
+ }
385
+ class M {
386
+ constructor(e = 10, t = null) {
387
+ this.limit = e, this.onDispose = t, this.cache = /* @__PURE__ */ new Map();
388
+ }
389
+ get(e) {
390
+ if (!this.cache.has(e)) return null;
391
+ const t = this.cache.get(e);
392
+ return this.cache.delete(e), this.cache.set(e, t), t;
393
+ }
394
+ set(e, t) {
395
+ if (this.cache.has(e))
396
+ this.cache.delete(e);
397
+ else if (this.cache.size >= this.limit) {
398
+ const i = this.cache.keys().next().value, n = this.cache.get(i);
399
+ this.onDispose && this.onDispose(n), this.cache.delete(i);
400
+ }
401
+ this.cache.set(e, t);
402
+ }
403
+ clear() {
404
+ if (this.onDispose)
405
+ for (let e of this.cache.values())
406
+ this.onDispose(e);
407
+ this.cache.clear();
408
+ }
409
+ }
410
+ class b {
411
+ constructor(e) {
412
+ this.parent = e, this.renderer = e.renderer, this.camera = e.camera, this.lastTime = 0, this.everRendered = !1, this.size = 256, this.length = this.size * this.size, this.colorScheme = e.theme === "dark" ? 0 : 1, this.particleScale = this.renderer.domElement.width / e.pixelRatio / 2e3 * e.particlesScale, this.worker = new D(), this.initBase();
413
+ }
414
+ initBase() {
415
+ const e = (o, m, d, u, v) => (o - m) * (v - u) / (d - m) + u, t = new T({
416
+ shape: [this.parent.width, this.parent.height],
417
+ minDistance: e(this.parent.density, 0, 300, 10, 2),
418
+ maxDistance: e(this.parent.density, 0, 300, 11, 3),
419
+ tries: 15
420
+ });
421
+ this.pointsBaseData = t.fill(), this.pointsData = [];
422
+ const i = this.parent.width / 2, n = this.parent.height / 2;
423
+ for (let o = 0; o < this.pointsBaseData.length; o++)
424
+ this.pointsData.push(this.pointsBaseData[o][0] - i, this.pointsBaseData[o][1] - n);
425
+ this.count = this.pointsData.length / 2, this.posTex = this.createDataTexturePosition(this.pointsData), this.posNearestTex = this.createDataTexturePosition(this.pointsData), this.colorNearestTex = this.createDataTextureColor(new Float32Array(this.length * 4)), this.rt1 = this.createRenderTarget(), this.rt2 = this.createRenderTarget(), this.simScene = new s.Scene(), this.simCamera = new s.OrthographicCamera(-1, 1, 1, -1, 0, 1), this.simMaterial = new s.ShaderMaterial({
426
+ uniforms: {
427
+ uPosition: { value: this.posTex },
428
+ uPosRefs: { value: this.posTex },
429
+ uPosNearest: { value: this.posNearestTex },
430
+ uMousePos: { value: new s.Vector2(0, 0) },
431
+ uTime: { value: 0 },
432
+ uDeltaTime: { value: 0 },
433
+ uProgress: { value: 0 },
434
+ uSize: { value: this.size }
435
+ },
436
+ vertexShader: `
437
+ void main() {
438
+ gl_Position = vec4(position, 1.0);
439
+ }
440
+ `,
441
+ fragmentShader: S
442
+ });
443
+ const c = new s.Mesh(new s.PlaneGeometry(2, 2), this.simMaterial);
444
+ this.simScene.add(c);
445
+ const a = new s.BufferGeometry(), l = new Float32Array(this.count * 3), r = new Float32Array(this.count * 2), h = new Float32Array(this.count * 4);
446
+ for (let o = 0; o < this.count; o++) {
447
+ let m = o % this.size, d = Math.floor(o / this.size);
448
+ r[o * 2] = m / this.size, r[o * 2 + 1] = d / this.size, h[o * 4] = Math.random(), h[o * 4 + 1] = Math.random(), h[o * 4 + 2] = Math.random(), h[o * 4 + 3] = Math.random();
449
+ }
450
+ a.setAttribute("position", new s.BufferAttribute(l, 3)), a.setAttribute("uv", new s.BufferAttribute(r, 2)), a.setAttribute("seeds", new s.BufferAttribute(h, 4)), this.renderMaterial = new s.ShaderMaterial({
451
+ uniforms: {
452
+ uPosition: { value: this.posTex },
453
+ uColorTex: { value: this.colorNearestTex },
454
+ uTime: { value: 0 },
455
+ uColor: { value: new s.Color(this.parent.color) },
456
+ uAlpha: { value: 1 },
457
+ uProgress: { value: 0 },
458
+ uPulseProgress: { value: 0 },
459
+ uMousePos: { value: new s.Vector2(0, 0) },
460
+ uRez: { value: new s.Vector2(this.renderer.domElement.width, this.renderer.domElement.height) },
461
+ uParticleScale: { value: this.particleScale },
462
+ uPixelRatio: { value: this.parent.pixelRatio },
463
+ uColorScheme: { value: this.colorScheme }
464
+ },
465
+ vertexShader: f + `
466
+ ` + C,
467
+ fragmentShader: f + `
468
+ ` + R,
469
+ transparent: !0,
470
+ depthTest: !1,
471
+ depthWrite: !1
472
+ }), this.mesh = new s.Points(a, this.renderMaterial), this.parent.threeScene.add(this.mesh), this.resize();
473
+ }
474
+ createDataTextureColor(e) {
475
+ const t = e instanceof Float32Array, i = t ? e : new Float32Array(this.length * 4);
476
+ t || i.set(e);
477
+ const n = new s.DataTexture(i, this.size, this.size, s.RGBAFormat, s.FloatType);
478
+ return n.needsUpdate = !0, n;
479
+ }
480
+ createDataTexturePosition(e) {
481
+ const t = new Float32Array(this.length * 4), i = e.length / 2, n = 1 / (this.parent.width / 2), c = 1 / (this.parent.height / 2);
482
+ for (let l = 0; l < i; l++) {
483
+ let r = l * 4;
484
+ t[r + 0] = e[l * 2 + 0] * n, t[r + 1] = e[l * 2 + 1] * c, t[r + 2] = 0, t[r + 3] = 0;
485
+ }
486
+ const a = new s.DataTexture(t, this.size, this.size, s.RGBAFormat, s.FloatType);
487
+ return a.needsUpdate = !0, a;
488
+ }
489
+ createRenderTarget() {
490
+ return new s.WebGLRenderTarget(this.size, this.size, {
491
+ wrapS: s.RepeatWrapping,
492
+ wrapT: s.RepeatWrapping,
493
+ minFilter: s.NearestFilter,
494
+ magFilter: s.NearestFilter,
495
+ format: s.RGBAFormat,
496
+ type: s.HalfFloatType,
497
+ depthBuffer: !1,
498
+ stencilBuffer: !1
499
+ });
500
+ }
501
+ async processImage(e) {
502
+ return new Promise((t) => {
503
+ this.worker.onmessage = (i) => {
504
+ const n = this.createDataTexturePosition(i.data.nearestPoints), c = this.createDataTextureColor(i.data.nearestColors);
505
+ t({ posTex: n, colorTex: c });
506
+ }, this.worker.postMessage({
507
+ imageData: {
508
+ data: e.data,
509
+ width: e.width,
510
+ height: e.height
511
+ },
512
+ pointsBase: this.pointsBaseData,
513
+ density: this.parent.density,
514
+ width: this.parent.width,
515
+ height: this.parent.height
516
+ }, [e.data.buffer]);
517
+ });
518
+ }
519
+ update() {
520
+ const e = this.parent.clock.getElapsedTime(), t = e - this.lastTime;
521
+ this.lastTime = e, this.simMaterial.uniforms.uPosition.value = this.everRendered ? this.rt1.texture : this.posTex, this.simMaterial.uniforms.uTime.value = e, this.simMaterial.uniforms.uDeltaTime.value = t, this.simMaterial.uniforms.uProgress.value = this.parent.progress, this.renderer.setRenderTarget(this.rt2), this.renderer.render(this.simScene, this.simCamera), this.renderer.setRenderTarget(null), this.renderMaterial.uniforms.uPosition.value = this.rt2.texture, this.renderMaterial.uniforms.uTime.value = e, this.renderMaterial.uniforms.uProgress.value = this.parent.progress;
522
+ let i = this.rt1;
523
+ this.rt1 = this.rt2, this.rt2 = i, this.everRendered = !0;
524
+ }
525
+ resize() {
526
+ this.renderMaterial.uniforms.uRez.value.set(this.renderer.domElement.width, this.renderer.domElement.height), this.renderMaterial.uniforms.uPixelRatio.value = this.parent.pixelRatio;
527
+ const e = this.camera.fov, t = this.camera.position.z, i = 2 * Math.tan(e * Math.PI / 180 / 2) * t, n = i * (this.renderer.domElement.width / this.renderer.domElement.height);
528
+ this.mesh.scale.set(n / 2, -i / 2, 1);
529
+ }
530
+ destroy() {
531
+ this.worker.terminate(), this.mesh.geometry.dispose(), this.mesh.material.dispose(), this.rt1.dispose(), this.rt2.dispose(), this.posTex.dispose(), this.posNearestTex.dispose(), this.colorNearestTex.dispose(), this.simMaterial.dispose(), this.renderMaterial.dispose();
532
+ }
533
+ }
534
+ class B {
535
+ constructor(e, t = {}) {
536
+ this.canvas = e, this.container = e.parentElement, this.options = t, this.width = e.clientWidth || 500, this.height = e.clientHeight || 500, this.theme = t.theme || "dark", this.particlesScale = t.particlesScale || 0.5, this.density = t.density || 150, this.cameraZoom = t.cameraZoom || 3.5, this.color = t.color || (this.theme === "dark" ? "#aecbfa" : "#121212"), this.pixelRatio = window.devicePixelRatio, this.duration = t.duration || 0.6, this.progress = 0, this.clock = new s.Clock(), this.lru = new M(10, (i) => {
537
+ i.posTex && i.posTex.dispose(), i.colorTex && i.colorTex.dispose();
538
+ }), this.initThree(), this.manager = new b(this), this.animate = this.animate.bind(this), requestAnimationFrame(this.animate);
539
+ }
540
+ initThree() {
541
+ this.threeScene = new s.Scene(), this.threeScene.background = new s.Color(this.theme === "dark" ? 1184274 : 16777215), this.camera = new s.PerspectiveCamera(40, this.canvas.clientWidth / this.canvas.clientHeight, 0.1, 1e3), this.camera.position.z = this.cameraZoom, this.renderer = new s.WebGLRenderer({
542
+ canvas: this.canvas,
543
+ antialias: !0,
544
+ alpha: !0,
545
+ powerPreference: "high-performance"
546
+ }), this.renderer.setPixelRatio(this.pixelRatio), this.renderer.setSize(this.canvas.clientWidth, this.canvas.clientHeight);
547
+ }
548
+ async getImageData(e) {
549
+ return new Promise((t, i) => {
550
+ let n = new Image();
551
+ n.crossOrigin = "anonymous";
552
+ let c = e, a = !1;
553
+ if (e.startsWith("<svg")) {
554
+ const r = new Blob([e], { type: "image/svg+xml;charset=utf-8" });
555
+ c = URL.createObjectURL(r), a = !0;
556
+ }
557
+ const l = () => {
558
+ a && URL.revokeObjectURL(c), n.onload = null, n.onerror = null, n = null;
559
+ };
560
+ n.onload = () => {
561
+ const r = document.createElement("canvas");
562
+ r.width = this.width, r.height = this.height;
563
+ const h = r.getContext("2d"), o = this.width, m = this.height;
564
+ let d = n.width, u = n.height;
565
+ const v = Math.min(o / n.width, m / n.height);
566
+ v < 1 && (d = n.width * v, u = n.height * v);
567
+ const w = (o - d) / 2, z = (m - u) / 2;
568
+ h.drawImage(n, w, z, d, u);
569
+ const P = h.getImageData(0, 0, this.width, this.height);
570
+ r.width = 0, r.height = 0, t(P), l();
571
+ }, n.onerror = (r) => {
572
+ i(r), l();
573
+ }, n.src = c;
574
+ });
575
+ }
576
+ async render(e) {
577
+ if (!e) return;
578
+ const t = this.lru.get(e);
579
+ let i, n;
580
+ if (t)
581
+ i = t.posTex, n = t.colorTex;
582
+ else {
583
+ const c = await this.getImageData(e), a = await this.manager.processImage(c);
584
+ i = a.posTex, n = a.colorTex, this.lru.set(e, { posTex: i, colorTex: n });
585
+ }
586
+ this.manager.posNearestTex = i, this.manager.colorNearestTex = n, this.manager.simMaterial.uniforms.uPosNearest.value = i, this.manager.renderMaterial.uniforms.uColorTex.value = n, x.to(this, { progress: 1, duration: this.duration, ease: "power3.inOut" });
587
+ }
588
+ scatter() {
589
+ x.to(this, { progress: 0, duration: this.duration, ease: "power3.inOut" });
590
+ }
591
+ animate() {
592
+ this.renderer && (this.manager.update(), this.renderer.render(this.threeScene, this.camera), requestAnimationFrame(this.animate));
593
+ }
594
+ destroy() {
595
+ this.lru.clear(), this.manager.destroy(), this.renderer.dispose(), this.renderer = null;
596
+ }
597
+ }
598
+ export {
599
+ B as ParticleImage
600
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "particle-image",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Particle image effect using three.js",
6
+ "keywords": [
7
+ "threejs",
8
+ "particles",
9
+ "morphing",
10
+ "3d",
11
+ "webgl",
12
+ "animation",
13
+ "gsap",
14
+ "particle-effect",
15
+ "image-to-particles",
16
+ "canvas",
17
+ "glsl",
18
+ "shaders",
19
+ "gpgpu",
20
+ "vfx"
21
+ ],
22
+ "author": "shjyh",
23
+ "license": "MIT",
24
+ "main": "dist/particle-image.es.js",
25
+ "module": "dist/particle-image.es.js",
26
+ "types": "dist/particle-image.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "import": "./dist/particle-image.es.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "scripts": {
36
+ "dev": "vite",
37
+ "build": "vite build",
38
+ "preview": "vite preview",
39
+ "prepublishOnly": "echo 'Ready to publish'"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/shjyh/ParticleImage.git"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/shjyh/ParticleImage/issues"
47
+ },
48
+ "homepage": "https://github.com/shjyh/ParticleImage#readme",
49
+ "dependencies": {
50
+ "gsap": "^3.14.2",
51
+ "poisson-disk-sampling": "^2.3.1",
52
+ "three": "^0.182.0"
53
+ },
54
+ "devDependencies": {
55
+ "vite": "^7.3.1"
56
+ }
57
+ }