particle-overlay 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/particle_overlay.wasm +0 -0
- package/host.js +50 -0
- package/package.json +25 -0
- package/presets.json +147 -0
- package/worker.js +108 -0
|
Binary file
|
package/host.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Particle Overlay - Host Manager
|
|
3
|
+
* Manages OffscreenCanvas creation and offloads rendering to the Web Worker.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class ParticleOverlayHost {
|
|
7
|
+
constructor(canvas, presetId, params = {}) {
|
|
8
|
+
this.canvas = canvas;
|
|
9
|
+
this.presetId = presetId;
|
|
10
|
+
this.params = params;
|
|
11
|
+
this.worker = null;
|
|
12
|
+
this.init();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
init() {
|
|
16
|
+
if (!this.canvas.transferControlToOffscreen) {
|
|
17
|
+
console.warn("OffscreenCanvas is not supported in this browser. Falling back to main-thread rendering placeholder.");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Spawn the worker
|
|
22
|
+
this.worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
|
|
23
|
+
|
|
24
|
+
// Transfer the canvas to the worker
|
|
25
|
+
const offscreen = this.canvas.transferControlToOffscreen();
|
|
26
|
+
this.worker.postMessage({
|
|
27
|
+
action: 'start',
|
|
28
|
+
canvas: offscreen,
|
|
29
|
+
presetId: this.presetId,
|
|
30
|
+
params: this.params
|
|
31
|
+
}, [offscreen]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
updateParams(params) {
|
|
35
|
+
if (this.worker) {
|
|
36
|
+
this.worker.postMessage({
|
|
37
|
+
action: 'update',
|
|
38
|
+
params
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
destroy() {
|
|
44
|
+
if (this.worker) {
|
|
45
|
+
this.worker.postMessage({ action: 'stop' });
|
|
46
|
+
this.worker.terminate();
|
|
47
|
+
this.worker = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "particle-overlay",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "High-performance, off-thread particle overlay engine using WebAssembly and Web Workers",
|
|
5
|
+
"main": "host.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 0"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"host.js",
|
|
12
|
+
"worker.js",
|
|
13
|
+
"presets.json",
|
|
14
|
+
"dist/particle_overlay.wasm"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"particles",
|
|
18
|
+
"wasm",
|
|
19
|
+
"offscreen-canvas",
|
|
20
|
+
"web-worker",
|
|
21
|
+
"overlay"
|
|
22
|
+
],
|
|
23
|
+
"author": "Alien AI Team",
|
|
24
|
+
"license": "MIT"
|
|
25
|
+
}
|
package/presets.json
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "falling-hearts",
|
|
4
|
+
"name": "Falling hearts",
|
|
5
|
+
"description": "Romantic hearts drifting down the page",
|
|
6
|
+
"icon": "iconify://mdi:heart",
|
|
7
|
+
"parameters": [
|
|
8
|
+
{ "key": "density", "label": "Heart count", "type": "number", "min": 5, "max": 150, "step": 5, "default": 35 },
|
|
9
|
+
{ "key": "speed", "label": "Fall speed", "type": "number", "min": 1, "max": 10, "step": 0.5, "default": 2.5 },
|
|
10
|
+
{ "key": "size", "label": "Heart size", "type": "number", "min": 10, "max": 48, "step": 2, "default": 22 },
|
|
11
|
+
{ "key": "color", "label": "Heart color", "type": "color", "default": "#ff6b81" }
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "rain-shower",
|
|
16
|
+
"name": "Rain",
|
|
17
|
+
"description": "Falling rain with splashes along the bottom",
|
|
18
|
+
"icon": "iconify://mdi:weather-pouring",
|
|
19
|
+
"parameters": [
|
|
20
|
+
{ "key": "density", "label": "Rain density", "type": "number", "min": 10, "max": 300, "step": 10, "default": 80 },
|
|
21
|
+
{ "key": "speed", "label": "Rain speed", "type": "number", "min": 5, "max": 25, "step": 1, "default": 14 },
|
|
22
|
+
{ "key": "size", "label": "Drop size", "type": "number", "min": 1, "max": 4, "step": 0.25, "default": 2.25 },
|
|
23
|
+
{ "key": "splash", "label": "Splashes", "type": "boolean", "default": true },
|
|
24
|
+
{ "key": "color", "label": "Rain color", "type": "color", "default": "#aedbf0" }
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "snow-fall",
|
|
29
|
+
"name": "Snow",
|
|
30
|
+
"description": "Gentle snowflakes floating down",
|
|
31
|
+
"icon": "iconify://mdi:snowflake",
|
|
32
|
+
"parameters": [
|
|
33
|
+
{ "key": "density", "label": "Flake count", "type": "number", "min": 10, "max": 200, "step": 10, "default": 60 },
|
|
34
|
+
{ "key": "speed", "label": "Fall speed", "type": "number", "min": 0.5, "max": 4, "step": 0.25, "default": 1.5 },
|
|
35
|
+
{ "key": "size", "label": "Flake size", "type": "number", "min": 2, "max": 12, "step": 1, "default": 5 },
|
|
36
|
+
{ "key": "color", "label": "Snow color", "type": "color", "default": "#ffffff" }
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "floating-bubbles",
|
|
41
|
+
"name": "Bubbles",
|
|
42
|
+
"description": "Translucent bubbles rising from the bottom",
|
|
43
|
+
"icon": "iconify://mdi:circle-multiple-outline",
|
|
44
|
+
"parameters": [
|
|
45
|
+
{ "key": "density", "label": "Bubble count", "type": "number", "min": 5, "max": 80, "step": 5, "default": 20 },
|
|
46
|
+
{ "key": "speed", "label": "Rise speed", "type": "number", "min": 0.5, "max": 6, "step": 0.5, "default": 1.8 },
|
|
47
|
+
{ "key": "size", "label": "Max bubble size", "type": "number", "min": 8, "max": 50, "step": 2, "default": 24 },
|
|
48
|
+
{ "key": "color", "label": "Bubble tint", "type": "color", "default": "#8ae2ff" }
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "thunderstorm",
|
|
53
|
+
"name": "Thunderstorm",
|
|
54
|
+
"description": "Heavy rain with sudden lightning flashes",
|
|
55
|
+
"icon": "iconify://mdi:weather-lightning-rainy",
|
|
56
|
+
"parameters": [
|
|
57
|
+
{ "key": "density", "label": "Rain density", "type": "number", "min": 20, "max": 300, "step": 10, "default": 120 },
|
|
58
|
+
{ "key": "speed", "label": "Rain speed", "type": "number", "min": 8, "max": 30, "step": 1, "default": 18 },
|
|
59
|
+
{ "key": "flashFrequency", "label": "Lightning rate", "type": "number", "min": 1, "max": 10, "step": 1, "default": 4 },
|
|
60
|
+
{ "key": "color", "label": "Rain color", "type": "color", "default": "#7faec6" }
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "fireworks",
|
|
65
|
+
"name": "Fireworks",
|
|
66
|
+
"description": "Colorful bursts that explode and fade across the page",
|
|
67
|
+
"icon": "iconify://mdi:firework",
|
|
68
|
+
"parameters": [
|
|
69
|
+
{ "key": "frequency", "label": "Burst rate", "type": "number", "min": 1, "max": 8, "step": 1, "default": 3 },
|
|
70
|
+
{ "key": "particles", "label": "Particles per burst", "type": "number", "min": 20, "max": 100, "step": 10, "default": 50 },
|
|
71
|
+
{ "key": "size", "label": "Particle size", "type": "number", "min": 1.5, "max": 6, "step": 0.5, "default": 3 },
|
|
72
|
+
{ "key": "gravity", "label": "Gravity", "type": "number", "min": 0.02, "max": 0.15, "step": 0.01, "default": 0.06 }
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"id": "starry-night",
|
|
77
|
+
"name": "Starry night",
|
|
78
|
+
"description": "Twinkling stars and shooting stars crossing the sky",
|
|
79
|
+
"icon": "iconify://mdi:weather-night",
|
|
80
|
+
"parameters": [
|
|
81
|
+
{ "key": "density", "label": "Star count", "type": "number", "min": 10, "max": 200, "step": 10, "default": 60 },
|
|
82
|
+
{ "key": "speed", "label": "Twinkle speed", "type": "number", "min": 0.1, "max": 2, "step": 0.1, "default": 0.6 },
|
|
83
|
+
{ "key": "color", "label": "Star color", "type": "color", "default": "#ffffff" },
|
|
84
|
+
{ "key": "shootingStars", "label": "Shooting stars", "type": "boolean", "default": true }
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "drifting-clouds",
|
|
89
|
+
"name": "Drifting clouds",
|
|
90
|
+
"description": "Soft, fluffy clouds floating lazily across the screen",
|
|
91
|
+
"icon": "iconify://mdi:weather-cloudy",
|
|
92
|
+
"parameters": [
|
|
93
|
+
{ "key": "density", "label": "Cloud count", "type": "number", "min": 2, "max": 12, "step": 1, "default": 4 },
|
|
94
|
+
{ "key": "speed", "label": "Cloud speed", "type": "number", "min": 0.1, "max": 2, "step": 0.1, "default": 0.4 },
|
|
95
|
+
{ "key": "color", "label": "Cloud color", "type": "color", "default": "#ffffff" },
|
|
96
|
+
{ "key": "opacity", "label": "Cloud opacity", "type": "number", "min": 0.05, "max": 0.6, "step": 0.05, "default": 0.15 }
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"id": "matrix-rain",
|
|
101
|
+
"name": "Matrix rain",
|
|
102
|
+
"description": "Cascading columns of glowing digital code characters",
|
|
103
|
+
"icon": "iconify://mdi:matrix",
|
|
104
|
+
"parameters": [
|
|
105
|
+
{ "key": "density", "label": "Rain columns", "type": "number", "min": 5, "max": 60, "step": 5, "default": 25 },
|
|
106
|
+
{ "key": "speed", "label": "Drop speed", "type": "number", "min": 1, "max": 15, "step": 1, "default": 6 },
|
|
107
|
+
{ "key": "fontSize", "label": "Font size", "type": "number", "min": 8, "max": 24, "step": 1, "default": 14 },
|
|
108
|
+
{ "key": "color", "label": "Glow color", "type": "color", "default": "#00ff41" }
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"id": "sakura-petals",
|
|
113
|
+
"name": "Sakura petals",
|
|
114
|
+
"description": "Soft pink cherry blossom petals fluttering and spinning down",
|
|
115
|
+
"icon": "iconify://mdi:flower",
|
|
116
|
+
"parameters": [
|
|
117
|
+
{ "key": "density", "label": "Petal count", "type": "number", "min": 5, "max": 100, "step": 5, "default": 25 },
|
|
118
|
+
{ "key": "speed", "label": "Fall speed", "type": "number", "min": 0.5, "max": 5, "step": 0.5, "default": 1.8 },
|
|
119
|
+
{ "key": "size", "label": "Petal size", "type": "number", "min": 6, "max": 20, "step": 1, "default": 12 },
|
|
120
|
+
{ "key": "color", "label": "Petal color", "type": "color", "default": "#ffb7c5" }
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"id": "moonlight",
|
|
125
|
+
"name": "Moonlight",
|
|
126
|
+
"description": "A glowing moon with gentle moonbeams and floating dust",
|
|
127
|
+
"icon": "iconify://mdi:moon-waning-crescent",
|
|
128
|
+
"parameters": [
|
|
129
|
+
{ "key": "density", "label": "Dust count", "type": "number", "min": 5, "max": 60, "step": 5, "default": 20 },
|
|
130
|
+
{ "key": "moonColor", "label": "Moon color", "type": "color", "default": "#fffbe3" },
|
|
131
|
+
{ "key": "showMoon", "label": "Show moon", "type": "boolean", "default": true },
|
|
132
|
+
{ "key": "glowIntensity", "label": "Glow intensity", "type": "number", "min": 1, "max": 10, "step": 1, "default": 5 }
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": "autumn-leaves",
|
|
137
|
+
"name": "Autumn leaves",
|
|
138
|
+
"description": "Colorful leaves drifting down with a gentle sway",
|
|
139
|
+
"icon": "iconify://mdi:leaf",
|
|
140
|
+
"parameters": [
|
|
141
|
+
{ "key": "density", "label": "Leaf count", "type": "number", "min": 5, "max": 100, "step": 5, "default": 30 },
|
|
142
|
+
{ "key": "speed", "label": "Fall speed", "type": "number", "min": 0.5, "max": 6, "step": 0.5, "default": 2 },
|
|
143
|
+
{ "key": "size", "label": "Leaf size", "type": "number", "min": 12, "max": 40, "step": 2, "default": 24 },
|
|
144
|
+
{ "key": "color", "label": "Leaf tint", "type": "color", "default": "#e67e22" }
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
]
|
package/worker.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Particle Overlay - Web Worker
|
|
3
|
+
* Performs physics calculations and renders directly to the OffscreenCanvas.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let canvas = null;
|
|
7
|
+
let ctx = null;
|
|
8
|
+
let wasm = null;
|
|
9
|
+
let animFrameId = null;
|
|
10
|
+
let presetId = null;
|
|
11
|
+
let params = null;
|
|
12
|
+
|
|
13
|
+
self.onmessage = async function(e) {
|
|
14
|
+
const { action, canvas: transferredCanvas, presetId: nextPresetId, params: nextParams } = e.data;
|
|
15
|
+
|
|
16
|
+
if (action === 'start') {
|
|
17
|
+
canvas = transferredCanvas;
|
|
18
|
+
ctx = canvas.getContext('2d');
|
|
19
|
+
presetId = nextPresetId;
|
|
20
|
+
params = nextParams;
|
|
21
|
+
|
|
22
|
+
// Load and compile WASM
|
|
23
|
+
wasm = await initWasm();
|
|
24
|
+
if (wasm) {
|
|
25
|
+
startLoop();
|
|
26
|
+
}
|
|
27
|
+
} else if (action === 'update') {
|
|
28
|
+
params = nextParams;
|
|
29
|
+
if (wasm && presetId) {
|
|
30
|
+
wasm.overlay_init(presetId, JSON.stringify(params ?? {}), canvas.width, canvas.height);
|
|
31
|
+
}
|
|
32
|
+
} else if (action === 'stop') {
|
|
33
|
+
if (animFrameId) {
|
|
34
|
+
cancelAnimationFrame(animFrameId);
|
|
35
|
+
}
|
|
36
|
+
self.close();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
async function initWasm() {
|
|
41
|
+
try {
|
|
42
|
+
// Load the WASM binary (CDNs can serve this dynamically)
|
|
43
|
+
const response = await fetch('./dist/particle_overlay.wasm');
|
|
44
|
+
const bytes = await response.arrayBuffer();
|
|
45
|
+
const { instance } = await WebAssembly.instantiate(bytes, {});
|
|
46
|
+
return instance.exports;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error("Failed to load particle-overlay WASM in worker:", err);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function startLoop() {
|
|
54
|
+
// Initialize the engine inside WASM
|
|
55
|
+
if (!wasm.overlay_init(presetId, JSON.stringify(params ?? {}), canvas.width, canvas.height)) {
|
|
56
|
+
console.error(`WASM overlay_init failed for preset: ${presetId}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const tick = () => {
|
|
61
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
62
|
+
|
|
63
|
+
// 1. Advance physics inside WASM
|
|
64
|
+
const count = wasm.overlay_tick(1 / 60);
|
|
65
|
+
|
|
66
|
+
// 2. Read direct memory pointer
|
|
67
|
+
const stride = wasm.overlay_particle_stride();
|
|
68
|
+
const ptr = wasm.overlay_particles_ptr();
|
|
69
|
+
const mem = wasm.memory.buffer;
|
|
70
|
+
const floats = new Float32Array(mem, ptr, count * stride);
|
|
71
|
+
|
|
72
|
+
// 3. Draw particles directly from WASM memory view
|
|
73
|
+
// Note: Rendering paths for custom kinds (LINE, GLYPH, SAKURA, LEAF, etc.)
|
|
74
|
+
// are implemented in host.js, or can be drawn here.
|
|
75
|
+
for (let i = 0; i < count; i++) {
|
|
76
|
+
const o = i * stride;
|
|
77
|
+
const kind = floats[o];
|
|
78
|
+
const x = floats[o + 1];
|
|
79
|
+
const y = floats[o + 2];
|
|
80
|
+
const a = floats[o + 3];
|
|
81
|
+
const b = floats[o + 4];
|
|
82
|
+
const rot = floats[o + 5];
|
|
83
|
+
const opacity = floats[o + 6];
|
|
84
|
+
const color = colorFromU32(floats[o + 7]);
|
|
85
|
+
|
|
86
|
+
ctx.globalAlpha = opacity;
|
|
87
|
+
|
|
88
|
+
// Simple fallback drawing: draw circle
|
|
89
|
+
ctx.beginPath();
|
|
90
|
+
ctx.arc(x, y, a || 4, 0, Math.PI * 2);
|
|
91
|
+
ctx.fillStyle = color;
|
|
92
|
+
ctx.fill();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
ctx.globalAlpha = 1;
|
|
96
|
+
animFrameId = requestAnimationFrame(tick);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
tick();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function colorFromU32(n) {
|
|
103
|
+
const v = n >>> 0;
|
|
104
|
+
const r = (v >> 16) & 0xff;
|
|
105
|
+
const g = (v >> 8) & 0xff;
|
|
106
|
+
const b = v & 0xff;
|
|
107
|
+
return `rgb(${r},${g},${b})`;
|
|
108
|
+
}
|