@zakkster/lite-ambient-fx 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/AmbientFX.d.ts +179 -0
- package/AmbientFX.js +868 -0
- package/CHANGELOG.md +74 -0
- package/LICENSE.txt +21 -0
- package/README.md +436 -0
- package/llms.txt +210 -0
- package/package.json +60 -0
package/llms.txt
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# @zakkster/lite-ambient-fx
|
|
2
|
+
|
|
3
|
+
> Fullscreen ambient particle atmospheres in one file. Six themed presets
|
|
4
|
+
> across four behaviors (EMBER, MIST, FLOAT, CHAOS) plus a registry hook
|
|
5
|
+
> for adding your own. Zero dependencies. Sprite-cached radial gradients,
|
|
6
|
+
> zero-alloc render loop, monomorphic particle shape, DPR-aware,
|
|
7
|
+
> resize-preserving, visibility-paused, delta-time clamped.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npm install @zakkster/lite-ambient-fx
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Minimal use
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { createAmbientFX } from "@zakkster/lite-ambient-fx";
|
|
19
|
+
const fx = createAmbientFX(canvasElement, { theme: "Void" });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Public API
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
type BuiltInBehavior = "EMBER" | "MIST" | "FLOAT" | "CHAOS";
|
|
26
|
+
type BehaviorName = BuiltInBehavior | (string & {});
|
|
27
|
+
type ThemeName = "Fire" | "Night" | "Ice" | "Frost" | "Toxic" | "Void";
|
|
28
|
+
|
|
29
|
+
interface AmbientConfig {
|
|
30
|
+
behavior: BehaviorName;
|
|
31
|
+
colors: string[];
|
|
32
|
+
spark: string;
|
|
33
|
+
count: number;
|
|
34
|
+
wind: { x: number; y: number };
|
|
35
|
+
decay: number;
|
|
36
|
+
speed: number;
|
|
37
|
+
size: number;
|
|
38
|
+
alpha: number; // clamped to [0, 1]
|
|
39
|
+
turbulence: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface AmbientOptions {
|
|
43
|
+
theme?: ThemeName; // default "Fire"
|
|
44
|
+
overrides?: Partial<AmbientConfig>; // shallow-merges wind
|
|
45
|
+
autoStart?: boolean; // default true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface AmbientInstance {
|
|
49
|
+
setTheme(name: ThemeName): void;
|
|
50
|
+
updateConfig(overrides: Partial<AmbientConfig>): void;
|
|
51
|
+
readonly config: AmbientConfig;
|
|
52
|
+
readonly theme: ThemeName;
|
|
53
|
+
readonly count: number;
|
|
54
|
+
readonly running: boolean;
|
|
55
|
+
pause(): void;
|
|
56
|
+
resume(): void;
|
|
57
|
+
destroy(): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface Particle {
|
|
61
|
+
id: number;
|
|
62
|
+
color: string;
|
|
63
|
+
spriteCanvas: HTMLCanvasElement | null;
|
|
64
|
+
z: number;
|
|
65
|
+
life: number;
|
|
66
|
+
x: number; y: number;
|
|
67
|
+
size: number;
|
|
68
|
+
vx: number; vy: number;
|
|
69
|
+
decay: number;
|
|
70
|
+
maxAlpha: number;
|
|
71
|
+
anchorX: number; anchorY: number; // MIST-only, zeroed otherwise
|
|
72
|
+
pulseOffset: number; // MIST-only, zeroed otherwise
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface FrameContext {
|
|
76
|
+
cfg: AmbientConfig;
|
|
77
|
+
W: number; H: number;
|
|
78
|
+
dt: number; // ms since last frame, clamped to 50
|
|
79
|
+
ds: number; // dt / 16
|
|
80
|
+
timestamp: number; // RAF-callback timestamp
|
|
81
|
+
isInit: boolean;
|
|
82
|
+
getSprite(color: string, logicalSize: number): HTMLCanvasElement;
|
|
83
|
+
respawn(p: Particle, isInit: boolean): void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface BehaviorDefinition {
|
|
87
|
+
spriteLogical: number;
|
|
88
|
+
spawn(p: Particle, frame: FrameContext): void;
|
|
89
|
+
tick(particles: Particle[], ctx: CanvasRenderingContext2D, frame: FrameContext): void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Factory
|
|
93
|
+
function createAmbientFX(canvas: HTMLCanvasElement, options?: AmbientOptions): AmbientInstance;
|
|
94
|
+
|
|
95
|
+
// Registry
|
|
96
|
+
const BEHAVIORS: Record<string, BehaviorDefinition>;
|
|
97
|
+
function registerBehavior(name: string, def: BehaviorDefinition): void;
|
|
98
|
+
|
|
99
|
+
// Constants
|
|
100
|
+
const VERSION: string;
|
|
101
|
+
const THEMES: Record<ThemeName, AmbientConfig>;
|
|
102
|
+
const THEME_META: Array<{ id: ThemeName; name: string; icon: string; behavior: BuiltInBehavior }>;
|
|
103
|
+
|
|
104
|
+
// Pure helpers
|
|
105
|
+
function mergeThemeConfig(base: AmbientConfig, overrides: Partial<AmbientConfig> | null): AmbientConfig;
|
|
106
|
+
function validateConfig(cfg: AmbientConfig): AmbientConfig;
|
|
107
|
+
function envelopeAlpha(mode: BuiltInBehavior, life: number, maxAlpha: number): number;
|
|
108
|
+
function sinLut(index: number): number;
|
|
109
|
+
function deltaScale(dtMs: number): number;
|
|
110
|
+
function clearAmbientSpriteCache(colors?: string[]): void;
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Behavior semantics (built-ins)
|
|
114
|
+
|
|
115
|
+
- **EMBER** -- rising particles with wind + turbulence; 15% "super" sparks
|
|
116
|
+
with 0.3x decay and 1.5x speed. `life` is 0..1 progress; fade-in 0-0.2,
|
|
117
|
+
fade-out 0.2-1. `spriteLogical: 64`.
|
|
118
|
+
- **MIST** -- large breathing fog blobs; wrap at edges; sine-LUT breath
|
|
119
|
+
modulation on alpha and size. `life` is an ms accumulator that wraps at
|
|
120
|
+
72_000 to avoid Float32 drift. Requires `mix-blend-mode: screen` on
|
|
121
|
+
the canvas CSS to look right. `spriteLogical: 128`.
|
|
122
|
+
- **FLOAT** -- rising particles with sine-wave horizontal sway; no
|
|
123
|
+
turbulence. `life` is 0..1; fade-in 0-0.1, sustain, fade-out 0.9-1.
|
|
124
|
+
`spriteLogical: 64`.
|
|
125
|
+
- **CHAOS** -- random omnidirectional velocity; straight lines; ~7.8Hz
|
|
126
|
+
bit-flicker on alpha (via `Math.trunc(timestamp / 128)`). `life` is
|
|
127
|
+
0..1 progress; dies at >=1 or on edge exit. `spriteLogical: 64`.
|
|
128
|
+
|
|
129
|
+
## Adding a custom behavior
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
import { registerBehavior } from "@zakkster/lite-ambient-fx";
|
|
133
|
+
|
|
134
|
+
registerBehavior("SNOW", {
|
|
135
|
+
spriteLogical: 64,
|
|
136
|
+
spawn(p, frame) {
|
|
137
|
+
// MUST populate every field of `p`. MUST NOT add new fields.
|
|
138
|
+
// MUST set p.spriteCanvas via frame.getSprite(color, spriteLogical).
|
|
139
|
+
// ... (see README "Adding a custom behavior" for the full example)
|
|
140
|
+
},
|
|
141
|
+
tick(particles, ctx, frame) {
|
|
142
|
+
// ... own physics + alpha envelope, call frame.respawn(p, false) on death.
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The `Particle` shape is monomorphic -- every field (including MIST-only
|
|
148
|
+
`anchorX`/`anchorY`/`pulseOffset`) is present on every particle. Custom
|
|
149
|
+
behaviors zero out the fields they don't use.
|
|
150
|
+
|
|
151
|
+
## Configuration semantics
|
|
152
|
+
|
|
153
|
+
- `updateConfig` changes take effect for **new spawns**; alive particles
|
|
154
|
+
keep the values baked at spawn. Sliding a knob feels smooth this way.
|
|
155
|
+
- `wind` on `overrides` shallow-merges: `{ wind: { x: 1 } }` preserves
|
|
156
|
+
the existing `y`.
|
|
157
|
+
- `overrides.colors` or `overrides.spark` triggers targeted sprite-cache
|
|
158
|
+
eviction of the previous palette.
|
|
159
|
+
- `overrides.behavior` re-primes sprites (mist vs core sprite logical
|
|
160
|
+
sizes differ) and re-inits the particle pool.
|
|
161
|
+
- `overrides.count` re-inits the pool (grow or trim tail).
|
|
162
|
+
|
|
163
|
+
## Sizing
|
|
164
|
+
|
|
165
|
+
Canvas should be a fullscreen overlay:
|
|
166
|
+
|
|
167
|
+
```css
|
|
168
|
+
canvas#fx {
|
|
169
|
+
position: fixed;
|
|
170
|
+
inset: 0;
|
|
171
|
+
width: 100dvw;
|
|
172
|
+
height: 100dvh;
|
|
173
|
+
z-index: 0;
|
|
174
|
+
mix-blend-mode: screen;
|
|
175
|
+
pointer-events: none;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The module handles DPR internally; sprites are rasterized at
|
|
180
|
+
`logicalSize x devicePixelRatio`.
|
|
181
|
+
|
|
182
|
+
## Performance envelope
|
|
183
|
+
|
|
184
|
+
- Target: 40-500 particles as an atmosphere layer behind normal UI.
|
|
185
|
+
- Not the tool for 10 000+ particles. Reach for an SoA GPU pipeline instead.
|
|
186
|
+
- Zero allocation on the render loop:
|
|
187
|
+
- Nested `Map<color, Map<physical, canvas>>` sprite cache -- no key
|
|
188
|
+
strings.
|
|
189
|
+
- Sprite reference cached on the particle at spawn time -- no
|
|
190
|
+
per-frame `getSprite` calls.
|
|
191
|
+
- Particle shape monomorphic -- all fields present at push time.
|
|
192
|
+
- Behavior dispatch hoisted outside the loop -- one lookup per frame.
|
|
193
|
+
- Pooled `FrameContext` -- allocated once, mutated per frame.
|
|
194
|
+
- Shared 360-entry Float32 sine LUT constructed once at module load.
|
|
195
|
+
|
|
196
|
+
## Runtime targets
|
|
197
|
+
|
|
198
|
+
- ES2020 + Canvas 2D.
|
|
199
|
+
- Chrome/Edge/Firefox (last two majors), Safari 14+, Twitch Extensions.
|
|
200
|
+
- Node 18+ (for tests only, under a minimal DOM shim).
|
|
201
|
+
- Not SSR-safe to construct; module import is safe (no top-level DOM
|
|
202
|
+
access).
|
|
203
|
+
|
|
204
|
+
## Zero-dependency pledge
|
|
205
|
+
|
|
206
|
+
No runtime imports from anywhere. Single-file ESM. `sideEffects: false`.
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT (c) Zahary Shinikchiev
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zakkster/lite-ambient-fx",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-dependency fullscreen ambient particle atmospheres. Six themed presets across four behaviors. Sprite-cached, DPR-aware, resize-preserving, visibility-paused. One file.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./AmbientFX.js",
|
|
7
|
+
"module": "./AmbientFX.js",
|
|
8
|
+
"types": "./AmbientFX.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./AmbientFX.d.ts",
|
|
12
|
+
"node": "./AmbientFX.js",
|
|
13
|
+
"import": "./AmbientFX.js",
|
|
14
|
+
"default": "./AmbientFX.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"AmbientFX.js",
|
|
19
|
+
"AmbientFX.d.ts",
|
|
20
|
+
"CHANGELOG.md",
|
|
21
|
+
"LICENSE.txt",
|
|
22
|
+
"README.md",
|
|
23
|
+
"llms.txt"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "node --test 'test/*.mjs'",
|
|
28
|
+
"demo": "npx serve ."
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"particles",
|
|
32
|
+
"ambient",
|
|
33
|
+
"atmosphere",
|
|
34
|
+
"canvas",
|
|
35
|
+
"fx",
|
|
36
|
+
"zero-dependency",
|
|
37
|
+
"twitch-extension",
|
|
38
|
+
"background",
|
|
39
|
+
"presets",
|
|
40
|
+
"single-file",
|
|
41
|
+
"zakkster"
|
|
42
|
+
],
|
|
43
|
+
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/PeshoVurtoleta/lite-ambient-fx.git"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/PeshoVurtoleta/lite-ambient-fx/issues"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/PeshoVurtoleta/lite-ambient-fx#readme",
|
|
53
|
+
"funding": {
|
|
54
|
+
"type": "github",
|
|
55
|
+
"url": "https://github.com/sponsors/PeshoVurtoleta"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18"
|
|
59
|
+
}
|
|
60
|
+
}
|