wavegrid 0.2.0 → 0.3.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 +18 -5
- package/README.md +47 -15
- package/esm/fallback.js +10 -7
- package/esm/filter.js +8 -4
- package/esm/index.js +1 -1
- package/esm/main.js +77 -10
- package/esm/receiver.js +12 -6
- package/fallback.d.ts +1 -1
- package/fallback.js +9 -6
- package/filter.d.ts +7 -3
- package/filter.js +9 -5
- package/index.d.ts +2 -2
- package/index.js +3 -1
- package/main.d.ts +12 -2
- package/main.js +110 -10
- package/package.json +5 -5
- package/receiver.d.ts +18 -1
- package/receiver.js +11 -5
package/LICENSE
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
The San Francisco License (SF License)
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Interweb, Inc.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Permission is granted, free of charge, to any person or organization
|
|
6
|
+
obtaining a copy of this software and associated documentation files
|
|
7
|
+
(the "Software"), to use, copy, modify, merge, publish, distribute,
|
|
8
|
+
sublicense, and/or sell copies of the Software, and to permit others
|
|
9
|
+
to whom the Software is furnished to do the same.
|
|
10
|
+
|
|
11
|
+
The only requirement is that this license notice and copyright notice
|
|
12
|
+
shall be included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
18
|
+
CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
19
|
+
TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE
|
|
20
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -10,22 +10,23 @@
|
|
|
10
10
|
</a>
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
|
-
The **brain** of the
|
|
13
|
+
The **brain** of the Wavegrid installation. Sits between the control layer (Canvas/Simulator) and the physical hardware (BEYOND/OSC via `@wavegrid/osc`).
|
|
14
14
|
|
|
15
15
|
## Design Principles
|
|
16
16
|
|
|
17
|
-
1. **Never jolt** — runs its own independent low-pass filter on all incoming state
|
|
18
|
-
2. **Always alive** — on signal loss, gracefully falls back to ambient 3D sine wave animations
|
|
19
|
-
3. **
|
|
17
|
+
1. **Never jolt** — runs its own independent low-pass filter on all incoming state
|
|
18
|
+
2. **Always alive** — on signal loss, gracefully falls back to ambient 3D sine wave animations
|
|
19
|
+
3. **Pluggable** — input and output are adapters; swap them for any protocol or hardware
|
|
20
|
+
4. **Lean** — no hardware dependencies in the core; OSC lives in `@wavegrid/osc`
|
|
20
21
|
|
|
21
22
|
## Architecture
|
|
22
23
|
|
|
23
24
|
```
|
|
24
|
-
Canvas ──ws──▶ Simulator ──ws──▶ Receiver ──
|
|
25
|
+
Canvas ──ws──▶ Simulator ──ws──▶ Receiver ──adapter──▶ Hardware
|
|
25
26
|
│
|
|
26
27
|
own LP filter
|
|
27
28
|
sine fallback
|
|
28
|
-
|
|
29
|
+
shard support
|
|
29
30
|
```
|
|
30
31
|
|
|
31
32
|
## Usage
|
|
@@ -33,16 +34,29 @@ Canvas ──ws──▶ Simulator ──ws──▶ Receiver ──osc──▶
|
|
|
33
34
|
```sh
|
|
34
35
|
pnpm dev:receiver
|
|
35
36
|
# Connects to simulator at ws://localhost:3000
|
|
36
|
-
# Outputs state to console (or
|
|
37
|
+
# Outputs state to console (or hardware when configured)
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
### With OSC hardware (requires `@wavegrid/osc`)
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
```sh
|
|
43
|
+
ROUTING_CONFIG=./routing.json pnpm dev:receiver
|
|
44
|
+
BEYOND_HOST=192.168.50.10 pnpm dev:receiver
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Programmatic
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { Receiver, WebSocketInput, ConsoleOutput } from 'wavegrid';
|
|
51
|
+
|
|
52
|
+
const receiver = new Receiver({
|
|
53
|
+
input: new WebSocketInput({ url: 'ws://localhost:3000' }),
|
|
54
|
+
output: new ConsoleOutput(),
|
|
55
|
+
numCannons: 49,
|
|
56
|
+
gridColumns: 7
|
|
57
|
+
});
|
|
58
|
+
receiver.start();
|
|
59
|
+
```
|
|
46
60
|
|
|
47
61
|
## Configuration
|
|
48
62
|
|
|
@@ -51,5 +65,23 @@ When the upstream WebSocket connection drops:
|
|
|
51
65
|
| `SIMULATOR_URL` | `ws://localhost:3000` | Upstream WebSocket |
|
|
52
66
|
| `RECEIVER_ALPHA` | `0.06` | Low-pass filter smoothing (lower = smoother) |
|
|
53
67
|
| `FALLBACK_DELAY` | `3000` | ms before switching to sine fallback |
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
68
|
+
| `WS_OUTPUT_PORT` | — | Optional WebSocket relay output port |
|
|
69
|
+
| `SHARD_START` | — | First cannon index (inclusive) |
|
|
70
|
+
| `SHARD_END` | — | Last cannon index (inclusive) |
|
|
71
|
+
| `NUM_CANNONS` | `49` | Total cannons in the grid |
|
|
72
|
+
| `GRID_COLUMNS` | `7` | Number of columns in the grid |
|
|
73
|
+
| `ROUTING_CONFIG` | — | Path to JSON routing config (enables OSC) |
|
|
74
|
+
| `BEYOND_HOST` | — | Quick single-target BEYOND OSC host |
|
|
75
|
+
| `BEYOND_PORT` | `9000` | BEYOND OSC port |
|
|
76
|
+
| `FB4_HOST` | — | Quick single-target FB4 OSC host |
|
|
77
|
+
| `FB4_PORT` | `8000` | FB4 OSC port |
|
|
78
|
+
|
|
79
|
+
## Built-in Adapters
|
|
80
|
+
|
|
81
|
+
| Adapter | Direction | Purpose |
|
|
82
|
+
|---------|-----------|---------|
|
|
83
|
+
| `WebSocketInput` | Input | Connects to upstream simulator/server |
|
|
84
|
+
| `ConsoleOutput` | Output | Logs frames to console (dev/debug) |
|
|
85
|
+
| `CallbackOutput` | Output | Calls your function each tick |
|
|
86
|
+
| `MultiOutput` | Output | Fans out to N adapters at once |
|
|
87
|
+
| `WebSocketOutput` | Output | Broadcasts to downstream WS clients |
|
package/esm/fallback.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* The waves create organic, slowly evolving color movement — like the
|
|
9
9
|
* installation is breathing on its own.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { DEFAULT_GRID_COLUMNS } from './filter';
|
|
12
12
|
export const DEFAULT_FALLBACK_CONFIG = {
|
|
13
13
|
baseHue: 220,
|
|
14
14
|
hueSpread: 60,
|
|
@@ -28,15 +28,18 @@ export const DEFAULT_FALLBACK_CONFIG = {
|
|
|
28
28
|
* 2. Secondary wave moves perpendicular (brightness)
|
|
29
29
|
* 3. Tertiary slow wave modulates saturation for depth
|
|
30
30
|
*/
|
|
31
|
-
export function computeFallbackFrame(grid, tick, config = DEFAULT_FALLBACK_CONFIG) {
|
|
31
|
+
export function computeFallbackFrame(grid, tick, config = DEFAULT_FALLBACK_CONFIG, gridColumns = DEFAULT_GRID_COLUMNS) {
|
|
32
32
|
const { baseHue, hueSpread, brightnessMin, brightnessMax, spatialFreq, timeFreq, timeFreq2 } = config;
|
|
33
33
|
const brightRange = brightnessMax - brightnessMin;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
34
|
+
const cols = Math.max(1, gridColumns);
|
|
35
|
+
for (let i = 0; i < grid.length; i++) {
|
|
36
|
+
const row = Math.floor(i / cols);
|
|
37
|
+
const col = i % cols;
|
|
38
|
+
const maxCol = Math.max(1, cols - 1);
|
|
39
|
+
const maxRow = Math.max(1, Math.ceil(grid.length / cols) - 1);
|
|
37
40
|
// Normalize to -1..1
|
|
38
|
-
const nx = (col /
|
|
39
|
-
const ny = (row /
|
|
41
|
+
const nx = (col / maxCol) * 2 - 1;
|
|
42
|
+
const ny = (row / maxRow) * 2 - 1;
|
|
40
43
|
// Primary diagonal wave → hue
|
|
41
44
|
const wave1 = Math.sin((nx + ny) * spatialFreq * Math.PI + tick * timeFreq);
|
|
42
45
|
// Secondary perpendicular wave → brightness
|
package/esm/filter.js
CHANGED
|
@@ -5,11 +5,15 @@
|
|
|
5
5
|
* smoothing so that even if the upstream connection drops mid-transition,
|
|
6
6
|
* the output to hardware never jolts.
|
|
7
7
|
*/
|
|
8
|
-
export const
|
|
9
|
-
export const
|
|
8
|
+
export const DEFAULT_NUM_CANNONS = 49;
|
|
9
|
+
export const DEFAULT_GRID_COLUMNS = 7;
|
|
10
10
|
export const DEFAULT_RECEIVER_ALPHA = 0.06;
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Create a filtered grid of the given size.
|
|
13
|
+
* Defaults to 49 cannons for the 7×7 Civic Center installation.
|
|
14
|
+
*/
|
|
15
|
+
export function createFilteredGrid(numCannons = DEFAULT_NUM_CANNONS) {
|
|
16
|
+
return Array.from({ length: numCannons }, () => ({
|
|
13
17
|
h: 220,
|
|
14
18
|
s: 90,
|
|
15
19
|
b: 80,
|
package/esm/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { CallbackOutput, ConsoleOutput, MultiOutput, WebSocketInput, WebSocketOutput } from './adapters';
|
|
2
|
-
export { angleDelta, applyUpstreamState, createFilteredGrid, DEFAULT_RECEIVER_ALPHA, tickFilter } from './filter';
|
|
2
|
+
export { angleDelta, applyUpstreamState, createFilteredGrid, DEFAULT_GRID_COLUMNS, DEFAULT_NUM_CANNONS, DEFAULT_RECEIVER_ALPHA, tickFilter } from './filter';
|
|
3
3
|
export { computeFallbackFrame, DEFAULT_FALLBACK_CONFIG } from './fallback';
|
|
4
4
|
export { Receiver } from './receiver';
|
package/esm/main.js
CHANGED
|
@@ -1,24 +1,86 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Receiver entry point.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Configure input and output adapters via environment variables:
|
|
5
|
+
*
|
|
6
|
+
* SIMULATOR_URL WebSocket upstream (default ws://localhost:3000)
|
|
7
|
+
* RECEIVER_ALPHA LP filter alpha (default 0.06)
|
|
8
|
+
* FALLBACK_DELAY Ms before sine fallback (default 3000)
|
|
9
|
+
* WS_OUTPUT_PORT Optional WebSocket relay port
|
|
10
|
+
* SHARD_START/END Optional cannon index range
|
|
11
|
+
* NUM_CANNONS Total cannons in grid (default 49)
|
|
12
|
+
* GRID_COLUMNS Number of columns (default 7)
|
|
13
|
+
* ROUTING_CONFIG Path to a JSON routing config file (enables OSC output)
|
|
14
|
+
* BEYOND_HOST/PORT Quick single-target BEYOND OSC (alternative to routing file)
|
|
15
|
+
* FB4_HOST/PORT Quick single-target FB4 OSC (alternative to routing file)
|
|
6
16
|
*/
|
|
17
|
+
import * as fs from 'fs';
|
|
7
18
|
import { ConsoleOutput, MultiOutput, WebSocketInput, WebSocketOutput } from './adapters';
|
|
19
|
+
import { DEFAULT_GRID_COLUMNS, DEFAULT_NUM_CANNONS } from './filter';
|
|
8
20
|
import { Receiver } from './receiver';
|
|
9
21
|
const SIMULATOR_URL = process.env.SIMULATOR_URL || 'ws://localhost:3000';
|
|
10
22
|
const ALPHA = parseFloat(process.env.RECEIVER_ALPHA || '0.06');
|
|
11
23
|
const FALLBACK_DELAY = parseInt(process.env.FALLBACK_DELAY || '3000', 10);
|
|
12
24
|
const WS_OUTPUT_PORT = process.env.WS_OUTPUT_PORT ? parseInt(process.env.WS_OUTPUT_PORT, 10) : undefined;
|
|
25
|
+
const NUM_CANNONS = process.env.NUM_CANNONS ? parseInt(process.env.NUM_CANNONS, 10) : DEFAULT_NUM_CANNONS;
|
|
26
|
+
const GRID_COLUMNS = process.env.GRID_COLUMNS ? parseInt(process.env.GRID_COLUMNS, 10) : DEFAULT_GRID_COLUMNS;
|
|
27
|
+
let shard;
|
|
28
|
+
if (process.env.SHARD_START !== undefined && process.env.SHARD_END !== undefined) {
|
|
29
|
+
shard = {
|
|
30
|
+
start: parseInt(process.env.SHARD_START, 10),
|
|
31
|
+
end: parseInt(process.env.SHARD_END, 10)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
13
34
|
// ─── Input adapter ───
|
|
14
35
|
const input = new WebSocketInput({ url: SIMULATOR_URL });
|
|
15
36
|
// ─── Output adapter(s) ───
|
|
16
37
|
const outputs = [new ConsoleOutput()];
|
|
38
|
+
const outputLabels = ['Console'];
|
|
39
|
+
// OSC adapters are in @wavegrid/osc — try to load them if env vars are set
|
|
40
|
+
const hasOscConfig = process.env.ROUTING_CONFIG || process.env.BEYOND_HOST || process.env.FB4_HOST;
|
|
41
|
+
if (hasOscConfig) {
|
|
42
|
+
try {
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
44
|
+
const osc = require('@wavegrid/osc');
|
|
45
|
+
if (process.env.ROUTING_CONFIG) {
|
|
46
|
+
const raw = fs.readFileSync(process.env.ROUTING_CONFIG, 'utf8');
|
|
47
|
+
const routingConfig = JSON.parse(raw);
|
|
48
|
+
const routed = osc.createRoutedOutput(routingConfig);
|
|
49
|
+
routed.connect();
|
|
50
|
+
outputs.push(routed);
|
|
51
|
+
outputLabels.push(`Routed OSC → [${routed.targetNames.join(', ')}]`);
|
|
52
|
+
}
|
|
53
|
+
if (process.env.BEYOND_HOST) {
|
|
54
|
+
const host = process.env.BEYOND_HOST;
|
|
55
|
+
const port = parseInt(process.env.BEYOND_PORT || '9000', 10);
|
|
56
|
+
const projectorMap = {};
|
|
57
|
+
for (let i = 0; i < NUM_CANNONS; i++)
|
|
58
|
+
projectorMap[i] = i;
|
|
59
|
+
const beyond = new osc.BeyondOscOutput({ host, port, projectorMap });
|
|
60
|
+
beyond.connect();
|
|
61
|
+
outputs.push(beyond);
|
|
62
|
+
outputLabels.push(`BEYOND OSC → ${host}:${port}`);
|
|
63
|
+
}
|
|
64
|
+
if (process.env.FB4_HOST) {
|
|
65
|
+
const host = process.env.FB4_HOST;
|
|
66
|
+
const port = parseInt(process.env.FB4_PORT || '8000', 10);
|
|
67
|
+
console.warn(' ⚠ FB4_HOST set but no serial map — use ROUTING_CONFIG for per-cannon FB4 mapping');
|
|
68
|
+
const fb4 = new osc.FB4OscOutput({ host, port, serialMap: {} });
|
|
69
|
+
fb4.connect();
|
|
70
|
+
outputs.push(fb4);
|
|
71
|
+
outputLabels.push(`FB4 OSC → ${host}:${port}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
console.warn(' ⚠ OSC env vars set but @wavegrid/osc is not installed. Run: pnpm add @wavegrid/osc');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
17
78
|
let wsOutput = null;
|
|
18
79
|
if (WS_OUTPUT_PORT) {
|
|
19
80
|
wsOutput = new WebSocketOutput({ port: WS_OUTPUT_PORT });
|
|
20
81
|
wsOutput.listen();
|
|
21
82
|
outputs.push(wsOutput);
|
|
83
|
+
outputLabels.push(`WebSocket :${WS_OUTPUT_PORT}`);
|
|
22
84
|
}
|
|
23
85
|
const output = outputs.length === 1 ? outputs[0] : new MultiOutput(outputs);
|
|
24
86
|
// ─── Receiver ───
|
|
@@ -26,17 +88,22 @@ const receiver = new Receiver({
|
|
|
26
88
|
input,
|
|
27
89
|
output,
|
|
28
90
|
alpha: ALPHA,
|
|
29
|
-
fallbackDelay: FALLBACK_DELAY
|
|
91
|
+
fallbackDelay: FALLBACK_DELAY,
|
|
92
|
+
shard,
|
|
93
|
+
numCannons: NUM_CANNONS,
|
|
94
|
+
gridColumns: GRID_COLUMNS
|
|
30
95
|
});
|
|
31
96
|
console.log('');
|
|
32
|
-
console.log('
|
|
33
|
-
console.log('
|
|
34
|
-
console.log('
|
|
35
|
-
console.log('
|
|
97
|
+
console.log(' ╭──────────────────────────────────────╮');
|
|
98
|
+
console.log(' │ Wavegrid · Receiver │');
|
|
99
|
+
console.log(' │ the brain │');
|
|
100
|
+
console.log(' ╰──────────────────────────────────────╯');
|
|
36
101
|
console.log('');
|
|
37
|
-
console.log(`
|
|
38
|
-
console.log(`
|
|
39
|
-
console.log(`
|
|
102
|
+
console.log(` → Input: WebSocket @ ${SIMULATOR_URL}`);
|
|
103
|
+
console.log(` → Output: ${outputLabels.join(' + ')}`);
|
|
104
|
+
console.log(` → Alpha: ${ALPHA} Fallback delay: ${FALLBACK_DELAY}ms`);
|
|
105
|
+
console.log(` → Grid: ${NUM_CANNONS} cannons (${GRID_COLUMNS} columns)`);
|
|
106
|
+
console.log(` → Shard: ${shard ? `cannons ${shard.start}–${shard.end} (${shard.end - shard.start + 1} of ${NUM_CANNONS})` : `all cannons (no shard)`}`);
|
|
40
107
|
console.log('');
|
|
41
108
|
receiver.start();
|
|
42
109
|
// Graceful shutdown
|
package/esm/receiver.js
CHANGED
|
@@ -12,14 +12,16 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { ConsoleOutput, WebSocketInput } from './adapters';
|
|
14
14
|
import { computeFallbackFrame, DEFAULT_FALLBACK_CONFIG } from './fallback';
|
|
15
|
-
import { applyUpstreamState, createFilteredGrid, DEFAULT_RECEIVER_ALPHA, tickFilter } from './filter';
|
|
15
|
+
import { applyUpstreamState, createFilteredGrid, DEFAULT_GRID_COLUMNS, DEFAULT_NUM_CANNONS, DEFAULT_RECEIVER_ALPHA, tickFilter } from './filter';
|
|
16
16
|
export const DEFAULT_RECEIVER_CONFIG = {
|
|
17
17
|
input: new WebSocketInput({ url: 'ws://localhost:3000' }),
|
|
18
18
|
output: new ConsoleOutput(),
|
|
19
19
|
alpha: DEFAULT_RECEIVER_ALPHA,
|
|
20
20
|
fallbackDelay: 3000,
|
|
21
21
|
fallback: DEFAULT_FALLBACK_CONFIG,
|
|
22
|
-
tickMs: 1000 / 60
|
|
22
|
+
tickMs: 1000 / 60,
|
|
23
|
+
numCannons: DEFAULT_NUM_CANNONS,
|
|
24
|
+
gridColumns: DEFAULT_GRID_COLUMNS
|
|
23
25
|
};
|
|
24
26
|
export class Receiver {
|
|
25
27
|
config;
|
|
@@ -32,17 +34,21 @@ export class Receiver {
|
|
|
32
34
|
_running = false;
|
|
33
35
|
constructor(config = {}) {
|
|
34
36
|
this.config = { ...DEFAULT_RECEIVER_CONFIG, ...config };
|
|
35
|
-
this.grid = createFilteredGrid();
|
|
37
|
+
this.grid = createFilteredGrid(this.config.numCannons);
|
|
36
38
|
}
|
|
37
39
|
get status() { return this._status; }
|
|
38
40
|
get fallbackActive() { return this._fallbackActive; }
|
|
39
|
-
/** Get the current output state (after filtering). */
|
|
41
|
+
/** Get the current output state (after filtering and sharding). */
|
|
40
42
|
getOutputState() {
|
|
41
|
-
|
|
43
|
+
const full = this.grid.map(c => ({
|
|
42
44
|
h: c.h,
|
|
43
45
|
s: c.s,
|
|
44
46
|
b: c.b
|
|
45
47
|
}));
|
|
48
|
+
const shard = this.config.shard;
|
|
49
|
+
if (!shard)
|
|
50
|
+
return full;
|
|
51
|
+
return full.slice(shard.start, shard.end + 1);
|
|
46
52
|
}
|
|
47
53
|
/** Start the receiver — connects input and begins the tick loop. */
|
|
48
54
|
start() {
|
|
@@ -98,7 +104,7 @@ export class Receiver {
|
|
|
98
104
|
}
|
|
99
105
|
// If fallback is active, compute sine wave targets
|
|
100
106
|
if (this._fallbackActive) {
|
|
101
|
-
computeFallbackFrame(this.grid, this.tick, this.config.fallback);
|
|
107
|
+
computeFallbackFrame(this.grid, this.tick, this.config.fallback, this.config.gridColumns);
|
|
102
108
|
}
|
|
103
109
|
// Always tick the low-pass filter — this ensures smooth output
|
|
104
110
|
// whether receiving data, transitioning to fallback, or in fallback
|
package/fallback.d.ts
CHANGED
|
@@ -36,4 +36,4 @@ export declare const DEFAULT_FALLBACK_CONFIG: FallbackConfig;
|
|
|
36
36
|
* 2. Secondary wave moves perpendicular (brightness)
|
|
37
37
|
* 3. Tertiary slow wave modulates saturation for depth
|
|
38
38
|
*/
|
|
39
|
-
export declare function computeFallbackFrame(grid: FilteredCannon[], tick: number, config?: FallbackConfig): void;
|
|
39
|
+
export declare function computeFallbackFrame(grid: FilteredCannon[], tick: number, config?: FallbackConfig, gridColumns?: number): void;
|
package/fallback.js
CHANGED
|
@@ -32,15 +32,18 @@ exports.DEFAULT_FALLBACK_CONFIG = {
|
|
|
32
32
|
* 2. Secondary wave moves perpendicular (brightness)
|
|
33
33
|
* 3. Tertiary slow wave modulates saturation for depth
|
|
34
34
|
*/
|
|
35
|
-
function computeFallbackFrame(grid, tick, config = exports.DEFAULT_FALLBACK_CONFIG) {
|
|
35
|
+
function computeFallbackFrame(grid, tick, config = exports.DEFAULT_FALLBACK_CONFIG, gridColumns = filter_1.DEFAULT_GRID_COLUMNS) {
|
|
36
36
|
const { baseHue, hueSpread, brightnessMin, brightnessMax, spatialFreq, timeFreq, timeFreq2 } = config;
|
|
37
37
|
const brightRange = brightnessMax - brightnessMin;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
38
|
+
const cols = Math.max(1, gridColumns);
|
|
39
|
+
for (let i = 0; i < grid.length; i++) {
|
|
40
|
+
const row = Math.floor(i / cols);
|
|
41
|
+
const col = i % cols;
|
|
42
|
+
const maxCol = Math.max(1, cols - 1);
|
|
43
|
+
const maxRow = Math.max(1, Math.ceil(grid.length / cols) - 1);
|
|
41
44
|
// Normalize to -1..1
|
|
42
|
-
const nx = (col /
|
|
43
|
-
const ny = (row /
|
|
45
|
+
const nx = (col / maxCol) * 2 - 1;
|
|
46
|
+
const ny = (row / maxRow) * 2 - 1;
|
|
44
47
|
// Primary diagonal wave → hue
|
|
45
48
|
const wave1 = Math.sin((nx + ny) * spatialFreq * Math.PI + tick * timeFreq);
|
|
46
49
|
// Secondary perpendicular wave → brightness
|
package/filter.d.ts
CHANGED
|
@@ -15,10 +15,14 @@ export interface FilteredCannon extends CannonState {
|
|
|
15
15
|
targetS: number;
|
|
16
16
|
targetB: number;
|
|
17
17
|
}
|
|
18
|
-
export declare const
|
|
19
|
-
export declare const
|
|
18
|
+
export declare const DEFAULT_NUM_CANNONS = 49;
|
|
19
|
+
export declare const DEFAULT_GRID_COLUMNS = 7;
|
|
20
20
|
export declare const DEFAULT_RECEIVER_ALPHA = 0.06;
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Create a filtered grid of the given size.
|
|
23
|
+
* Defaults to 49 cannons for the 7×7 Civic Center installation.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createFilteredGrid(numCannons?: number): FilteredCannon[];
|
|
22
26
|
/**
|
|
23
27
|
* Shortest angular distance on the hue circle.
|
|
24
28
|
*/
|
package/filter.js
CHANGED
|
@@ -7,16 +7,20 @@
|
|
|
7
7
|
* the output to hardware never jolts.
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.DEFAULT_RECEIVER_ALPHA = exports.
|
|
10
|
+
exports.DEFAULT_RECEIVER_ALPHA = exports.DEFAULT_GRID_COLUMNS = exports.DEFAULT_NUM_CANNONS = void 0;
|
|
11
11
|
exports.createFilteredGrid = createFilteredGrid;
|
|
12
12
|
exports.angleDelta = angleDelta;
|
|
13
13
|
exports.tickFilter = tickFilter;
|
|
14
14
|
exports.applyUpstreamState = applyUpstreamState;
|
|
15
|
-
exports.
|
|
16
|
-
exports.
|
|
15
|
+
exports.DEFAULT_NUM_CANNONS = 49;
|
|
16
|
+
exports.DEFAULT_GRID_COLUMNS = 7;
|
|
17
17
|
exports.DEFAULT_RECEIVER_ALPHA = 0.06;
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Create a filtered grid of the given size.
|
|
20
|
+
* Defaults to 49 cannons for the 7×7 Civic Center installation.
|
|
21
|
+
*/
|
|
22
|
+
function createFilteredGrid(numCannons = exports.DEFAULT_NUM_CANNONS) {
|
|
23
|
+
return Array.from({ length: numCannons }, () => ({
|
|
20
24
|
h: 220,
|
|
21
25
|
s: 90,
|
|
22
26
|
b: 80,
|
package/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export type { AddressMapping, InputAdapter, MappedOutputConfig, OutputAdapter } from './adapters';
|
|
2
2
|
export { CallbackOutput, ConsoleOutput, MultiOutput, WebSocketInput, WebSocketOutput } from './adapters';
|
|
3
3
|
export type { CannonState, FilteredCannon } from './filter';
|
|
4
|
-
export { angleDelta, applyUpstreamState, createFilteredGrid, DEFAULT_RECEIVER_ALPHA, tickFilter } from './filter';
|
|
4
|
+
export { angleDelta, applyUpstreamState, createFilteredGrid, DEFAULT_GRID_COLUMNS, DEFAULT_NUM_CANNONS, DEFAULT_RECEIVER_ALPHA, tickFilter } from './filter';
|
|
5
5
|
export type { FallbackConfig } from './fallback';
|
|
6
6
|
export { computeFallbackFrame, DEFAULT_FALLBACK_CONFIG } from './fallback';
|
|
7
|
-
export type { ReceiverConfig, ReceiverState, ReceiverStatus } from './receiver';
|
|
7
|
+
export type { ReceiverConfig, ReceiverState, ReceiverStatus, ShardConfig } from './receiver';
|
|
8
8
|
export { Receiver } from './receiver';
|
package/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
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;
|
|
3
|
+
exports.Receiver = exports.DEFAULT_FALLBACK_CONFIG = exports.computeFallbackFrame = exports.tickFilter = exports.DEFAULT_RECEIVER_ALPHA = exports.DEFAULT_NUM_CANNONS = exports.DEFAULT_GRID_COLUMNS = exports.createFilteredGrid = exports.applyUpstreamState = exports.angleDelta = exports.WebSocketOutput = exports.WebSocketInput = exports.MultiOutput = exports.ConsoleOutput = exports.CallbackOutput = void 0;
|
|
4
4
|
var adapters_1 = require("./adapters");
|
|
5
5
|
Object.defineProperty(exports, "CallbackOutput", { enumerable: true, get: function () { return adapters_1.CallbackOutput; } });
|
|
6
6
|
Object.defineProperty(exports, "ConsoleOutput", { enumerable: true, get: function () { return adapters_1.ConsoleOutput; } });
|
|
@@ -11,6 +11,8 @@ var filter_1 = require("./filter");
|
|
|
11
11
|
Object.defineProperty(exports, "angleDelta", { enumerable: true, get: function () { return filter_1.angleDelta; } });
|
|
12
12
|
Object.defineProperty(exports, "applyUpstreamState", { enumerable: true, get: function () { return filter_1.applyUpstreamState; } });
|
|
13
13
|
Object.defineProperty(exports, "createFilteredGrid", { enumerable: true, get: function () { return filter_1.createFilteredGrid; } });
|
|
14
|
+
Object.defineProperty(exports, "DEFAULT_GRID_COLUMNS", { enumerable: true, get: function () { return filter_1.DEFAULT_GRID_COLUMNS; } });
|
|
15
|
+
Object.defineProperty(exports, "DEFAULT_NUM_CANNONS", { enumerable: true, get: function () { return filter_1.DEFAULT_NUM_CANNONS; } });
|
|
14
16
|
Object.defineProperty(exports, "DEFAULT_RECEIVER_ALPHA", { enumerable: true, get: function () { return filter_1.DEFAULT_RECEIVER_ALPHA; } });
|
|
15
17
|
Object.defineProperty(exports, "tickFilter", { enumerable: true, get: function () { return filter_1.tickFilter; } });
|
|
16
18
|
var fallback_1 = require("./fallback");
|
package/main.d.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Receiver entry point.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Configure input and output adapters via environment variables:
|
|
5
|
+
*
|
|
6
|
+
* SIMULATOR_URL WebSocket upstream (default ws://localhost:3000)
|
|
7
|
+
* RECEIVER_ALPHA LP filter alpha (default 0.06)
|
|
8
|
+
* FALLBACK_DELAY Ms before sine fallback (default 3000)
|
|
9
|
+
* WS_OUTPUT_PORT Optional WebSocket relay port
|
|
10
|
+
* SHARD_START/END Optional cannon index range
|
|
11
|
+
* NUM_CANNONS Total cannons in grid (default 49)
|
|
12
|
+
* GRID_COLUMNS Number of columns (default 7)
|
|
13
|
+
* ROUTING_CONFIG Path to a JSON routing config file (enables OSC output)
|
|
14
|
+
* BEYOND_HOST/PORT Quick single-target BEYOND OSC (alternative to routing file)
|
|
15
|
+
* FB4_HOST/PORT Quick single-target FB4 OSC (alternative to routing file)
|
|
6
16
|
*/
|
|
7
17
|
export {};
|
package/main.js
CHANGED
|
@@ -2,25 +2,120 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Receiver entry point.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Configure input and output adapters via environment variables:
|
|
6
|
+
*
|
|
7
|
+
* SIMULATOR_URL WebSocket upstream (default ws://localhost:3000)
|
|
8
|
+
* RECEIVER_ALPHA LP filter alpha (default 0.06)
|
|
9
|
+
* FALLBACK_DELAY Ms before sine fallback (default 3000)
|
|
10
|
+
* WS_OUTPUT_PORT Optional WebSocket relay port
|
|
11
|
+
* SHARD_START/END Optional cannon index range
|
|
12
|
+
* NUM_CANNONS Total cannons in grid (default 49)
|
|
13
|
+
* GRID_COLUMNS Number of columns (default 7)
|
|
14
|
+
* ROUTING_CONFIG Path to a JSON routing config file (enables OSC output)
|
|
15
|
+
* BEYOND_HOST/PORT Quick single-target BEYOND OSC (alternative to routing file)
|
|
16
|
+
* FB4_HOST/PORT Quick single-target FB4 OSC (alternative to routing file)
|
|
7
17
|
*/
|
|
18
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
21
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
22
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(o, k2, desc);
|
|
25
|
+
}) : (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
o[k2] = m[k];
|
|
28
|
+
}));
|
|
29
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
30
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
31
|
+
}) : function(o, v) {
|
|
32
|
+
o["default"] = v;
|
|
33
|
+
});
|
|
34
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
35
|
+
var ownKeys = function(o) {
|
|
36
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
37
|
+
var ar = [];
|
|
38
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
39
|
+
return ar;
|
|
40
|
+
};
|
|
41
|
+
return ownKeys(o);
|
|
42
|
+
};
|
|
43
|
+
return function (mod) {
|
|
44
|
+
if (mod && mod.__esModule) return mod;
|
|
45
|
+
var result = {};
|
|
46
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
47
|
+
__setModuleDefault(result, mod);
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
8
51
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
const fs = __importStar(require("fs"));
|
|
9
53
|
const adapters_1 = require("./adapters");
|
|
54
|
+
const filter_1 = require("./filter");
|
|
10
55
|
const receiver_1 = require("./receiver");
|
|
11
56
|
const SIMULATOR_URL = process.env.SIMULATOR_URL || 'ws://localhost:3000';
|
|
12
57
|
const ALPHA = parseFloat(process.env.RECEIVER_ALPHA || '0.06');
|
|
13
58
|
const FALLBACK_DELAY = parseInt(process.env.FALLBACK_DELAY || '3000', 10);
|
|
14
59
|
const WS_OUTPUT_PORT = process.env.WS_OUTPUT_PORT ? parseInt(process.env.WS_OUTPUT_PORT, 10) : undefined;
|
|
60
|
+
const NUM_CANNONS = process.env.NUM_CANNONS ? parseInt(process.env.NUM_CANNONS, 10) : filter_1.DEFAULT_NUM_CANNONS;
|
|
61
|
+
const GRID_COLUMNS = process.env.GRID_COLUMNS ? parseInt(process.env.GRID_COLUMNS, 10) : filter_1.DEFAULT_GRID_COLUMNS;
|
|
62
|
+
let shard;
|
|
63
|
+
if (process.env.SHARD_START !== undefined && process.env.SHARD_END !== undefined) {
|
|
64
|
+
shard = {
|
|
65
|
+
start: parseInt(process.env.SHARD_START, 10),
|
|
66
|
+
end: parseInt(process.env.SHARD_END, 10)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
15
69
|
// ─── Input adapter ───
|
|
16
70
|
const input = new adapters_1.WebSocketInput({ url: SIMULATOR_URL });
|
|
17
71
|
// ─── Output adapter(s) ───
|
|
18
72
|
const outputs = [new adapters_1.ConsoleOutput()];
|
|
73
|
+
const outputLabels = ['Console'];
|
|
74
|
+
// OSC adapters are in @wavegrid/osc — try to load them if env vars are set
|
|
75
|
+
const hasOscConfig = process.env.ROUTING_CONFIG || process.env.BEYOND_HOST || process.env.FB4_HOST;
|
|
76
|
+
if (hasOscConfig) {
|
|
77
|
+
try {
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
79
|
+
const osc = require('@wavegrid/osc');
|
|
80
|
+
if (process.env.ROUTING_CONFIG) {
|
|
81
|
+
const raw = fs.readFileSync(process.env.ROUTING_CONFIG, 'utf8');
|
|
82
|
+
const routingConfig = JSON.parse(raw);
|
|
83
|
+
const routed = osc.createRoutedOutput(routingConfig);
|
|
84
|
+
routed.connect();
|
|
85
|
+
outputs.push(routed);
|
|
86
|
+
outputLabels.push(`Routed OSC → [${routed.targetNames.join(', ')}]`);
|
|
87
|
+
}
|
|
88
|
+
if (process.env.BEYOND_HOST) {
|
|
89
|
+
const host = process.env.BEYOND_HOST;
|
|
90
|
+
const port = parseInt(process.env.BEYOND_PORT || '9000', 10);
|
|
91
|
+
const projectorMap = {};
|
|
92
|
+
for (let i = 0; i < NUM_CANNONS; i++)
|
|
93
|
+
projectorMap[i] = i;
|
|
94
|
+
const beyond = new osc.BeyondOscOutput({ host, port, projectorMap });
|
|
95
|
+
beyond.connect();
|
|
96
|
+
outputs.push(beyond);
|
|
97
|
+
outputLabels.push(`BEYOND OSC → ${host}:${port}`);
|
|
98
|
+
}
|
|
99
|
+
if (process.env.FB4_HOST) {
|
|
100
|
+
const host = process.env.FB4_HOST;
|
|
101
|
+
const port = parseInt(process.env.FB4_PORT || '8000', 10);
|
|
102
|
+
console.warn(' ⚠ FB4_HOST set but no serial map — use ROUTING_CONFIG for per-cannon FB4 mapping');
|
|
103
|
+
const fb4 = new osc.FB4OscOutput({ host, port, serialMap: {} });
|
|
104
|
+
fb4.connect();
|
|
105
|
+
outputs.push(fb4);
|
|
106
|
+
outputLabels.push(`FB4 OSC → ${host}:${port}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
console.warn(' ⚠ OSC env vars set but @wavegrid/osc is not installed. Run: pnpm add @wavegrid/osc');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
19
113
|
let wsOutput = null;
|
|
20
114
|
if (WS_OUTPUT_PORT) {
|
|
21
115
|
wsOutput = new adapters_1.WebSocketOutput({ port: WS_OUTPUT_PORT });
|
|
22
116
|
wsOutput.listen();
|
|
23
117
|
outputs.push(wsOutput);
|
|
118
|
+
outputLabels.push(`WebSocket :${WS_OUTPUT_PORT}`);
|
|
24
119
|
}
|
|
25
120
|
const output = outputs.length === 1 ? outputs[0] : new adapters_1.MultiOutput(outputs);
|
|
26
121
|
// ─── Receiver ───
|
|
@@ -28,17 +123,22 @@ const receiver = new receiver_1.Receiver({
|
|
|
28
123
|
input,
|
|
29
124
|
output,
|
|
30
125
|
alpha: ALPHA,
|
|
31
|
-
fallbackDelay: FALLBACK_DELAY
|
|
126
|
+
fallbackDelay: FALLBACK_DELAY,
|
|
127
|
+
shard,
|
|
128
|
+
numCannons: NUM_CANNONS,
|
|
129
|
+
gridColumns: GRID_COLUMNS
|
|
32
130
|
});
|
|
33
131
|
console.log('');
|
|
34
|
-
console.log('
|
|
35
|
-
console.log('
|
|
36
|
-
console.log('
|
|
37
|
-
console.log('
|
|
132
|
+
console.log(' ╭──────────────────────────────────────╮');
|
|
133
|
+
console.log(' │ Wavegrid · Receiver │');
|
|
134
|
+
console.log(' │ the brain │');
|
|
135
|
+
console.log(' ╰──────────────────────────────────────╯');
|
|
38
136
|
console.log('');
|
|
39
|
-
console.log(`
|
|
40
|
-
console.log(`
|
|
41
|
-
console.log(`
|
|
137
|
+
console.log(` → Input: WebSocket @ ${SIMULATOR_URL}`);
|
|
138
|
+
console.log(` → Output: ${outputLabels.join(' + ')}`);
|
|
139
|
+
console.log(` → Alpha: ${ALPHA} Fallback delay: ${FALLBACK_DELAY}ms`);
|
|
140
|
+
console.log(` → Grid: ${NUM_CANNONS} cannons (${GRID_COLUMNS} columns)`);
|
|
141
|
+
console.log(` → Shard: ${shard ? `cannons ${shard.start}–${shard.end} (${shard.end - shard.start + 1} of ${NUM_CANNONS})` : `all cannons (no shard)`}`);
|
|
42
142
|
console.log('');
|
|
43
143
|
receiver.start();
|
|
44
144
|
// Graceful shutdown
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wavegrid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"author": "Dan Lynch <pyramation@gmail.com>",
|
|
5
|
-
"description": "Receiver brain — resilient state engine with independent low-pass filter, 3D sine wave fallback, and
|
|
5
|
+
"description": "Receiver brain — resilient state engine with independent low-pass filter, 3D sine wave fallback, and pluggable adapter pattern",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"module": "esm/index.js",
|
|
8
8
|
"types": "index.d.ts",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
},
|
|
32
32
|
"keywords": [
|
|
33
33
|
"laser",
|
|
34
|
-
"osc",
|
|
35
34
|
"receiver",
|
|
36
35
|
"brain",
|
|
37
|
-
"
|
|
36
|
+
"adapter",
|
|
37
|
+
"wavegrid"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"ws": "^8.18.0"
|
|
@@ -43,5 +43,5 @@
|
|
|
43
43
|
"@types/ws": "^8.5.13",
|
|
44
44
|
"makage": "^0.3.0"
|
|
45
45
|
},
|
|
46
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "1fc162ccd34d4b7e3594d26ee20043fb24a14ec6"
|
|
47
47
|
}
|
package/receiver.d.ts
CHANGED
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
import { InputAdapter, OutputAdapter } from './adapters';
|
|
14
14
|
import { FallbackConfig } from './fallback';
|
|
15
15
|
import { CannonState, FilteredCannon } from './filter';
|
|
16
|
+
export interface ShardConfig {
|
|
17
|
+
/** First cannon index (inclusive). */
|
|
18
|
+
start: number;
|
|
19
|
+
/** Last cannon index (inclusive). */
|
|
20
|
+
end: number;
|
|
21
|
+
}
|
|
16
22
|
export interface ReceiverConfig {
|
|
17
23
|
/** Input adapter — where state comes from. */
|
|
18
24
|
input: InputAdapter;
|
|
@@ -26,6 +32,17 @@ export interface ReceiverConfig {
|
|
|
26
32
|
fallback: FallbackConfig;
|
|
27
33
|
/** Tick rate in ms (default 1000/60 ~ 16.67ms). */
|
|
28
34
|
tickMs: number;
|
|
35
|
+
/**
|
|
36
|
+
* Optional shard — only output cannons in this index range.
|
|
37
|
+
* When omitted, the receiver outputs all cannons.
|
|
38
|
+
* The LP filter still processes the full grid; sharding only
|
|
39
|
+
* affects which cannons are sent to the output adapter.
|
|
40
|
+
*/
|
|
41
|
+
shard?: ShardConfig;
|
|
42
|
+
/** Total number of cannons in the grid. Default 49. */
|
|
43
|
+
numCannons: number;
|
|
44
|
+
/** Number of columns in the grid (for fallback spatial mapping). Default 7. */
|
|
45
|
+
gridColumns: number;
|
|
29
46
|
}
|
|
30
47
|
export declare const DEFAULT_RECEIVER_CONFIG: ReceiverConfig;
|
|
31
48
|
export type ReceiverStatus = 'connected' | 'reconnecting' | 'fallback';
|
|
@@ -48,7 +65,7 @@ export declare class Receiver {
|
|
|
48
65
|
constructor(config?: Partial<ReceiverConfig>);
|
|
49
66
|
get status(): ReceiverStatus;
|
|
50
67
|
get fallbackActive(): boolean;
|
|
51
|
-
/** Get the current output state (after filtering). */
|
|
68
|
+
/** Get the current output state (after filtering and sharding). */
|
|
52
69
|
getOutputState(): CannonState[];
|
|
53
70
|
/** Start the receiver — connects input and begins the tick loop. */
|
|
54
71
|
start(): void;
|
package/receiver.js
CHANGED
|
@@ -22,7 +22,9 @@ exports.DEFAULT_RECEIVER_CONFIG = {
|
|
|
22
22
|
alpha: filter_1.DEFAULT_RECEIVER_ALPHA,
|
|
23
23
|
fallbackDelay: 3000,
|
|
24
24
|
fallback: fallback_1.DEFAULT_FALLBACK_CONFIG,
|
|
25
|
-
tickMs: 1000 / 60
|
|
25
|
+
tickMs: 1000 / 60,
|
|
26
|
+
numCannons: filter_1.DEFAULT_NUM_CANNONS,
|
|
27
|
+
gridColumns: filter_1.DEFAULT_GRID_COLUMNS
|
|
26
28
|
};
|
|
27
29
|
class Receiver {
|
|
28
30
|
config;
|
|
@@ -35,17 +37,21 @@ class Receiver {
|
|
|
35
37
|
_running = false;
|
|
36
38
|
constructor(config = {}) {
|
|
37
39
|
this.config = { ...exports.DEFAULT_RECEIVER_CONFIG, ...config };
|
|
38
|
-
this.grid = (0, filter_1.createFilteredGrid)();
|
|
40
|
+
this.grid = (0, filter_1.createFilteredGrid)(this.config.numCannons);
|
|
39
41
|
}
|
|
40
42
|
get status() { return this._status; }
|
|
41
43
|
get fallbackActive() { return this._fallbackActive; }
|
|
42
|
-
/** Get the current output state (after filtering). */
|
|
44
|
+
/** Get the current output state (after filtering and sharding). */
|
|
43
45
|
getOutputState() {
|
|
44
|
-
|
|
46
|
+
const full = this.grid.map(c => ({
|
|
45
47
|
h: c.h,
|
|
46
48
|
s: c.s,
|
|
47
49
|
b: c.b
|
|
48
50
|
}));
|
|
51
|
+
const shard = this.config.shard;
|
|
52
|
+
if (!shard)
|
|
53
|
+
return full;
|
|
54
|
+
return full.slice(shard.start, shard.end + 1);
|
|
49
55
|
}
|
|
50
56
|
/** Start the receiver — connects input and begins the tick loop. */
|
|
51
57
|
start() {
|
|
@@ -101,7 +107,7 @@ class Receiver {
|
|
|
101
107
|
}
|
|
102
108
|
// If fallback is active, compute sine wave targets
|
|
103
109
|
if (this._fallbackActive) {
|
|
104
|
-
(0, fallback_1.computeFallbackFrame)(this.grid, this.tick, this.config.fallback);
|
|
110
|
+
(0, fallback_1.computeFallbackFrame)(this.grid, this.tick, this.config.fallback, this.config.gridColumns);
|
|
105
111
|
}
|
|
106
112
|
// Always tick the low-pass filter — this ensures smooth output
|
|
107
113
|
// whether receiving data, transitioning to fallback, or in fallback
|