ghostdust 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/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/index.js +86 -0
- package/dist/react.js +120 -0
- package/dist/sampler-p8JbOhAo.js +234 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dalzein
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# ghostdust
|
|
2
|
+
|
|
3
|
+
Turn text, SVGs, and images into interactive anti-gravity particles. Sweep your cursor through them to scatter the dust; springs pull every particle back home.
|
|
4
|
+
|
|
5
|
+
Framework-agnostic WebGL core with a React wrapper. No dependencies.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install ghostdust
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Vanilla
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import ghostdust from 'ghostdust';
|
|
17
|
+
|
|
18
|
+
// text, in any loaded font
|
|
19
|
+
const gd = await ghostdust.fromText('#logo', 'ghostdust', {
|
|
20
|
+
fontFamily: 'Google Sans',
|
|
21
|
+
colors: ['#e8e6f0', '#a78bfa'], // one color = solid, more = gradient
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// an image: File, Blob, <img> element, or URL
|
|
25
|
+
await ghostdust.fromImage(canvasEl, '/hero.png', {
|
|
26
|
+
removeBackground: false, // keep opaque backgrounds as particles
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// raw SVG markup
|
|
30
|
+
await ghostdust.fromSvg(canvasEl, '<svg>...</svg>', { intro: 'scatter' });
|
|
31
|
+
|
|
32
|
+
gd.count; // number of particles
|
|
33
|
+
gd.engine; // the ParticleEngine, for live param tweaks
|
|
34
|
+
gd.destroy(); // stop and release everything
|
|
35
|
+
|
|
36
|
+
// swap content on a live instance without rebuilding it
|
|
37
|
+
await gd.setText('boo', { colors: ['#22d3ee'] });
|
|
38
|
+
await gd.setImage(file, { removeBackground: false });
|
|
39
|
+
await gd.setSvg('<svg>...</svg>');
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Each `from*` call takes a `<canvas>` element (or selector) plus options:
|
|
43
|
+
|
|
44
|
+
| Option | Default | |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| `targetParticles` | `14000` | approximate particle count |
|
|
47
|
+
| `margin` | `0.72` | fraction of the canvas the art may occupy |
|
|
48
|
+
| `intro` | `'scatter'` (`'bloom'` for text) | entrance: fly in from outside, or condense from a local haze (best for small canvases) |
|
|
49
|
+
| `physics` | engine defaults | `{ radius, force, stiffness, damping, speedBoost }` |
|
|
50
|
+
| `pointer` | `true` | wire up cursor/touch tracking |
|
|
51
|
+
| `removeBackground` | `true` | images only: drop pixels matching the estimated background color of opaque images; `false` keeps every visible pixel |
|
|
52
|
+
| `fontFamily`, `weight`, `colors` | `system-ui`, `600`, white | text only |
|
|
53
|
+
|
|
54
|
+
## React
|
|
55
|
+
|
|
56
|
+
```jsx
|
|
57
|
+
import { ParticleText, ParticleImage } from 'ghostdust/react';
|
|
58
|
+
|
|
59
|
+
<ParticleText
|
|
60
|
+
text="ghostdust"
|
|
61
|
+
fontFamily="Google Sans"
|
|
62
|
+
colors={['#e8e6f0', '#a78bfa']}
|
|
63
|
+
physics={{ radius: 60, force: 900 }}
|
|
64
|
+
className="wordmark" // size it with CSS; any size works
|
|
65
|
+
/>
|
|
66
|
+
|
|
67
|
+
<ParticleImage src="/hero.png" removeBackground={false} label="Hero artwork" />
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Props mirror the vanilla options. Components resize with their element (not just the window), and are accessible by default: pass `label` for an image description, or `label={null}` when a nearby heading already names the content.
|
|
71
|
+
|
|
72
|
+
Components also take `bleed` (default `0.75`): the transparent canvas overhangs the layout box by that fraction on every side, so particles pushed around by the cursor aren't clipped at the element's edges. It never affects layout or clicks — set `bleed={0}` for a tightly bounded canvas.
|
|
73
|
+
|
|
74
|
+
## Local development
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
npm run dev # demo site
|
|
78
|
+
npm run build:lib # build the package to dist/
|
|
79
|
+
npm run build # build the demo site to dist-site/
|
|
80
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { i as e, n as t, r as n, t as r } from "./sampler-p8JbOhAo.js";
|
|
2
|
+
//#region src/lib/core/ghostdust.js
|
|
3
|
+
function i(e) {
|
|
4
|
+
let t = typeof e == "string" ? document.querySelector(e) : e;
|
|
5
|
+
if (!t || typeof t.getContext != "function") throw Error("ghostdust: target must be a <canvas> element or selector");
|
|
6
|
+
return t;
|
|
7
|
+
}
|
|
8
|
+
function a(i, a) {
|
|
9
|
+
let { margin: o, intro: s, physics: c, pointer: l = !0 } = a, u = new e(i, {
|
|
10
|
+
margin: o,
|
|
11
|
+
intro: s
|
|
12
|
+
});
|
|
13
|
+
c && Object.assign(u.params, c), u.start();
|
|
14
|
+
let d = null;
|
|
15
|
+
if (l) {
|
|
16
|
+
let e = (e, t) => {
|
|
17
|
+
let n = i.getBoundingClientRect();
|
|
18
|
+
u.setPointer(e - n.left, t - n.top, !0);
|
|
19
|
+
}, t = (t) => e(t.clientX, t.clientY), n = (t) => {
|
|
20
|
+
let n = t.touches[0];
|
|
21
|
+
n && e(n.clientX, n.clientY);
|
|
22
|
+
}, r = () => u.setPointer(0, 0, !1);
|
|
23
|
+
window.addEventListener("pointermove", t), window.addEventListener("blur", r), window.addEventListener("touchmove", n, { passive: !0 }), window.addEventListener("touchend", r), d = () => {
|
|
24
|
+
window.removeEventListener("pointermove", t), window.removeEventListener("blur", r), window.removeEventListener("touchmove", n), window.removeEventListener("touchend", r);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
engine: u,
|
|
29
|
+
canvas: i,
|
|
30
|
+
get count() {
|
|
31
|
+
return u.count;
|
|
32
|
+
},
|
|
33
|
+
async setText(e, t = {}) {
|
|
34
|
+
u.setParticles(await n(e, t));
|
|
35
|
+
},
|
|
36
|
+
async setImage(e, t = {}) {
|
|
37
|
+
u.setParticles(await r(e, t));
|
|
38
|
+
},
|
|
39
|
+
async setSvg(e, n = {}) {
|
|
40
|
+
u.setParticles(await t(e, n));
|
|
41
|
+
},
|
|
42
|
+
destroy() {
|
|
43
|
+
d?.(), u.destroy();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function o(e, t, n) {
|
|
48
|
+
let r = a(i(e), t);
|
|
49
|
+
try {
|
|
50
|
+
return await n(r), r;
|
|
51
|
+
} catch (e) {
|
|
52
|
+
throw r.destroy(), e;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function s(e, t, n = {}) {
|
|
56
|
+
let { fontFamily: r, weight: i, colors: a, targetParticles: s, ...c } = n;
|
|
57
|
+
return o(e, {
|
|
58
|
+
intro: "bloom",
|
|
59
|
+
...c
|
|
60
|
+
}, (e) => e.setText(t, {
|
|
61
|
+
fontFamily: r,
|
|
62
|
+
weight: i,
|
|
63
|
+
colors: a,
|
|
64
|
+
targetParticles: s
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
function c(e, t, n = {}) {
|
|
68
|
+
let { targetParticles: r, removeBackground: i, ...a } = n;
|
|
69
|
+
return o(e, a, (e) => e.setImage(t, {
|
|
70
|
+
targetParticles: r,
|
|
71
|
+
removeBackground: i
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
function l(e, t, n = {}) {
|
|
75
|
+
let { targetParticles: r, ...i } = n;
|
|
76
|
+
return o(e, i, (e) => e.setSvg(t, { targetParticles: r }));
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/lib/index.js
|
|
80
|
+
var u = {
|
|
81
|
+
fromText: s,
|
|
82
|
+
fromImage: c,
|
|
83
|
+
fromSvg: l
|
|
84
|
+
};
|
|
85
|
+
//#endregion
|
|
86
|
+
export { e as ParticleEngine, u as default, c as fromImage, l as fromSvg, s as fromText, r as sampleImage, t as sampleSvg, n as sampleText };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { i as e, r as t, t as n } from "./sampler-p8JbOhAo.js";
|
|
2
|
+
import { useEffect as r, useRef as i } from "react";
|
|
3
|
+
import { jsx as a } from "react/jsx-runtime";
|
|
4
|
+
//#region src/lib/react.jsx
|
|
5
|
+
function o(t, n, { margin: a, intro: o, physicsKey: s }) {
|
|
6
|
+
let c = i(null);
|
|
7
|
+
return r(() => {
|
|
8
|
+
let n = c.current, r;
|
|
9
|
+
try {
|
|
10
|
+
r = new e(n, {
|
|
11
|
+
margin: a,
|
|
12
|
+
intro: o
|
|
13
|
+
});
|
|
14
|
+
} catch {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
s && Object.assign(r.params, JSON.parse(s)), r.start();
|
|
18
|
+
let i = !1;
|
|
19
|
+
t().then((e) => {
|
|
20
|
+
i || r.setParticles(e);
|
|
21
|
+
}).catch(() => {});
|
|
22
|
+
let l = (e) => {
|
|
23
|
+
let t = n.getBoundingClientRect();
|
|
24
|
+
r.setPointer(e.clientX - t.left, e.clientY - t.top, !0);
|
|
25
|
+
}, u = (e) => {
|
|
26
|
+
let t = e.touches[0];
|
|
27
|
+
if (t) {
|
|
28
|
+
let e = n.getBoundingClientRect();
|
|
29
|
+
r.setPointer(t.clientX - e.left, t.clientY - e.top, !0);
|
|
30
|
+
}
|
|
31
|
+
}, d = () => r.setPointer(0, 0, !1);
|
|
32
|
+
return window.addEventListener("pointermove", l), window.addEventListener("blur", d), window.addEventListener("touchmove", u, { passive: !0 }), window.addEventListener("touchend", d), () => {
|
|
33
|
+
i = !0, window.removeEventListener("pointermove", l), window.removeEventListener("blur", d), window.removeEventListener("touchmove", u), window.removeEventListener("touchend", d), r.destroy();
|
|
34
|
+
};
|
|
35
|
+
}, n), c;
|
|
36
|
+
}
|
|
37
|
+
function s(e) {
|
|
38
|
+
return e ? {
|
|
39
|
+
role: "img",
|
|
40
|
+
"aria-label": e
|
|
41
|
+
} : { "aria-hidden": "true" };
|
|
42
|
+
}
|
|
43
|
+
function c({ canvasRef: e, bleed: t, label: n, className: r, style: i }) {
|
|
44
|
+
let o = 1 + 2 * t;
|
|
45
|
+
return /* @__PURE__ */ a("span", {
|
|
46
|
+
className: r,
|
|
47
|
+
style: {
|
|
48
|
+
display: "block",
|
|
49
|
+
position: "relative",
|
|
50
|
+
...i
|
|
51
|
+
},
|
|
52
|
+
...s(n),
|
|
53
|
+
children: /* @__PURE__ */ a("canvas", {
|
|
54
|
+
ref: e,
|
|
55
|
+
"aria-hidden": "true",
|
|
56
|
+
style: {
|
|
57
|
+
position: "absolute",
|
|
58
|
+
left: `${-t * 100}%`,
|
|
59
|
+
top: `${-t * 100}%`,
|
|
60
|
+
width: `${o * 100}%`,
|
|
61
|
+
height: `${o * 100}%`,
|
|
62
|
+
pointerEvents: "none"
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function l({ text: e, colors: n = ["#ffffff"], fontFamily: r = "system-ui", weight: i = 600, targetParticles: s = 1600, margin: l = .9, bleed: u = .75, intro: d = "bloom", physics: f, label: p = e, className: m, style: h }) {
|
|
68
|
+
let g = n.join(","), _ = f ? JSON.stringify(f) : "", v = l / (1 + 2 * u), y = o(() => t(e, {
|
|
69
|
+
colors: g.split(","),
|
|
70
|
+
fontFamily: r,
|
|
71
|
+
weight: i,
|
|
72
|
+
targetParticles: s
|
|
73
|
+
}), [
|
|
74
|
+
e,
|
|
75
|
+
g,
|
|
76
|
+
r,
|
|
77
|
+
i,
|
|
78
|
+
s,
|
|
79
|
+
v,
|
|
80
|
+
d,
|
|
81
|
+
_
|
|
82
|
+
], {
|
|
83
|
+
margin: v,
|
|
84
|
+
intro: d,
|
|
85
|
+
physicsKey: _
|
|
86
|
+
});
|
|
87
|
+
return /* @__PURE__ */ a(c, {
|
|
88
|
+
canvasRef: y,
|
|
89
|
+
bleed: u,
|
|
90
|
+
label: p,
|
|
91
|
+
className: m,
|
|
92
|
+
style: h
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function u({ src: e, targetParticles: t = 4e3, removeBackground: r = !0, margin: i = .9, bleed: s = .75, intro: l = "bloom", physics: u, label: d = null, className: f, style: p }) {
|
|
96
|
+
let m = u ? JSON.stringify(u) : "", h = i / (1 + 2 * s), g = o(() => n(e, {
|
|
97
|
+
targetParticles: t,
|
|
98
|
+
removeBackground: r
|
|
99
|
+
}), [
|
|
100
|
+
e,
|
|
101
|
+
t,
|
|
102
|
+
r,
|
|
103
|
+
h,
|
|
104
|
+
l,
|
|
105
|
+
m
|
|
106
|
+
], {
|
|
107
|
+
margin: h,
|
|
108
|
+
intro: l,
|
|
109
|
+
physicsKey: m
|
|
110
|
+
});
|
|
111
|
+
return /* @__PURE__ */ a(c, {
|
|
112
|
+
canvasRef: g,
|
|
113
|
+
bleed: s,
|
|
114
|
+
label: d,
|
|
115
|
+
className: f,
|
|
116
|
+
style: p
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { u as ParticleImage, l as ParticleText };
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
function e(e, t, n) {
|
|
2
|
+
let r = e.createShader(t);
|
|
3
|
+
if (e.shaderSource(r, n), e.compileShader(r), !e.getShaderParameter(r, e.COMPILE_STATUS)) throw Error(e.getShaderInfoLog(r));
|
|
4
|
+
return r;
|
|
5
|
+
}
|
|
6
|
+
var t = class {
|
|
7
|
+
constructor(t, { margin: n = .72, intro: r = "scatter" } = {}) {
|
|
8
|
+
this.canvas = t, this.margin = n, this.intro = r;
|
|
9
|
+
let i = t.getContext("webgl", {
|
|
10
|
+
antialias: !1,
|
|
11
|
+
alpha: !0,
|
|
12
|
+
premultipliedAlpha: !1
|
|
13
|
+
});
|
|
14
|
+
if (!i) throw Error("WebGL not supported");
|
|
15
|
+
this.gl = i;
|
|
16
|
+
let a = i.createProgram();
|
|
17
|
+
if (i.attachShader(a, e(i, i.VERTEX_SHADER, "\nattribute vec2 a_pos;\nattribute vec4 a_color;\nattribute float a_size;\nattribute float a_speed;\n\nuniform vec2 u_resolution;\nuniform float u_dpr;\n\nvarying vec4 v_color;\nvarying float v_speed;\n\nvoid main() {\n // pixel coords -> clip space\n vec2 clip = (a_pos / u_resolution) * 2.0 - 1.0;\n gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);\n gl_PointSize = a_size * u_dpr * (1.0 + min(a_speed * 0.0016, 0.9));\n v_color = a_color;\n v_speed = a_speed;\n}\n")), i.attachShader(a, e(i, i.FRAGMENT_SHADER, "\nprecision mediump float;\nvarying vec4 v_color;\nvarying float v_speed;\n\nvoid main() {\n vec2 uv = gl_PointCoord * 2.0 - 1.0;\n float d = dot(uv, uv);\n if (d > 1.0) discard;\n float alpha = smoothstep(1.0, 0.55, d);\n // particles glow slightly hotter while moving fast\n float heat = min(v_speed * 0.0012, 1.0);\n vec3 col = mix(v_color.rgb, vec3(1.0), heat * 0.55);\n gl_FragColor = vec4(col, alpha * v_color.a);\n}\n")), i.linkProgram(a), !i.getProgramParameter(a, i.LINK_STATUS)) throw Error(i.getProgramInfoLog(a));
|
|
18
|
+
this.program = a, this.loc = {
|
|
19
|
+
pos: i.getAttribLocation(a, "a_pos"),
|
|
20
|
+
color: i.getAttribLocation(a, "a_color"),
|
|
21
|
+
size: i.getAttribLocation(a, "a_size"),
|
|
22
|
+
speed: i.getAttribLocation(a, "a_speed"),
|
|
23
|
+
resolution: i.getUniformLocation(a, "u_resolution"),
|
|
24
|
+
dpr: i.getUniformLocation(a, "u_dpr")
|
|
25
|
+
}, this.posBuf = i.createBuffer(), this.colorBuf = i.createBuffer(), this.sizeBuf = i.createBuffer(), this.speedBuf = i.createBuffer(), i.enable(i.BLEND), i.blendFunc(i.SRC_ALPHA, i.ONE_MINUS_SRC_ALPHA), this.count = 0, this.pos = null, this.vel = null, this.home = null, this.norm = null, this.speed = null, this.params = {
|
|
26
|
+
stiffness: 42,
|
|
27
|
+
damping: 5.2,
|
|
28
|
+
radius: 130,
|
|
29
|
+
force: 2600,
|
|
30
|
+
speedBoost: 14
|
|
31
|
+
}, this.pointer = {
|
|
32
|
+
x: -1e6,
|
|
33
|
+
y: -1e6,
|
|
34
|
+
vx: 0,
|
|
35
|
+
vy: 0,
|
|
36
|
+
speed: 0,
|
|
37
|
+
active: !1
|
|
38
|
+
}, this._lastPointer = null, this.dpr = 1, this._raf = 0, this._lastT = 0, this._running = !1, this._onResize = () => this.resize(), window.addEventListener("resize", this._onResize), this._ro = typeof ResizeObserver < "u" ? new ResizeObserver(this._onResize) : null, this._ro?.observe(t), this.resize();
|
|
39
|
+
}
|
|
40
|
+
resize() {
|
|
41
|
+
let { canvas: e } = this;
|
|
42
|
+
this.dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
43
|
+
let t = e.clientWidth, n = e.clientHeight;
|
|
44
|
+
!t || !n || (e.width = Math.round(t * this.dpr), e.height = Math.round(n * this.dpr), this.gl.viewport(0, 0, e.width, e.height), this.layoutHomes());
|
|
45
|
+
}
|
|
46
|
+
layoutHomes() {
|
|
47
|
+
if (!this.norm) return;
|
|
48
|
+
let e = this.canvas.clientWidth, t = this.canvas.clientHeight, n = Infinity, r = -Infinity, i = Infinity, a = -Infinity;
|
|
49
|
+
for (let e = 0; e < this.count; e++) {
|
|
50
|
+
let t = this.norm[e * 2], o = this.norm[e * 2 + 1];
|
|
51
|
+
t < n && (n = t), t > r && (r = t), o < i && (i = o), o > a && (a = o);
|
|
52
|
+
}
|
|
53
|
+
let o = Math.max(r - n, 1e-6), s = Math.max(a - i, 1e-6), c = Math.min(e / o, t / s) * this.margin, l = e / 2, u = t / 2;
|
|
54
|
+
for (let e = 0; e < this.count; e++) this.home[e * 2] = l + this.norm[e * 2] * c, this.home[e * 2 + 1] = u + this.norm[e * 2 + 1] * c;
|
|
55
|
+
}
|
|
56
|
+
setParticles(e) {
|
|
57
|
+
let t = e.norm.length / 2;
|
|
58
|
+
this.count = t, this.norm = e.norm, this.pos = new Float32Array(t * 2), this.vel = new Float32Array(t * 2), this.home = new Float32Array(t * 2), this.speed = new Float32Array(t), this.layoutHomes();
|
|
59
|
+
let n = this.canvas.clientWidth, r = this.canvas.clientHeight;
|
|
60
|
+
for (let e = 0; e < t; e++) {
|
|
61
|
+
if (this.intro === "bloom") {
|
|
62
|
+
let i = e / t * Math.PI * 2 + e % 7 * .9, a = Math.min(n, r) * .5 * (e * 2654435761 % 1e3 / 1e3);
|
|
63
|
+
this.pos[e * 2] = this.home[e * 2] + Math.cos(i) * a, this.pos[e * 2 + 1] = this.home[e * 2 + 1] + Math.sin(i) * a;
|
|
64
|
+
} else {
|
|
65
|
+
let i = e / t * Math.PI * 2 + e % 7 * .9, a = Math.max(n, r) * (.6 + e * 2654435761 % 1e3 / 2500);
|
|
66
|
+
this.pos[e * 2] = n / 2 + Math.cos(i) * a, this.pos[e * 2 + 1] = r / 2 + Math.sin(i) * a;
|
|
67
|
+
}
|
|
68
|
+
this.vel[e * 2] = 0, this.vel[e * 2 + 1] = 0;
|
|
69
|
+
}
|
|
70
|
+
let i = this.gl, a = new Float32Array(t);
|
|
71
|
+
for (let e = 0; e < t; e++) a[e] = 1.6 + e * 40503 % 1e3 / 1e3 * 1.3;
|
|
72
|
+
i.bindBuffer(i.ARRAY_BUFFER, this.colorBuf), i.bufferData(i.ARRAY_BUFFER, e.colors, i.STATIC_DRAW), i.bindBuffer(i.ARRAY_BUFFER, this.sizeBuf), i.bufferData(i.ARRAY_BUFFER, a, i.STATIC_DRAW), i.bindBuffer(i.ARRAY_BUFFER, this.posBuf), i.bufferData(i.ARRAY_BUFFER, this.pos, i.DYNAMIC_DRAW), i.bindBuffer(i.ARRAY_BUFFER, this.speedBuf), i.bufferData(i.ARRAY_BUFFER, this.speed, i.DYNAMIC_DRAW);
|
|
73
|
+
}
|
|
74
|
+
setPointer(e, t, n) {
|
|
75
|
+
let r = performance.now(), i = this.pointer;
|
|
76
|
+
if (this._lastPointer && n) {
|
|
77
|
+
let n = Math.max((r - this._lastPointer.t) / 1e3, 1e-4), a = (e - this._lastPointer.x) / n, o = (t - this._lastPointer.y) / n;
|
|
78
|
+
i.vx = i.vx * .7 + a * .3, i.vy = i.vy * .7 + o * .3;
|
|
79
|
+
}
|
|
80
|
+
i.x = e, i.y = t, i.active = n, i.speed = Math.hypot(i.vx, i.vy), this._lastPointer = n ? {
|
|
81
|
+
x: e,
|
|
82
|
+
y: t,
|
|
83
|
+
t: r
|
|
84
|
+
} : null, n || (i.vx = 0, i.vy = 0, i.speed = 0, i.x = -1e6, i.y = -1e6);
|
|
85
|
+
}
|
|
86
|
+
step(e) {
|
|
87
|
+
let { pos: t, vel: n, home: r, speed: i, count: a, params: o, pointer: s } = this;
|
|
88
|
+
if (!t) return;
|
|
89
|
+
let c = o.stiffness, l = Math.exp(-o.damping * e), u = o.radius, d = u * u, f = s.x, p = s.y, m = o.force + s.speed * o.speedBoost;
|
|
90
|
+
for (let o = 0; o < a; o++) {
|
|
91
|
+
let a = o * 2, s = a + 1, h = t[a], g = t[s], _ = n[a], v = n[s];
|
|
92
|
+
_ += (r[a] - h) * c * e, v += (r[s] - g) * c * e;
|
|
93
|
+
let y = h - f, b = g - p, x = y * y + b * b;
|
|
94
|
+
if (x < d) {
|
|
95
|
+
let t = Math.sqrt(x) || 1e-4, n = 1 - t / u, r = m * n * n / t;
|
|
96
|
+
_ += y * r * e, v += b * r * e;
|
|
97
|
+
}
|
|
98
|
+
_ *= l, v *= l, h += _ * e, g += v * e, t[a] = h, t[s] = g, n[a] = _, n[s] = v, i[o] = Math.hypot(_, v);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
render() {
|
|
102
|
+
let e = this.gl;
|
|
103
|
+
e.clearColor(0, 0, 0, 0), e.clear(e.COLOR_BUFFER_BIT), this.count && (e.useProgram(this.program), e.uniform2f(this.loc.resolution, this.canvas.clientWidth, this.canvas.clientHeight), e.uniform1f(this.loc.dpr, this.dpr), e.bindBuffer(e.ARRAY_BUFFER, this.posBuf), e.bufferSubData(e.ARRAY_BUFFER, 0, this.pos), e.enableVertexAttribArray(this.loc.pos), e.vertexAttribPointer(this.loc.pos, 2, e.FLOAT, !1, 0, 0), e.bindBuffer(e.ARRAY_BUFFER, this.speedBuf), e.bufferSubData(e.ARRAY_BUFFER, 0, this.speed), e.enableVertexAttribArray(this.loc.speed), e.vertexAttribPointer(this.loc.speed, 1, e.FLOAT, !1, 0, 0), e.bindBuffer(e.ARRAY_BUFFER, this.colorBuf), e.enableVertexAttribArray(this.loc.color), e.vertexAttribPointer(this.loc.color, 4, e.UNSIGNED_BYTE, !0, 0, 0), e.bindBuffer(e.ARRAY_BUFFER, this.sizeBuf), e.enableVertexAttribArray(this.loc.size), e.vertexAttribPointer(this.loc.size, 1, e.FLOAT, !1, 0, 0), e.drawArrays(e.POINTS, 0, this.count));
|
|
104
|
+
}
|
|
105
|
+
start() {
|
|
106
|
+
if (this._running) return;
|
|
107
|
+
this._running = !0, this._lastT = performance.now();
|
|
108
|
+
let e = (t) => {
|
|
109
|
+
if (!this._running) return;
|
|
110
|
+
let n = Math.min((t - this._lastT) / 1e3, 1 / 30);
|
|
111
|
+
this._lastT = t, this.step(n), this.render(), this._raf = requestAnimationFrame(e);
|
|
112
|
+
};
|
|
113
|
+
this._raf = requestAnimationFrame(e);
|
|
114
|
+
}
|
|
115
|
+
destroy() {
|
|
116
|
+
this._running = !1, cancelAnimationFrame(this._raf), window.removeEventListener("resize", this._onResize), this._ro?.disconnect();
|
|
117
|
+
let e = this.gl;
|
|
118
|
+
e.deleteBuffer(this.posBuf), e.deleteBuffer(this.colorBuf), e.deleteBuffer(this.sizeBuf), e.deleteBuffer(this.speedBuf), e.deleteProgram(this.program);
|
|
119
|
+
}
|
|
120
|
+
}, n = 1e3, r = 14e3, i = 40;
|
|
121
|
+
function a(e) {
|
|
122
|
+
let t = new DOMParser().parseFromString(e, "image/svg+xml"), r = t.querySelector("svg");
|
|
123
|
+
if (!r || t.querySelector("parsererror")) throw Error("Not a valid SVG file");
|
|
124
|
+
let i = parseFloat(r.getAttribute("width")), a = parseFloat(r.getAttribute("height")), o = r.getAttribute("viewBox");
|
|
125
|
+
if ((!i || !a) && o) {
|
|
126
|
+
let e = o.trim().split(/[\s,]+/).map(Number);
|
|
127
|
+
i = e[2], a = e[3];
|
|
128
|
+
}
|
|
129
|
+
(!i || !a) && (i = 512, a = 512), o || r.setAttribute("viewBox", `0 0 ${i} ${a}`);
|
|
130
|
+
let s = n / Math.max(i, a);
|
|
131
|
+
return r.setAttribute("width", Math.round(i * s)), r.setAttribute("height", Math.round(a * s)), new XMLSerializer().serializeToString(r);
|
|
132
|
+
}
|
|
133
|
+
function o(e) {
|
|
134
|
+
return new Promise((t, n) => {
|
|
135
|
+
let r = URL.createObjectURL(e), i = new Image();
|
|
136
|
+
i.onload = () => {
|
|
137
|
+
URL.revokeObjectURL(r), t(i);
|
|
138
|
+
}, i.onerror = () => {
|
|
139
|
+
URL.revokeObjectURL(r), n(/* @__PURE__ */ Error("Could not decode image"));
|
|
140
|
+
}, i.src = r;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function s(e, t, n) {
|
|
144
|
+
let r = [
|
|
145
|
+
[0, 0],
|
|
146
|
+
[t - 1, 0],
|
|
147
|
+
[0, n - 1],
|
|
148
|
+
[t - 1, n - 1],
|
|
149
|
+
[t >> 1, 0],
|
|
150
|
+
[t >> 1, n - 1],
|
|
151
|
+
[0, n >> 1],
|
|
152
|
+
[t - 1, n >> 1]
|
|
153
|
+
], i = 0, a = 0, o = 0, s = 0;
|
|
154
|
+
for (let [n, c] of r) {
|
|
155
|
+
let r = (c * t + n) * 4;
|
|
156
|
+
e[r + 3] > 200 && (i += e[r], a += e[r + 1], o += e[r + 2], s++);
|
|
157
|
+
}
|
|
158
|
+
return s < r.length / 2 ? null : [
|
|
159
|
+
i / s,
|
|
160
|
+
a / s,
|
|
161
|
+
o / s
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
function c(e, t, n, r) {
|
|
165
|
+
let a = r ? s(e, t, n) : null;
|
|
166
|
+
return {
|
|
167
|
+
test: (t) => {
|
|
168
|
+
if (e[t + 3] <= i) return !1;
|
|
169
|
+
if (!a) return !0;
|
|
170
|
+
let n = e[t] - a[0], r = e[t + 1] - a[1], o = e[t + 2] - a[2];
|
|
171
|
+
return n * n + r * r + o * o > 3600;
|
|
172
|
+
},
|
|
173
|
+
hasBg: !!a
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function l(e, t = {}) {
|
|
177
|
+
let { targetParticles: a = r, removeBackground: o = !0 } = t, s = Math.min(n / Math.max(e.width, e.height, 1), 4), l = Math.max(Math.round(e.width * s), 1), u = Math.max(Math.round(e.height * s), 1), d = document.createElement("canvas");
|
|
178
|
+
d.width = l, d.height = u;
|
|
179
|
+
let f = d.getContext("2d", { willReadFrequently: !0 });
|
|
180
|
+
f.drawImage(e, 0, 0, l, u);
|
|
181
|
+
let { data: p } = f.getImageData(0, 0, l, u), { test: m, hasBg: h } = c(p, l, u, o), g = 0;
|
|
182
|
+
for (let e = 0; e < p.length; e += 4) m(e) && g++;
|
|
183
|
+
if (!g && h) {
|
|
184
|
+
m = (e) => p[e + 3] > i;
|
|
185
|
+
for (let e = 0; e < p.length; e += 4) m(e) && g++;
|
|
186
|
+
}
|
|
187
|
+
if (!g) throw Error("Image appears to be empty (no visible pixels)");
|
|
188
|
+
let _ = Math.max(1, Math.round(Math.sqrt(g / a))), v = [], y = [], b = [], x = Infinity, S = -Infinity, C = Infinity, w = -Infinity;
|
|
189
|
+
for (let e = 0; e < u; e += _) for (let t = 0; t < l; t += _) {
|
|
190
|
+
let n = (e * l + t) * 4;
|
|
191
|
+
if (m(n)) {
|
|
192
|
+
let r = t + ((t * 7919 + e * 104729) % 100 / 100 - .5) * _ * .8, i = e + ((t * 15485 + e * 32452) % 100 / 100 - .5) * _ * .8;
|
|
193
|
+
v.push(r), y.push(i), b.push(p[n], p[n + 1], p[n + 2], p[n + 3]), r < x && (x = r), r > S && (S = r), i < C && (C = i), i > w && (w = i);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
let T = v.length, E = Math.max(S - x, 1), D = Math.max(w - C, 1), O = (x + S) / 2, k = (C + w) / 2, A = Math.max(E, D), j = new Float32Array(T * 2);
|
|
197
|
+
for (let e = 0; e < T; e++) j[e * 2] = (v[e] - O) / A, j[e * 2 + 1] = (y[e] - k) / A;
|
|
198
|
+
return {
|
|
199
|
+
norm: j,
|
|
200
|
+
colors: new Uint8Array(b)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async function u(e, t = {}) {
|
|
204
|
+
let n = a(e);
|
|
205
|
+
return l(await o(new Blob([n], { type: "image/svg+xml" })), t);
|
|
206
|
+
}
|
|
207
|
+
var d = 220;
|
|
208
|
+
async function f(e, t = {}) {
|
|
209
|
+
let { fontFamily: n = "system-ui", weight: i = 600, colors: a = ["#ffffff"], targetParticles: o = r } = t, s = e.trim();
|
|
210
|
+
if (!s) throw Error("Text is empty");
|
|
211
|
+
let c = `${i} ${d}px "${n}", sans-serif`;
|
|
212
|
+
try {
|
|
213
|
+
await document.fonts.load(c, s);
|
|
214
|
+
} catch {}
|
|
215
|
+
let u = document.createElement("canvas"), f = u.getContext("2d");
|
|
216
|
+
f.font = c;
|
|
217
|
+
let p = f.measureText(s), m = p.actualBoundingBoxAscent || d * .8, h = p.actualBoundingBoxDescent || d * .25, g = Math.round(d * .1);
|
|
218
|
+
if (u.width = Math.ceil(p.width) + 44, u.height = Math.ceil(m + h) + 44, f.font = c, a.length > 1) {
|
|
219
|
+
let e = f.createLinearGradient(g, 0, u.width - g, 0);
|
|
220
|
+
a.forEach((t, n) => e.addColorStop(n / (a.length - 1), t)), f.fillStyle = e;
|
|
221
|
+
} else f.fillStyle = a[0];
|
|
222
|
+
return f.fillText(s, g, g + m), l(u, { targetParticles: o });
|
|
223
|
+
}
|
|
224
|
+
async function p(e, t = {}) {
|
|
225
|
+
if (typeof e == "string") {
|
|
226
|
+
let n = new Image();
|
|
227
|
+
return n.crossOrigin = "anonymous", await new Promise((t, r) => {
|
|
228
|
+
n.onload = t, n.onerror = () => r(/* @__PURE__ */ Error("Could not load image URL")), n.src = e;
|
|
229
|
+
}), l(n, t);
|
|
230
|
+
}
|
|
231
|
+
return typeof HTMLImageElement < "u" && e instanceof HTMLImageElement ? l(e, t) : /svg/i.test(e.type) || /\.svg$/i.test(e.name || "") ? u(await e.text(), t) : l(await o(e), t);
|
|
232
|
+
}
|
|
233
|
+
//#endregion
|
|
234
|
+
export { t as i, u as n, f as r, p as t };
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ghostdust",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Turn text, SVGs, and images into interactive anti-gravity particles.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Daniel Alzein",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/dalzein/ghostdust.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/dalzein/ghostdust#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/dalzein/ghostdust/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"particles",
|
|
18
|
+
"webgl",
|
|
19
|
+
"canvas",
|
|
20
|
+
"text-effect",
|
|
21
|
+
"logo",
|
|
22
|
+
"animation",
|
|
23
|
+
"react"
|
|
24
|
+
],
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./dist/index.js",
|
|
30
|
+
"./react": "./dist/react.js"
|
|
31
|
+
},
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"scripts": {
|
|
34
|
+
"dev": "vite",
|
|
35
|
+
"build": "vite build",
|
|
36
|
+
"build:lib": "vite build --config vite.lib.config.js",
|
|
37
|
+
"prepublishOnly": "npm run build:lib",
|
|
38
|
+
"lint": "oxlint",
|
|
39
|
+
"preview": "vite preview"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"react": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/react": "^19.2.17",
|
|
51
|
+
"@types/react-dom": "^19.2.3",
|
|
52
|
+
"@vitejs/plugin-react": "^6.0.4",
|
|
53
|
+
"oxlint": "^1.75.0",
|
|
54
|
+
"react": "^19.2.8",
|
|
55
|
+
"react-dom": "^19.2.8",
|
|
56
|
+
"vite": "^8.2.0"
|
|
57
|
+
}
|
|
58
|
+
}
|