wavegrid 0.1.1

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/esm/filter.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Independent low-pass filter for the receiver.
3
+ *
4
+ * This is separate from the simulator's filter — the receiver runs its own
5
+ * smoothing so that even if the upstream connection drops mid-transition,
6
+ * the output to hardware never jolts.
7
+ */
8
+ export const NUM_CANNONS = 49;
9
+ export const GRID_SIZE = 7;
10
+ export const DEFAULT_RECEIVER_ALPHA = 0.06;
11
+ export function createFilteredGrid() {
12
+ return Array.from({ length: NUM_CANNONS }, () => ({
13
+ h: 220,
14
+ s: 90,
15
+ b: 80,
16
+ targetH: 220,
17
+ targetS: 90,
18
+ targetB: 80
19
+ }));
20
+ }
21
+ /**
22
+ * Shortest angular distance on the hue circle.
23
+ */
24
+ export function angleDelta(from, to) {
25
+ return ((to - from + 540) % 360) - 180;
26
+ }
27
+ /**
28
+ * Single tick of exponential low-pass interpolation.
29
+ * Returns true if any cannon changed (i.e., output is still settling).
30
+ */
31
+ export function tickFilter(grid, alpha = DEFAULT_RECEIVER_ALPHA) {
32
+ let changed = false;
33
+ for (let i = 0; i < grid.length; i++) {
34
+ const c = grid[i];
35
+ const dh = angleDelta(c.h, c.targetH);
36
+ const ds = c.targetS - c.s;
37
+ const db = c.targetB - c.b;
38
+ if (Math.abs(dh) > 0.3 || Math.abs(ds) > 0.3 || Math.abs(db) > 0.3) {
39
+ c.h = (c.h + dh * alpha + 360) % 360;
40
+ c.s = c.s + ds * alpha;
41
+ c.b = c.b + db * alpha;
42
+ changed = true;
43
+ }
44
+ else {
45
+ c.h = c.targetH;
46
+ c.s = c.targetS;
47
+ c.b = c.targetB;
48
+ }
49
+ }
50
+ return changed;
51
+ }
52
+ /**
53
+ * Set the target state for all cannons from an upstream state snapshot.
54
+ * The filter will smoothly converge to these values.
55
+ */
56
+ export function applyUpstreamState(grid, upstream) {
57
+ for (let i = 0; i < Math.min(grid.length, upstream.length); i++) {
58
+ grid[i].targetH = upstream[i].h;
59
+ grid[i].targetS = upstream[i].s;
60
+ grid[i].targetB = upstream[i].b;
61
+ }
62
+ }
package/esm/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { CallbackOutput, ConsoleOutput, MultiOutput, WebSocketInput, WebSocketOutput } from './adapters';
2
+ export { angleDelta, applyUpstreamState, createFilteredGrid, DEFAULT_RECEIVER_ALPHA, tickFilter } from './filter';
3
+ export { computeFallbackFrame, DEFAULT_FALLBACK_CONFIG } from './fallback';
4
+ export { Receiver } from './receiver';
package/esm/main.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Receiver entry point.
3
+ *
4
+ * Demonstrates the adapter pattern — configure input and output
5
+ * adapters via environment variables, then start the receiver.
6
+ */
7
+ import { ConsoleOutput, MultiOutput, WebSocketInput, WebSocketOutput } from './adapters';
8
+ import { Receiver } from './receiver';
9
+ const SIMULATOR_URL = process.env.SIMULATOR_URL || 'ws://localhost:3000';
10
+ const ALPHA = parseFloat(process.env.RECEIVER_ALPHA || '0.06');
11
+ const FALLBACK_DELAY = parseInt(process.env.FALLBACK_DELAY || '3000', 10);
12
+ const WS_OUTPUT_PORT = process.env.WS_OUTPUT_PORT ? parseInt(process.env.WS_OUTPUT_PORT, 10) : undefined;
13
+ // ─── Input adapter ───
14
+ const input = new WebSocketInput({ url: SIMULATOR_URL });
15
+ // ─── Output adapter(s) ───
16
+ const outputs = [new ConsoleOutput()];
17
+ let wsOutput = null;
18
+ if (WS_OUTPUT_PORT) {
19
+ wsOutput = new WebSocketOutput({ port: WS_OUTPUT_PORT });
20
+ wsOutput.listen();
21
+ outputs.push(wsOutput);
22
+ }
23
+ const output = outputs.length === 1 ? outputs[0] : new MultiOutput(outputs);
24
+ // ─── Receiver ───
25
+ const receiver = new Receiver({
26
+ input,
27
+ output,
28
+ alpha: ALPHA,
29
+ fallbackDelay: FALLBACK_DELAY
30
+ });
31
+ console.log('');
32
+ console.log(' \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E');
33
+ console.log(' \u2502 Illuminate \u00B7 Receiver \u2502');
34
+ console.log(' \u2502 the brain \u2502');
35
+ console.log(' \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F');
36
+ console.log('');
37
+ console.log(` \u2192 Input: WebSocket @ ${SIMULATOR_URL}`);
38
+ console.log(` \u2192 Output: Console${wsOutput ? ` + WebSocket :${WS_OUTPUT_PORT}` : ''}`);
39
+ console.log(` \u2192 Alpha: ${ALPHA} Fallback delay: ${FALLBACK_DELAY}ms`);
40
+ console.log('');
41
+ receiver.start();
42
+ // Graceful shutdown
43
+ process.on('SIGINT', () => {
44
+ console.log('\n\n Shutting down...');
45
+ receiver.stop();
46
+ process.exit(0);
47
+ });
48
+ process.on('SIGTERM', () => {
49
+ receiver.stop();
50
+ process.exit(0);
51
+ });
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The Receiver — the brain of the Illuminate installation.
3
+ *
4
+ * A pure state engine that:
5
+ * 1. Receives grid state from an InputAdapter (upstream source)
6
+ * 2. Runs an independent low-pass filter (smooth, never jolts)
7
+ * 3. Falls back to 3D sine waves on signal loss
8
+ * 4. Sends filtered output to an OutputAdapter (hardware target)
9
+ *
10
+ * Both input and output are pluggable adapters — swap them to connect
11
+ * to any protocol or hardware without modifying the receiver core.
12
+ */
13
+ import { ConsoleOutput, WebSocketInput } from './adapters';
14
+ import { computeFallbackFrame, DEFAULT_FALLBACK_CONFIG } from './fallback';
15
+ import { applyUpstreamState, createFilteredGrid, DEFAULT_RECEIVER_ALPHA, tickFilter } from './filter';
16
+ export const DEFAULT_RECEIVER_CONFIG = {
17
+ input: new WebSocketInput({ url: 'ws://localhost:3000' }),
18
+ output: new ConsoleOutput(),
19
+ alpha: DEFAULT_RECEIVER_ALPHA,
20
+ fallbackDelay: 3000,
21
+ fallback: DEFAULT_FALLBACK_CONFIG,
22
+ tickMs: 1000 / 60
23
+ };
24
+ export class Receiver {
25
+ config;
26
+ grid;
27
+ tickTimer = null;
28
+ tick = 0;
29
+ lastDataAt = Date.now();
30
+ _status = 'reconnecting';
31
+ _fallbackActive = false;
32
+ _running = false;
33
+ constructor(config = {}) {
34
+ this.config = { ...DEFAULT_RECEIVER_CONFIG, ...config };
35
+ this.grid = createFilteredGrid();
36
+ }
37
+ get status() { return this._status; }
38
+ get fallbackActive() { return this._fallbackActive; }
39
+ /** Get the current output state (after filtering). */
40
+ getOutputState() {
41
+ return this.grid.map(c => ({
42
+ h: c.h,
43
+ s: c.s,
44
+ b: c.b
45
+ }));
46
+ }
47
+ /** Start the receiver — connects input and begins the tick loop. */
48
+ start() {
49
+ if (this._running)
50
+ return;
51
+ this._running = true;
52
+ this.bindInput();
53
+ this.startTickLoop();
54
+ console.log(' \u25C8 Receiver started');
55
+ }
56
+ /** Stop the receiver — disconnects and stops the tick loop. */
57
+ stop() {
58
+ this._running = false;
59
+ if (this.tickTimer) {
60
+ clearInterval(this.tickTimer);
61
+ this.tickTimer = null;
62
+ }
63
+ this.config.input.disconnect();
64
+ this.config.output.close();
65
+ console.log(' \u25C8 Receiver stopped');
66
+ }
67
+ bindInput() {
68
+ const input = this.config.input;
69
+ input.on('connected', () => {
70
+ this._status = 'connected';
71
+ this.lastDataAt = Date.now();
72
+ console.log(' \u25C8 Input connected');
73
+ });
74
+ input.on('state', (upstream) => {
75
+ this.lastDataAt = Date.now();
76
+ applyUpstreamState(this.grid, upstream);
77
+ if (this._fallbackActive) {
78
+ console.log('\n \u25C8 Signal restored \u2014 blending back from fallback');
79
+ this._fallbackActive = false;
80
+ }
81
+ });
82
+ input.on('disconnected', () => {
83
+ this._status = 'reconnecting';
84
+ console.log('\n \u25C8 Input disconnected');
85
+ });
86
+ input.connect();
87
+ }
88
+ startTickLoop() {
89
+ this.tickTimer = setInterval(() => {
90
+ this.tick++;
91
+ const now = Date.now();
92
+ const timeSinceData = now - this.lastDataAt;
93
+ // Check if we should switch to fallback
94
+ if (timeSinceData > this.config.fallbackDelay && !this._fallbackActive) {
95
+ this._fallbackActive = true;
96
+ this._status = 'fallback';
97
+ console.log('\n \u25C8 Signal lost \u2014 entering sine wave fallback');
98
+ }
99
+ // If fallback is active, compute sine wave targets
100
+ if (this._fallbackActive) {
101
+ computeFallbackFrame(this.grid, this.tick, this.config.fallback);
102
+ }
103
+ // Always tick the low-pass filter — this ensures smooth output
104
+ // whether receiving data, transitioning to fallback, or in fallback
105
+ tickFilter(this.grid, this.config.alpha);
106
+ // Send filtered output to the output adapter
107
+ this.config.output.send(this.getOutputState());
108
+ }, this.config.tickMs);
109
+ }
110
+ }
package/fallback.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * 3D sine wave fallback animations.
3
+ *
4
+ * When the upstream signal is lost, the receiver doesn't freeze or go dark.
5
+ * Instead it smoothly transitions into ambient sine wave patterns that sweep
6
+ * across the 7×7 grid in three dimensions (hue, brightness, and time).
7
+ *
8
+ * The waves create organic, slowly evolving color movement — like the
9
+ * installation is breathing on its own.
10
+ */
11
+ import { FilteredCannon } from './filter';
12
+ export interface FallbackConfig {
13
+ /** Base hue center for the wave (0–360). Default 220 (civic blue). */
14
+ baseHue: number;
15
+ /** Hue spread of the wave. Default 60. */
16
+ hueSpread: number;
17
+ /** Brightness min. Default 30. */
18
+ brightnessMin: number;
19
+ /** Brightness max. Default 85. */
20
+ brightnessMax: number;
21
+ /** Spatial frequency for the wave across the grid. Default 0.8. */
22
+ spatialFreq: number;
23
+ /** Time frequency — how fast the wave moves (radians per tick). Default 0.015. */
24
+ timeFreq: number;
25
+ /** Secondary wave time frequency for depth. Default 0.009. */
26
+ timeFreq2: number;
27
+ }
28
+ export declare const DEFAULT_FALLBACK_CONFIG: FallbackConfig;
29
+ /**
30
+ * Compute one frame of the 3D sine wave fallback and write targets
31
+ * into the filtered grid. The receiver's low-pass filter then smoothly
32
+ * converges the output to these targets.
33
+ *
34
+ * Three overlapping sine waves create organic movement:
35
+ * 1. Primary wave sweeps diagonally across the grid (hue)
36
+ * 2. Secondary wave moves perpendicular (brightness)
37
+ * 3. Tertiary slow wave modulates saturation for depth
38
+ */
39
+ export declare function computeFallbackFrame(grid: FilteredCannon[], tick: number, config?: FallbackConfig): void;
package/fallback.js ADDED
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /**
3
+ * 3D sine wave fallback animations.
4
+ *
5
+ * When the upstream signal is lost, the receiver doesn't freeze or go dark.
6
+ * Instead it smoothly transitions into ambient sine wave patterns that sweep
7
+ * across the 7×7 grid in three dimensions (hue, brightness, and time).
8
+ *
9
+ * The waves create organic, slowly evolving color movement — like the
10
+ * installation is breathing on its own.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.DEFAULT_FALLBACK_CONFIG = void 0;
14
+ exports.computeFallbackFrame = computeFallbackFrame;
15
+ const filter_1 = require("./filter");
16
+ exports.DEFAULT_FALLBACK_CONFIG = {
17
+ baseHue: 220,
18
+ hueSpread: 60,
19
+ brightnessMin: 30,
20
+ brightnessMax: 85,
21
+ spatialFreq: 0.8,
22
+ timeFreq: 0.015,
23
+ timeFreq2: 0.009
24
+ };
25
+ /**
26
+ * Compute one frame of the 3D sine wave fallback and write targets
27
+ * into the filtered grid. The receiver's low-pass filter then smoothly
28
+ * converges the output to these targets.
29
+ *
30
+ * Three overlapping sine waves create organic movement:
31
+ * 1. Primary wave sweeps diagonally across the grid (hue)
32
+ * 2. Secondary wave moves perpendicular (brightness)
33
+ * 3. Tertiary slow wave modulates saturation for depth
34
+ */
35
+ function computeFallbackFrame(grid, tick, config = exports.DEFAULT_FALLBACK_CONFIG) {
36
+ const { baseHue, hueSpread, brightnessMin, brightnessMax, spatialFreq, timeFreq, timeFreq2 } = config;
37
+ const brightRange = brightnessMax - brightnessMin;
38
+ for (let i = 0; i < filter_1.NUM_CANNONS; i++) {
39
+ const row = Math.floor(i / filter_1.GRID_SIZE);
40
+ const col = i % filter_1.GRID_SIZE;
41
+ // Normalize to -1..1
42
+ const nx = (col / (filter_1.GRID_SIZE - 1)) * 2 - 1;
43
+ const ny = (row / (filter_1.GRID_SIZE - 1)) * 2 - 1;
44
+ // Primary diagonal wave → hue
45
+ const wave1 = Math.sin((nx + ny) * spatialFreq * Math.PI + tick * timeFreq);
46
+ // Secondary perpendicular wave → brightness
47
+ const wave2 = Math.sin((nx - ny) * spatialFreq * Math.PI * 0.7 + tick * timeFreq2);
48
+ // Tertiary slow radial wave → saturation depth
49
+ const dist = Math.sqrt(nx * nx + ny * ny);
50
+ const wave3 = Math.sin(dist * Math.PI + tick * timeFreq * 0.4);
51
+ // Map to HSB
52
+ const h = (baseHue + wave1 * hueSpread + 360) % 360;
53
+ const s = 70 + wave3 * 20; // 50–90 range
54
+ const b = brightnessMin + ((wave2 + 1) / 2) * brightRange;
55
+ grid[i].targetH = h;
56
+ grid[i].targetS = Math.max(0, Math.min(100, s));
57
+ grid[i].targetB = Math.max(0, Math.min(100, b));
58
+ }
59
+ }
package/filter.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Independent low-pass filter for the receiver.
3
+ *
4
+ * This is separate from the simulator's filter — the receiver runs its own
5
+ * smoothing so that even if the upstream connection drops mid-transition,
6
+ * the output to hardware never jolts.
7
+ */
8
+ export interface CannonState {
9
+ h: number;
10
+ s: number;
11
+ b: number;
12
+ }
13
+ export interface FilteredCannon extends CannonState {
14
+ targetH: number;
15
+ targetS: number;
16
+ targetB: number;
17
+ }
18
+ export declare const NUM_CANNONS = 49;
19
+ export declare const GRID_SIZE = 7;
20
+ export declare const DEFAULT_RECEIVER_ALPHA = 0.06;
21
+ export declare function createFilteredGrid(): FilteredCannon[];
22
+ /**
23
+ * Shortest angular distance on the hue circle.
24
+ */
25
+ export declare function angleDelta(from: number, to: number): number;
26
+ /**
27
+ * Single tick of exponential low-pass interpolation.
28
+ * Returns true if any cannon changed (i.e., output is still settling).
29
+ */
30
+ export declare function tickFilter(grid: FilteredCannon[], alpha?: number): boolean;
31
+ /**
32
+ * Set the target state for all cannons from an upstream state snapshot.
33
+ * The filter will smoothly converge to these values.
34
+ */
35
+ export declare function applyUpstreamState(grid: FilteredCannon[], upstream: CannonState[]): void;
package/filter.js ADDED
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /**
3
+ * Independent low-pass filter for the receiver.
4
+ *
5
+ * This is separate from the simulator's filter — the receiver runs its own
6
+ * smoothing so that even if the upstream connection drops mid-transition,
7
+ * the output to hardware never jolts.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.DEFAULT_RECEIVER_ALPHA = exports.GRID_SIZE = exports.NUM_CANNONS = void 0;
11
+ exports.createFilteredGrid = createFilteredGrid;
12
+ exports.angleDelta = angleDelta;
13
+ exports.tickFilter = tickFilter;
14
+ exports.applyUpstreamState = applyUpstreamState;
15
+ exports.NUM_CANNONS = 49;
16
+ exports.GRID_SIZE = 7;
17
+ exports.DEFAULT_RECEIVER_ALPHA = 0.06;
18
+ function createFilteredGrid() {
19
+ return Array.from({ length: exports.NUM_CANNONS }, () => ({
20
+ h: 220,
21
+ s: 90,
22
+ b: 80,
23
+ targetH: 220,
24
+ targetS: 90,
25
+ targetB: 80
26
+ }));
27
+ }
28
+ /**
29
+ * Shortest angular distance on the hue circle.
30
+ */
31
+ function angleDelta(from, to) {
32
+ return ((to - from + 540) % 360) - 180;
33
+ }
34
+ /**
35
+ * Single tick of exponential low-pass interpolation.
36
+ * Returns true if any cannon changed (i.e., output is still settling).
37
+ */
38
+ function tickFilter(grid, alpha = exports.DEFAULT_RECEIVER_ALPHA) {
39
+ let changed = false;
40
+ for (let i = 0; i < grid.length; i++) {
41
+ const c = grid[i];
42
+ const dh = angleDelta(c.h, c.targetH);
43
+ const ds = c.targetS - c.s;
44
+ const db = c.targetB - c.b;
45
+ if (Math.abs(dh) > 0.3 || Math.abs(ds) > 0.3 || Math.abs(db) > 0.3) {
46
+ c.h = (c.h + dh * alpha + 360) % 360;
47
+ c.s = c.s + ds * alpha;
48
+ c.b = c.b + db * alpha;
49
+ changed = true;
50
+ }
51
+ else {
52
+ c.h = c.targetH;
53
+ c.s = c.targetS;
54
+ c.b = c.targetB;
55
+ }
56
+ }
57
+ return changed;
58
+ }
59
+ /**
60
+ * Set the target state for all cannons from an upstream state snapshot.
61
+ * The filter will smoothly converge to these values.
62
+ */
63
+ function applyUpstreamState(grid, upstream) {
64
+ for (let i = 0; i < Math.min(grid.length, upstream.length); i++) {
65
+ grid[i].targetH = upstream[i].h;
66
+ grid[i].targetS = upstream[i].s;
67
+ grid[i].targetB = upstream[i].b;
68
+ }
69
+ }
package/index.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export type { AddressMapping, InputAdapter, MappedOutputConfig, OutputAdapter } from './adapters';
2
+ export { CallbackOutput, ConsoleOutput, MultiOutput, WebSocketInput, WebSocketOutput } from './adapters';
3
+ export type { CannonState, FilteredCannon } from './filter';
4
+ export { angleDelta, applyUpstreamState, createFilteredGrid, DEFAULT_RECEIVER_ALPHA, tickFilter } from './filter';
5
+ export type { FallbackConfig } from './fallback';
6
+ export { computeFallbackFrame, DEFAULT_FALLBACK_CONFIG } from './fallback';
7
+ export type { ReceiverConfig, ReceiverState, ReceiverStatus } from './receiver';
8
+ export { Receiver } from './receiver';
package/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Receiver = exports.DEFAULT_FALLBACK_CONFIG = exports.computeFallbackFrame = exports.tickFilter = exports.DEFAULT_RECEIVER_ALPHA = exports.createFilteredGrid = exports.applyUpstreamState = exports.angleDelta = exports.WebSocketOutput = exports.WebSocketInput = exports.MultiOutput = exports.ConsoleOutput = exports.CallbackOutput = void 0;
4
+ var adapters_1 = require("./adapters");
5
+ Object.defineProperty(exports, "CallbackOutput", { enumerable: true, get: function () { return adapters_1.CallbackOutput; } });
6
+ Object.defineProperty(exports, "ConsoleOutput", { enumerable: true, get: function () { return adapters_1.ConsoleOutput; } });
7
+ Object.defineProperty(exports, "MultiOutput", { enumerable: true, get: function () { return adapters_1.MultiOutput; } });
8
+ Object.defineProperty(exports, "WebSocketInput", { enumerable: true, get: function () { return adapters_1.WebSocketInput; } });
9
+ Object.defineProperty(exports, "WebSocketOutput", { enumerable: true, get: function () { return adapters_1.WebSocketOutput; } });
10
+ var filter_1 = require("./filter");
11
+ Object.defineProperty(exports, "angleDelta", { enumerable: true, get: function () { return filter_1.angleDelta; } });
12
+ Object.defineProperty(exports, "applyUpstreamState", { enumerable: true, get: function () { return filter_1.applyUpstreamState; } });
13
+ Object.defineProperty(exports, "createFilteredGrid", { enumerable: true, get: function () { return filter_1.createFilteredGrid; } });
14
+ Object.defineProperty(exports, "DEFAULT_RECEIVER_ALPHA", { enumerable: true, get: function () { return filter_1.DEFAULT_RECEIVER_ALPHA; } });
15
+ Object.defineProperty(exports, "tickFilter", { enumerable: true, get: function () { return filter_1.tickFilter; } });
16
+ var fallback_1 = require("./fallback");
17
+ Object.defineProperty(exports, "computeFallbackFrame", { enumerable: true, get: function () { return fallback_1.computeFallbackFrame; } });
18
+ Object.defineProperty(exports, "DEFAULT_FALLBACK_CONFIG", { enumerable: true, get: function () { return fallback_1.DEFAULT_FALLBACK_CONFIG; } });
19
+ var receiver_1 = require("./receiver");
20
+ Object.defineProperty(exports, "Receiver", { enumerable: true, get: function () { return receiver_1.Receiver; } });
package/main.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Receiver entry point.
3
+ *
4
+ * Demonstrates the adapter pattern — configure input and output
5
+ * adapters via environment variables, then start the receiver.
6
+ */
7
+ export {};
package/main.js ADDED
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * Receiver entry point.
4
+ *
5
+ * Demonstrates the adapter pattern — configure input and output
6
+ * adapters via environment variables, then start the receiver.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ const adapters_1 = require("./adapters");
10
+ const receiver_1 = require("./receiver");
11
+ const SIMULATOR_URL = process.env.SIMULATOR_URL || 'ws://localhost:3000';
12
+ const ALPHA = parseFloat(process.env.RECEIVER_ALPHA || '0.06');
13
+ const FALLBACK_DELAY = parseInt(process.env.FALLBACK_DELAY || '3000', 10);
14
+ const WS_OUTPUT_PORT = process.env.WS_OUTPUT_PORT ? parseInt(process.env.WS_OUTPUT_PORT, 10) : undefined;
15
+ // ─── Input adapter ───
16
+ const input = new adapters_1.WebSocketInput({ url: SIMULATOR_URL });
17
+ // ─── Output adapter(s) ───
18
+ const outputs = [new adapters_1.ConsoleOutput()];
19
+ let wsOutput = null;
20
+ if (WS_OUTPUT_PORT) {
21
+ wsOutput = new adapters_1.WebSocketOutput({ port: WS_OUTPUT_PORT });
22
+ wsOutput.listen();
23
+ outputs.push(wsOutput);
24
+ }
25
+ const output = outputs.length === 1 ? outputs[0] : new adapters_1.MultiOutput(outputs);
26
+ // ─── Receiver ───
27
+ const receiver = new receiver_1.Receiver({
28
+ input,
29
+ output,
30
+ alpha: ALPHA,
31
+ fallbackDelay: FALLBACK_DELAY
32
+ });
33
+ console.log('');
34
+ console.log(' \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E');
35
+ console.log(' \u2502 Illuminate \u00B7 Receiver \u2502');
36
+ console.log(' \u2502 the brain \u2502');
37
+ console.log(' \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F');
38
+ console.log('');
39
+ console.log(` \u2192 Input: WebSocket @ ${SIMULATOR_URL}`);
40
+ console.log(` \u2192 Output: Console${wsOutput ? ` + WebSocket :${WS_OUTPUT_PORT}` : ''}`);
41
+ console.log(` \u2192 Alpha: ${ALPHA} Fallback delay: ${FALLBACK_DELAY}ms`);
42
+ console.log('');
43
+ receiver.start();
44
+ // Graceful shutdown
45
+ process.on('SIGINT', () => {
46
+ console.log('\n\n Shutting down...');
47
+ receiver.stop();
48
+ process.exit(0);
49
+ });
50
+ process.on('SIGTERM', () => {
51
+ receiver.stop();
52
+ process.exit(0);
53
+ });
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "wavegrid",
3
+ "version": "0.1.1",
4
+ "author": "Dan Lynch <pyramation@gmail.com>",
5
+ "description": "Receiver brain — resilient state engine with independent low-pass filter, 3D sine wave fallback, and OSC bridge",
6
+ "main": "index.js",
7
+ "module": "esm/index.js",
8
+ "types": "index.d.ts",
9
+ "homepage": "https://github.com/constructive-io/Illuminate",
10
+ "license": "SEE LICENSE IN LICENSE",
11
+ "publishConfig": {
12
+ "access": "public",
13
+ "directory": "dist"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/constructive-io/Illuminate"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/constructive-io/Illuminate/issues"
21
+ },
22
+ "scripts": {
23
+ "clean": "makage clean",
24
+ "prepack": "npm run build",
25
+ "build": "makage build",
26
+ "build:dev": "makage build --dev",
27
+ "lint": "ESLINT_USE_FLAT_CONFIG=false eslint src --fix",
28
+ "test": "jest --passWithNoTests",
29
+ "test:watch": "jest --watch",
30
+ "dev": "ts-node src/main.ts"
31
+ },
32
+ "keywords": [
33
+ "laser",
34
+ "osc",
35
+ "receiver",
36
+ "brain",
37
+ "7x7"
38
+ ],
39
+ "dependencies": {
40
+ "ws": "^8.18.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/ws": "^8.5.13",
44
+ "makage": "^0.3.0"
45
+ },
46
+ "gitHead": "6f651b8d0dfbb6538c667dd3905ce6989ae18e46"
47
+ }
package/receiver.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The Receiver — the brain of the Illuminate installation.
3
+ *
4
+ * A pure state engine that:
5
+ * 1. Receives grid state from an InputAdapter (upstream source)
6
+ * 2. Runs an independent low-pass filter (smooth, never jolts)
7
+ * 3. Falls back to 3D sine waves on signal loss
8
+ * 4. Sends filtered output to an OutputAdapter (hardware target)
9
+ *
10
+ * Both input and output are pluggable adapters — swap them to connect
11
+ * to any protocol or hardware without modifying the receiver core.
12
+ */
13
+ import { InputAdapter, OutputAdapter } from './adapters';
14
+ import { FallbackConfig } from './fallback';
15
+ import { CannonState, FilteredCannon } from './filter';
16
+ export interface ReceiverConfig {
17
+ /** Input adapter — where state comes from. */
18
+ input: InputAdapter;
19
+ /** Output adapter — where filtered state goes. */
20
+ output: OutputAdapter;
21
+ /** Low-pass filter alpha (lower = smoother). Default 0.06. */
22
+ alpha: number;
23
+ /** Ms of no data before switching to fallback. Default 3000. */
24
+ fallbackDelay: number;
25
+ /** Fallback animation config. */
26
+ fallback: FallbackConfig;
27
+ /** Tick rate in ms (default 1000/60 ~ 16.67ms). */
28
+ tickMs: number;
29
+ }
30
+ export declare const DEFAULT_RECEIVER_CONFIG: ReceiverConfig;
31
+ export type ReceiverStatus = 'connected' | 'reconnecting' | 'fallback';
32
+ export interface ReceiverState {
33
+ status: ReceiverStatus;
34
+ grid: FilteredCannon[];
35
+ tick: number;
36
+ lastDataAt: number;
37
+ fallbackActive: boolean;
38
+ }
39
+ export declare class Receiver {
40
+ private config;
41
+ private grid;
42
+ private tickTimer;
43
+ private tick;
44
+ private lastDataAt;
45
+ private _status;
46
+ private _fallbackActive;
47
+ private _running;
48
+ constructor(config?: Partial<ReceiverConfig>);
49
+ get status(): ReceiverStatus;
50
+ get fallbackActive(): boolean;
51
+ /** Get the current output state (after filtering). */
52
+ getOutputState(): CannonState[];
53
+ /** Start the receiver — connects input and begins the tick loop. */
54
+ start(): void;
55
+ /** Stop the receiver — disconnects and stops the tick loop. */
56
+ stop(): void;
57
+ private bindInput;
58
+ private startTickLoop;
59
+ }