noisefloor 0.0.0-stage → 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 digitalemagine
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 CHANGED
@@ -1,3 +1,365 @@
1
- # Temporary Holding Version
1
+ # noisefloor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Analog-signal interference for UI: labels and images that break up like a weak signal, then recover. Layout never moves, screen readers and reduced-motion users are unaffected, and cross-origin images work without CORS. MIT, zero runtime dependencies, about 7.5 KB gzipped (4.5 KB for Scramble alone).
4
+
5
+ **[Live demo, docs and playground](https://digitalemagine.github.io/noisefloor/)**
6
+
7
+ ![noisefloor demo: ambient micro-glitches, the menu and headline scrambling on hover, both images glitching](https://digitalemagine.github.io/noisefloor/media/demo.gif)
8
+
9
+ - **Ambient**: Poisson-timed micro-glitches across any mix of text and image instances: a page that sometimes loses signal, without anyone touching it.
10
+ - **Scramble**: per-character text scramble with palette colours, vertical jitter, skew, RGB split, glow, flicker, glitch lines and scanlines. Typing mode (`reveal: 'type'`) types the label character by character, each scrambling briefly before it locks, at a fitted `duration` or a fixed `speed`, with an optional block caret. Layout never moves. Works on plain text: child elements inside the target are replaced by its text. Made for labels and headings, not paragraphs: every character is its own element, restyled on each frame.
11
+ - **Glitch**: canvas image glitch over an existing `<img>`: channel split, slice tears, block moves, colour lines, frame shake. No pixel reads, so cross-origin images work without CORS.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm i noisefloor
17
+ ```
18
+
19
+ ```js
20
+ import { Scramble, Glitch, Ambient } from 'noisefloor';
21
+ ```
22
+
23
+ Without a bundler, one script tag exposes the `Noisefloor` global:
24
+
25
+ ```html
26
+ <script src="https://unpkg.com/noisefloor"></script>
27
+ ```
28
+
29
+ ## Quick start
30
+
31
+ ```html
32
+ <nav>
33
+ <a href="#docs"><span class="label">Docs</span></a>
34
+ <a href="#about"><span class="label">About</span></a>
35
+ </nav>
36
+ <img class="glitch" src="feed.jpg" alt="Camera feed">
37
+
38
+ <script type="module">
39
+ import { Scramble, Glitch, Ambient } from 'noisefloor';
40
+
41
+ const labels = Scramble.bind('nav a .label');
42
+ const images = Glitch.bind('img.glitch');
43
+ new Ambient([...labels, ...images], { rate: 30, amount: 0.25 });
44
+ </script>
45
+ ```
46
+
47
+ With the script tag, use `Noisefloor.Scramble`, `Noisefloor.Glitch` and `Noisefloor.Ambient`.
48
+
49
+ ## API
50
+
51
+ Every effect is a class: `new Scramble(el, options)`, `new Glitch(img, options)`, `new Ambient(targets, options)`. `Scramble.bind()` and `Glitch.bind()` take a selector, element or list, and return the instances.
52
+
53
+ - `play()`: a full scramble (characters lock in order) or a full image glitch. `play({ text })` scrambles a label to new text.
54
+ - `burst({ amount, count, duration, intensity })`: a micro-glitch on part of the target (`count`: Scramble only); this is what `Ambient` calls. Ignored while playing.
55
+ - `set(options)`: change options, safe while playing. `options` returns the current, validated values.
56
+ - `finished`: a promise resolved when the current animation ends: `await label.play().finished`.
57
+ - `finish()` jumps to rest; `destroy()` removes the effect (Scramble leaves the label as plain text); `running`, `destroyed`, and for Glitch `ready` (image loaded and measured).
58
+ - `bind(targets, options, { trigger, host, once })`: `trigger` is an event name (default `mouseenter`, which also plays on focus) or `'visible'` (when the element scrolls into view); `once: true` plays on the first trigger only; `host: (el) => element` picks the element that listens (by default the closest link or button; otherwise the label itself for Scramble, the image wrapper for Glitch).
59
+ - `Ambient`: `fire()`, `setTargets()`, `start()`, `stop()`, `destroy()` (targets are not destroyed). It picks an idle target each time and pauses while the tab is hidden.
60
+
61
+ Typing instead of scrambling, on scroll:
62
+
63
+ ```js
64
+ Scramble.bind('h2', { reveal: 'type', cursor: true }, { trigger: 'visible', once: true });
65
+ ```
66
+
67
+ Cycling words: `play({ text })` changes the width once, when it starts. To keep even that from moving the layout, reserve the widest word with `min-width`:
68
+
69
+ ```html
70
+ <span id="status" style="min-width: 11ch">Idle</span>
71
+ <script type="module">
72
+ import { Scramble } from 'noisefloor';
73
+
74
+ const status = new Scramble(document.getElementById('status'));
75
+ const words = ['Idle', 'Scanning', 'Reacquiring', 'Locked'];
76
+ let i = 0;
77
+ setInterval(() => status.play({ text: words[++i % words.length] }), 2000);
78
+ </script>
79
+ ```
80
+
81
+ `ch` matches the widest word exactly only in a monospace font; with a proportional font, measure the widest word once, or round up.
82
+
83
+ Options are validated: numbers out of range are clamped, invalid values fall back to the default, and development builds warn once per option.
84
+
85
+ <details>
86
+ <summary>Scramble options</summary>
87
+
88
+ | Option | Default | Meaning |
89
+ |---|---|---|
90
+ | `duration` | `0.7` | Seconds until the last character locks. |
91
+ | `fps` | `24` | Glyph and jitter refresh rate. Lower reads more analog. |
92
+ | `settle` | `0.35` | Seconds for colour and position to decay after a character locks. |
93
+ | `order` | `"ltr"` | Lock order: ltr, rtl, center or random. With typing, also the order characters appear in. |
94
+ | `reveal` | `"all"` | 'all': every character scrambles at once. 'type': characters appear one by one in order, the width reserved from the start. |
95
+ | `trail` | `0.15` | Typing: seconds a character scrambles between appearing and locking. |
96
+ | `speed` | `0` | Typing: characters per second; when above 0 the run length follows the text and duration is ignored. |
97
+ | `cursor` | `false` | Typing: block caret at the typing head. Colour: --nf-cursor. |
98
+ | `chars` | `"01#%=$>≤<"` | Glyphs used while scrambling. |
99
+ | `palette` | `["#ff2bd6","#00f0ff","#f5ff3b"]` | Colours for scrambling glyphs and lines; [0] and [1] drive the RGB split. |
100
+ | `jitter` | `1.5` | Max vertical offset of a scrambling character, px. |
101
+ | `skew` | `8` | Max skew of a scrambling character, degrees. |
102
+ | `rgbSplit` | `1.5` | Chromatic aberration, px. |
103
+ | `glow` | `6` | Bloom on scrambling characters, px. |
104
+ | `flicker` | `0.12` | Chance (0-1) a scrambling character dims on a frame. |
105
+ | `lines` | `3` | Glitch lines drawn over the label (0-8). |
106
+ | `tear` | `2` | Horizontal sync slip of the whole label, px. |
107
+ | `scanlines` | `true` | Rolling scanline overlay while live. |
108
+
109
+ </details>
110
+
111
+ <details>
112
+ <summary>Glitch options</summary>
113
+
114
+ | Option | Default | Meaning |
115
+ |---|---|---|
116
+ | `duration` | `0.6` | Seconds per full glitch. |
117
+ | `fps` | `20` | Frame refresh rate. |
118
+ | `palette` | `["#ff2bd6","#00f0ff","#f5ff3b"]` | Colours for lines and tinted bands. |
119
+ | `rgbSplit` | `6` | Max R/B channel offset, px. |
120
+ | `slices` | `10` | Horizontal bands displaced per frame at full amount. |
121
+ | `tear` | `24` | Max sideways displacement of a band, px. |
122
+ | `blocks` | `6` | Rectangles copied out of place per frame. |
123
+ | `lines` | `4` | Palette-coloured scan lines per frame. |
124
+ | `jitter` | `2` | Vertical shake of the whole frame, px. |
125
+ | `flicker` | `0.1` | Chance (0-1) a frame dims. |
126
+ | `scanlines` | `true` | Rolling scanline overlay while live. |
127
+
128
+ </details>
129
+
130
+ <details>
131
+ <summary>Ambient options</summary>
132
+
133
+ | Option | Default | Meaning |
134
+ |---|---|---|
135
+ | `rate` | `30` | Micro-glitches per minute, across all targets. |
136
+ | `amount` | `0.2` | Share (0-1) of each target disturbed. |
137
+ | `duration` | `0.18` | Seconds per micro-glitch. |
138
+ | `intensity` | `0.5` | Artefact strength (0-1). |
139
+ | `enabled` | `true` | Start scheduling immediately. |
140
+
141
+ </details>
142
+
143
+ Live demo, full reference and a playground that writes the configuration for you: [the project site](https://digitalemagine.github.io/noisefloor/), also built locally by `npm run build:site`.
144
+
145
+ ## HTML attributes
146
+
147
+ For static pages and CMS content, the same effects can be declared in markup. Import `noisefloor/auto` once, or load `noisefloor.auto.iife.js` (the full library plus auto-init) instead of the plain script tag:
148
+
149
+ ```html
150
+ <script src="https://unpkg.com/noisefloor/dist/noisefloor.auto.iife.js"></script>
151
+
152
+ <a href="#docs"><span data-nf="scramble" data-nf-order="center">Docs</span></a>
153
+ <span data-nf="scramble" data-nf-reveal="type" data-nf-trigger="visible" data-nf-once>Reacquiring signal</span>
154
+ <img data-nf="glitch" data-nf-rgb-split="6" data-nf-ambient src="feed.jpg" alt="Camera feed">
155
+ ```
156
+
157
+ ```js
158
+ import 'noisefloor/auto';
159
+ ```
160
+
161
+ - `data-nf`: `scramble` or `glitch` (an `<img>` only).
162
+ - `data-nf-<option>`: any effect option, in kebab case (`data-nf-rgb-split`) or as one word (`data-nf-rgbsplit`). Numbers and strings as written; booleans are true when present and false with `"false"`; `data-nf-palette` is a comma-separated list.
163
+ - `data-nf-trigger`: as `bind()`'s `trigger` (default hover/focus of the closest link or button; `visible`; any event name), or `none` for no trigger. `data-nf-once`: unbind after the first play.
164
+ - `data-nf-ambient`: add the element to one shared Ambient; `ambient().set({ rate: 12 })` changes its options.
165
+
166
+ It scans the document when it is ready and watches it: elements added later are set up, removed ones are destroyed, and changing `data-nf` rebuilds the effect. Other attributes are read once. Elements that already have an instance are skipped. The module exports `scan(root)` (for shadow roots, which the observer does not see), `start()`, `stop()` and `ambient()`; the script-tag bundle exposes them as `Noisefloor.auto`.
167
+
168
+ Not for markup a framework renders: use the recipes below, which control when instances are created and destroyed.
169
+
170
+ ## Frameworks
171
+
172
+ React, Vue and Svelte own the nodes they render, while Scramble replaces an element's children and Glitch moves the img into a wrapper. Without care, the next render corrupts the label or throws (React: `insertBefore ... is not a child of this node`). Three rules keep them apart:
173
+
174
+ - **Render the label once**, then change it with `play({ text })`: the framework never patches the text node Scramble replaced. React: the first value from `useState`; Vue: `v-once`; Svelte: `untrack`.
175
+ - **Wrap the img in an element marked `data-nf-wrap`.** Glitch uses it instead of inserting its own wrapper, so the img stays where the framework put it.
176
+ - **Create instances after mount and `destroy()` them on unmount.** Options are read at mount; change them later with `set()`.
177
+
178
+ Checked with React 19 (StrictMode), Vue 3.5 and Svelte 5.
179
+
180
+ <details>
181
+ <summary>React</summary>
182
+
183
+ ```jsx
184
+ // ScrambleText.jsx
185
+ import { useEffect, useRef, useState } from 'react';
186
+ import { Scramble } from 'noisefloor';
187
+
188
+ export function ScrambleText({ text, options, bindOptions }) {
189
+ const ref = useRef(null);
190
+ const inst = useRef(null);
191
+ // Rendered once: Scramble owns the children from then on; later text goes through play().
192
+ const [initial] = useState(text);
193
+
194
+ useEffect(() => {
195
+ const [s] = Scramble.bind(ref.current, options, bindOptions);
196
+ inst.current = s;
197
+ return () => s.destroy();
198
+ }, []);
199
+
200
+ useEffect(() => {
201
+ const s = inst.current;
202
+ if (s && s.text !== text) s.play({ text });
203
+ }, [text]);
204
+
205
+ return <span ref={ref}>{initial}</span>;
206
+ }
207
+ ```
208
+
209
+ ```jsx
210
+ // GlitchImage.jsx
211
+ import { useEffect, useRef } from 'react';
212
+ import { Glitch } from 'noisefloor';
213
+
214
+ export function GlitchImage({ options, bindOptions, ...img }) {
215
+ const ref = useRef(null);
216
+
217
+ useEffect(() => {
218
+ const [g] = Glitch.bind(ref.current, options, bindOptions);
219
+ return () => g.destroy();
220
+ }, []);
221
+
222
+ // data-nf-wrap: Glitch uses this span instead of moving the img into its own wrapper.
223
+ return (
224
+ <span data-nf-wrap="" style={{ display: 'inline-block' }}>
225
+ <img ref={ref} {...img} />
226
+ </span>
227
+ );
228
+ }
229
+ ```
230
+
231
+ </details>
232
+
233
+ <details>
234
+ <summary>Vue</summary>
235
+
236
+ `ScrambleText.vue`:
237
+
238
+ ```vue
239
+ <script setup>
240
+ import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
241
+ import { Scramble } from 'noisefloor';
242
+
243
+ const props = defineProps({ text: { type: String, required: true }, options: Object, bindOptions: Object });
244
+ const el = ref(null);
245
+ let s = null;
246
+
247
+ onMounted(() => { [s] = Scramble.bind(el.value, props.options, props.bindOptions); });
248
+ watch(() => props.text, (text) => { if (s) s.play({ text }); });
249
+ onBeforeUnmount(() => { if (s) s.destroy(); });
250
+ </script>
251
+
252
+ <template>
253
+ <!-- v-once: Vue renders the first text and never patches it; later text goes through play() -->
254
+ <span ref="el" v-once>{{ text }}</span>
255
+ </template>
256
+ ```
257
+
258
+ `GlitchImage.vue`:
259
+
260
+ ```vue
261
+ <script setup>
262
+ import { onBeforeUnmount, onMounted, ref } from 'vue';
263
+ import { Glitch } from 'noisefloor';
264
+
265
+ const props = defineProps({ src: String, alt: String, options: Object, bindOptions: Object });
266
+ const img = ref(null);
267
+ let g = null;
268
+
269
+ onMounted(() => { [g] = Glitch.bind(img.value, props.options, props.bindOptions); });
270
+ onBeforeUnmount(() => { if (g) g.destroy(); });
271
+ </script>
272
+
273
+ <template>
274
+ <!-- data-nf-wrap: Glitch uses this span instead of moving the img into its own wrapper -->
275
+ <span data-nf-wrap style="display: inline-block"><img ref="img" :src="src" :alt="alt"></span>
276
+ </template>
277
+ ```
278
+
279
+ </details>
280
+
281
+ <details>
282
+ <summary>Svelte</summary>
283
+
284
+ `ScrambleText.svelte`:
285
+
286
+ ```svelte
287
+ <script>
288
+ import { untrack } from 'svelte';
289
+ import { Scramble } from 'noisefloor';
290
+
291
+ let { text, options, bindOptions } = $props();
292
+ // Rendered once: Scramble owns the children from then on; later text goes through play().
293
+ const initial = untrack(() => text);
294
+
295
+ function scramble(node, text) {
296
+ const [s] = Scramble.bind(node, options, bindOptions);
297
+ return {
298
+ update(next) { s.play({ text: next }); },
299
+ destroy() { s.destroy(); }
300
+ };
301
+ }
302
+ </script>
303
+
304
+ <span use:scramble={text}>{initial}</span>
305
+ ```
306
+
307
+ `GlitchImage.svelte`:
308
+
309
+ ```svelte
310
+ <script>
311
+ import { Glitch } from 'noisefloor';
312
+
313
+ let { options, bindOptions, ...img } = $props();
314
+
315
+ function glitch(node) {
316
+ const [g] = Glitch.bind(node, options, bindOptions);
317
+ return { destroy() { g.destroy(); } };
318
+ }
319
+ </script>
320
+
321
+ <!-- data-nf-wrap: Glitch uses this span instead of moving the img into its own wrapper -->
322
+ <span data-nf-wrap style="display: inline-block"><img use:glitch {...img} /></span>
323
+ ```
324
+
325
+ </details>
326
+
327
+ ## Content-Security-Policy
328
+
329
+ The base CSS is injected as a `<style>` element, which a policy without `'unsafe-inline'` in `style-src` blocks. Load the stylesheet yourself and turn injection off before creating instances:
330
+
331
+ ```js
332
+ import 'noisefloor/style.css'; // or <link rel="stylesheet" href="https://unpkg.com/noisefloor/dist/style.css">
333
+ import { configure } from 'noisefloor';
334
+
335
+ configure({ injectCSS: false });
336
+ ```
337
+
338
+ Inside a shadow root, the stylesheet must be loaded in that root.
339
+
340
+ ## Browser support
341
+
342
+ Safari 16, Chrome and Edge 107, Firefox 104 and later (the default build target of Vite, Baseline widely available). The published files use no syntax beyond what these browsers support; a bundler targeting older browsers transpiles them. With the script tag, an older browser skips the library and the labels and images show unchanged.
343
+
344
+ ## Development
345
+
346
+ ```sh
347
+ npm i && npx playwright install chromium
348
+ npm run typecheck # tsc --noEmit
349
+ npm test # unit tests (Vitest browser mode, headless Chromium)
350
+ npm run build # dist/: ESM, CJS, browser global bundle, .d.ts, style.css
351
+ npm run check:package # publint, arethetypeswrong, tree-shaking check (needs dist/)
352
+ npm run build:site # site/dist/index.html (demo, docs, playground)
353
+ npm run smoke # build, build:site, headless smoke test of the built site
354
+ npm run fidelity # build, build:site, image overlay vs real image comparison
355
+ ```
356
+
357
+ Design notes and decisions: `docs/DESIGN.md`. Changes: `CHANGELOG.md`.
358
+
359
+ ## About this project
360
+
361
+ Like some of my other recent projects, noisefloor is a Claude-first experiment: the code, tests, docs and site were written with an AI assistant (Claude Code). My role was to steer it: I set the direction, reviewed plans before any code, challenged answers, iterated and reviewed the changes. The site illustrations are mine. Every change runs through the checks in this repository: unit tests in headless Chromium, a smoke test of the built site and a pixel comparison of the image overlay against the real image. It surely has rough edges I haven't found yet; feedback is welcome.
362
+
363
+ ## Licence
364
+
365
+ MIT, Copyright (c) 2026 digitalemagine.
package/dist/auto.cjs ADDED
@@ -0,0 +1,172 @@
1
+ /*! noisefloor v0.1.0 | MIT | (c) 2026 digitalemagine */
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const require_ambient = require("./core.cjs");
4
+ //#region src/auto.ts
5
+ /**
6
+ * noisefloor/auto: effects declared in HTML attributes.
7
+ *
8
+ * <span data-nf="scramble" data-nf-order="center">Docs</span>
9
+ * <img data-nf="glitch" data-nf-rgb-split="6" data-nf-ambient src="feed.jpg" alt="">
10
+ *
11
+ * Importing this module (or loading noisefloor.auto.iife.js) scans the
12
+ * document when it is ready and keeps watching it: elements added later are
13
+ * set up, removed ones are destroyed. The core `noisefloor` entry has no such
14
+ * side effect.
15
+ */
16
+ /** `data-nf-*` attributes that are not effect options. */
17
+ const RESERVED = [
18
+ "wrap",
19
+ "trigger",
20
+ "once",
21
+ "ambient"
22
+ ];
23
+ const live = /* @__PURE__ */ new Set();
24
+ let observer = null;
25
+ let shared = null;
26
+ /** start() pending on DOMContentLoaded; cleared by stop() */
27
+ let pending = false;
28
+ const target = (inst) => inst instanceof require_ambient.Glitch ? inst.img : inst.el;
29
+ /**
30
+ * Effect options from the element's `data-nf-*` attributes, typed by the
31
+ * defaults: numbers, booleans (present means true, "false" means false),
32
+ * `palette` as a comma-separated list, strings as given. Names are matched
33
+ * without dashes and case, since HTML lowercases attribute names:
34
+ * `data-nf-rgb-split` and `data-nf-rgbsplit` both set `rgbSplit`. Unknown names
35
+ * are passed on, so the effect warns about them in development builds.
36
+ */
37
+ function options(el, defaults) {
38
+ const o = {}, names = Object.keys(defaults), d = defaults;
39
+ for (const a of Array.from(el.attributes)) {
40
+ if (a.name.slice(0, 8) !== "data-nf-") continue;
41
+ const raw = a.name.slice(8);
42
+ if (RESERVED.indexOf(raw) >= 0) continue;
43
+ const flat = raw.replace(/-/g, "");
44
+ const k = names.filter((n) => n.toLowerCase() === flat)[0] || raw.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
45
+ const def = d[k], v = a.value;
46
+ if (typeof def === "number") o[k] = v.trim() === "" ? NaN : Number(v);
47
+ else if (typeof def === "boolean") o[k] = v !== "false";
48
+ else if (Array.isArray(def)) o[k] = v.split(/,(?![^(]*\))/).map((s) => s.trim()).filter(Boolean);
49
+ else o[k] = v;
50
+ }
51
+ return o;
52
+ }
53
+ /** Set up one `[data-nf]` element. Skips elements that already have an instance or are detached. */
54
+ function create(el) {
55
+ if (require_ambient.owner(el) || !el.isConnected) return null;
56
+ const kind = (el.getAttribute("data-nf") || "").trim().toLowerCase();
57
+ const trigger = el.getAttribute("data-nf-trigger") || "mouseenter";
58
+ const b = {
59
+ trigger,
60
+ once: el.hasAttribute("data-nf-once")
61
+ };
62
+ let inst;
63
+ if (kind === "scramble") {
64
+ const el2 = el, o = options(el, require_ambient.Scramble.defaults);
65
+ inst = trigger === "none" ? new require_ambient.Scramble(el2, o) : require_ambient.Scramble.bind(el2, o, b)[0];
66
+ } else if (kind === "glitch" && el.tagName === "IMG") {
67
+ const img = el, o = options(el, require_ambient.Glitch.defaults);
68
+ inst = trigger === "none" ? new require_ambient.Glitch(img, o) : require_ambient.Glitch.bind(img, o, b)[0];
69
+ } else {
70
+ try {
71
+ if (process.env.NODE_ENV !== "production") require_ambient.warn("auto.kind." + kind + el.tagName, kind === "glitch" ? `auto: data-nf="glitch" needs an <img>, got <${el.tagName.toLowerCase()}>; ignored` : `auto: data-nf must be "scramble" or "glitch", got ${JSON.stringify(kind)}; ignored`);
72
+ } catch {}
73
+ return null;
74
+ }
75
+ live.add(inst);
76
+ if (el.hasAttribute("data-nf-ambient")) {
77
+ const a = ambient();
78
+ a.setTargets(a.targets.concat(inst));
79
+ }
80
+ return inst;
81
+ }
82
+ function drop(inst) {
83
+ live.delete(inst);
84
+ inst.destroy();
85
+ }
86
+ function onMutations(records) {
87
+ let removed = false;
88
+ for (const r of records) {
89
+ if (r.type === "attributes") {
90
+ const el = r.target;
91
+ live.forEach((inst) => {
92
+ if (target(inst) === el) drop(inst);
93
+ });
94
+ if (el.hasAttribute("data-nf")) create(el);
95
+ continue;
96
+ }
97
+ for (const n of Array.from(r.removedNodes)) if (n.nodeType === 1) removed = true;
98
+ for (const n of Array.from(r.addedNodes)) if (n.nodeType === 1) scan(n);
99
+ }
100
+ if (removed) live.forEach((inst) => {
101
+ if (!target(inst).isConnected) drop(inst);
102
+ });
103
+ }
104
+ /**
105
+ * Set up every `[data-nf]` element in `root` (and `root` itself) that has no
106
+ * instance yet; returns the new instances. start() does this for the
107
+ * document; call it for content the observer does not see, such as a shadow
108
+ * root. Instances made here are not destroyed automatically when removed.
109
+ */
110
+ function scan(root = document) {
111
+ const els = Array.prototype.slice.call(root.querySelectorAll("[data-nf]"));
112
+ if (root.nodeType === 1 && root.hasAttribute("data-nf")) els.unshift(root);
113
+ const out = [];
114
+ for (const el of els) {
115
+ const inst = create(el);
116
+ if (inst) out.push(inst);
117
+ }
118
+ return out;
119
+ }
120
+ /**
121
+ * Scan the document and watch it for added or removed elements and changes of
122
+ * the `data-nf` attribute. Called on import; idempotent.
123
+ */
124
+ function start() {
125
+ pending = false;
126
+ if (observer || typeof document === "undefined") return;
127
+ scan(document);
128
+ if (typeof MutationObserver !== "function") return;
129
+ observer = new MutationObserver(onMutations);
130
+ observer.observe(document.documentElement, {
131
+ childList: true,
132
+ subtree: true,
133
+ attributes: true,
134
+ attributeFilter: ["data-nf"]
135
+ });
136
+ }
137
+ /** Stop watching and destroy every instance created from attributes, and the shared Ambient. */
138
+ function stop() {
139
+ pending = false;
140
+ if (observer) {
141
+ observer.disconnect();
142
+ observer = null;
143
+ }
144
+ live.forEach(drop);
145
+ if (shared) {
146
+ shared.destroy();
147
+ shared = null;
148
+ }
149
+ }
150
+ /**
151
+ * The Ambient that drives elements marked `data-nf-ambient`, created on first
152
+ * use with the default options; change them with `ambient().set({ rate: 12 })`.
153
+ */
154
+ function ambient() {
155
+ if (!shared) shared = new require_ambient.Ambient([]);
156
+ return shared;
157
+ }
158
+ if (typeof document !== "undefined") {
159
+ if (document.readyState === "loading") {
160
+ pending = true;
161
+ document.addEventListener("DOMContentLoaded", () => {
162
+ if (pending) start();
163
+ }, { once: true });
164
+ } else start();
165
+ }
166
+ //#endregion
167
+ exports.ambient = ambient;
168
+ exports.scan = scan;
169
+ exports.start = start;
170
+ exports.stop = stop;
171
+
172
+ //# sourceMappingURL=auto.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto.cjs","names":["Glitch","owner","Scramble","Ambient"],"sources":["../src/auto.ts"],"sourcesContent":["/**\n * noisefloor/auto: effects declared in HTML attributes.\n *\n * <span data-nf=\"scramble\" data-nf-order=\"center\">Docs</span>\n * <img data-nf=\"glitch\" data-nf-rgb-split=\"6\" data-nf-ambient src=\"feed.jpg\" alt=\"\">\n *\n * Importing this module (or loading noisefloor.auto.iife.js) scans the\n * document when it is ready and keeps watching it: elements added later are\n * set up, removed ones are destroyed. The core `noisefloor` entry has no such\n * side effect.\n */\nimport { Ambient } from './ambient';\nimport { Glitch } from './glitch';\nimport { Scramble } from './scramble';\nimport { owner, warn } from './shared';\nimport type { BindOptions } from './types';\n\ntype Effect = Scramble | Glitch;\n\n/** `data-nf-*` attributes that are not effect options. */\nconst RESERVED = ['wrap', 'trigger', 'once', 'ambient'];\n\nconst live = new Set<Effect>();\nlet observer: MutationObserver | null = null;\nlet shared: Ambient | null = null;\n/** start() pending on DOMContentLoaded; cleared by stop() */\nlet pending = false;\n\nconst target = (inst: Effect): Element => (inst instanceof Glitch ? inst.img : inst.el);\n\n/**\n * Effect options from the element's `data-nf-*` attributes, typed by the\n * defaults: numbers, booleans (present means true, \"false\" means false),\n * `palette` as a comma-separated list, strings as given. Names are matched\n * without dashes and case, since HTML lowercases attribute names:\n * `data-nf-rgb-split` and `data-nf-rgbsplit` both set `rgbSplit`. Unknown names\n * are passed on, so the effect warns about them in development builds.\n */\nfunction options(el: Element, defaults: object): Record<string, unknown> {\n const o: Record<string, unknown> = {}, names = Object.keys(defaults), d = defaults as Record<string, unknown>;\n for (const a of Array.from(el.attributes)) {\n if (a.name.slice(0, 8) !== 'data-nf-') continue;\n const raw = a.name.slice(8);\n if (RESERVED.indexOf(raw) >= 0) continue;\n const flat = raw.replace(/-/g, '');\n const k = names.filter((n) => n.toLowerCase() === flat)[0] || raw.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());\n const def = d[k], v = a.value;\n if (typeof def === 'number') o[k] = v.trim() === '' ? NaN : Number(v);\n else if (typeof def === 'boolean') o[k] = v !== 'false';\n else if (Array.isArray(def)) o[k] = v.split(/,(?![^(]*\\))/).map((s) => s.trim()).filter(Boolean); // commas inside rgb(...) stay\n else o[k] = v;\n }\n return o;\n}\n\n/** Set up one `[data-nf]` element. Skips elements that already have an instance or are detached. */\nfunction create(el: Element): Effect | null {\n if (owner(el) || !el.isConnected) return null;\n const kind = (el.getAttribute('data-nf') || '').trim().toLowerCase();\n const trigger = el.getAttribute('data-nf-trigger') || 'mouseenter';\n const b = { trigger, once: el.hasAttribute('data-nf-once') } as BindOptions<Element>;\n let inst: Effect;\n if (kind === 'scramble') {\n const el2 = el as HTMLElement, o = options(el, Scramble.defaults);\n inst = trigger === 'none' ? new Scramble(el2, o) : Scramble.bind(el2, o, b)[0] as Scramble;\n } else if (kind === 'glitch' && el.tagName === 'IMG') {\n const img = el as HTMLImageElement, o = options(el, Glitch.defaults);\n inst = trigger === 'none' ? new Glitch(img, o) : Glitch.bind(img, o, b)[0] as Glitch;\n } else {\n if (!__NF_IIFE__) {\n try {\n if (process.env.NODE_ENV !== 'production') {\n warn('auto.kind.' + kind + el.tagName, kind === 'glitch'\n ? `auto: data-nf=\"glitch\" needs an <img>, got <${el.tagName.toLowerCase()}>; ignored`\n : `auto: data-nf must be \"scramble\" or \"glitch\", got ${JSON.stringify(kind)}; ignored`);\n }\n } catch { /* no process global */ }\n }\n return null;\n }\n live.add(inst);\n if (el.hasAttribute('data-nf-ambient')) { const a = ambient(); a.setTargets(a.targets.concat(inst)); }\n return inst;\n}\n\nfunction drop(inst: Effect): void {\n live.delete(inst);\n inst.destroy();\n}\n\nfunction onMutations(records: MutationRecord[]): void {\n let removed = false;\n for (const r of records) {\n if (r.type === 'attributes') {\n const el = r.target as Element;\n live.forEach((inst) => { if (target(inst) === el) drop(inst); });\n if (el.hasAttribute('data-nf')) create(el);\n continue;\n }\n for (const n of Array.from(r.removedNodes)) if (n.nodeType === 1) removed = true;\n for (const n of Array.from(r.addedNodes)) if (n.nodeType === 1) scan(n as Element);\n }\n if (removed) live.forEach((inst) => { if (!target(inst).isConnected) drop(inst); });\n}\n\n/**\n * Set up every `[data-nf]` element in `root` (and `root` itself) that has no\n * instance yet; returns the new instances. start() does this for the\n * document; call it for content the observer does not see, such as a shadow\n * root. Instances made here are not destroyed automatically when removed.\n */\nexport function scan(root: ParentNode = document): Effect[] {\n const els: Element[] = Array.prototype.slice.call(root.querySelectorAll('[data-nf]'));\n if ((root as Element).nodeType === 1 && (root as Element).hasAttribute('data-nf')) els.unshift(root as Element);\n const out: Effect[] = [];\n for (const el of els) { const inst = create(el); if (inst) out.push(inst); }\n return out;\n}\n\n/**\n * Scan the document and watch it for added or removed elements and changes of\n * the `data-nf` attribute. Called on import; idempotent.\n */\nexport function start(): void {\n pending = false;\n if (observer || typeof document === 'undefined') return;\n scan(document);\n if (typeof MutationObserver !== 'function') return;\n observer = new MutationObserver(onMutations);\n observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-nf'] });\n}\n\n/** Stop watching and destroy every instance created from attributes, and the shared Ambient. */\nexport function stop(): void {\n pending = false;\n if (observer) { observer.disconnect(); observer = null; }\n live.forEach(drop);\n if (shared) { shared.destroy(); shared = null; }\n}\n\n/**\n * The Ambient that drives elements marked `data-nf-ambient`, created on first\n * use with the default options; change them with `ambient().set({ rate: 12 })`.\n */\nexport function ambient(): Ambient {\n if (!shared) shared = new Ambient([]);\n return shared;\n}\n\nif (typeof document !== 'undefined') {\n if (document.readyState === 'loading') {\n pending = true;\n document.addEventListener('DOMContentLoaded', () => { if (pending) start(); }, { once: true });\n } else start();\n}\n"],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /*! noisefloor v0.1.0 | MIT | (c) 2026 digitalemagine */
2
+ import { n as Glitch, r as Ambient, t as Scramble } from "./core.cjs";
3
+ //#region src/auto.d.ts
4
+ type Effect = Scramble | Glitch;
5
+ /**
6
+ * Set up every `[data-nf]` element in `root` (and `root` itself) that has no
7
+ * instance yet; returns the new instances. start() does this for the
8
+ * document; call it for content the observer does not see, such as a shadow
9
+ * root. Instances made here are not destroyed automatically when removed.
10
+ */
11
+ export declare function scan(root?: ParentNode): Effect[];
12
+ /**
13
+ * Scan the document and watch it for added or removed elements and changes of
14
+ * the `data-nf` attribute. Called on import; idempotent.
15
+ */
16
+ export declare function start(): void;
17
+ /** Stop watching and destroy every instance created from attributes, and the shared Ambient. */
18
+ export declare function stop(): void;
19
+ /**
20
+ * The Ambient that drives elements marked `data-nf-ambient`, created on first
21
+ * use with the default options; change them with `ambient().set({ rate: 12 })`.
22
+ */
23
+ export declare function ambient(): Ambient;
24
+ //#endregion
25
+ //# sourceMappingURL=auto.d.cts.map
package/dist/auto.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /*! noisefloor v0.1.0 | MIT | (c) 2026 digitalemagine */
2
+ import { n as Glitch, r as Ambient, t as Scramble } from "./core.js";
3
+ //#region src/auto.d.ts
4
+ type Effect = Scramble | Glitch;
5
+ /**
6
+ * Set up every `[data-nf]` element in `root` (and `root` itself) that has no
7
+ * instance yet; returns the new instances. start() does this for the
8
+ * document; call it for content the observer does not see, such as a shadow
9
+ * root. Instances made here are not destroyed automatically when removed.
10
+ */
11
+ export declare function scan(root?: ParentNode): Effect[];
12
+ /**
13
+ * Scan the document and watch it for added or removed elements and changes of
14
+ * the `data-nf` attribute. Called on import; idempotent.
15
+ */
16
+ export declare function start(): void;
17
+ /** Stop watching and destroy every instance created from attributes, and the shared Ambient. */
18
+ export declare function stop(): void;
19
+ /**
20
+ * The Ambient that drives elements marked `data-nf-ambient`, created on first
21
+ * use with the default options; change them with `ambient().set({ rate: 12 })`.
22
+ */
23
+ export declare function ambient(): Ambient;
24
+ //#endregion
25
+ //# sourceMappingURL=auto.d.ts.map