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 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
+ }