@wavegrid/simulator 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 CHANGED
@@ -1,7 +1,20 @@
1
- All Rights Reserved.
1
+ The San Francisco License (SF License)
2
2
 
3
- Copyright (c) 2024 Interweb, Inc.
3
+ Copyright (c) 2026 Interweb, Inc.
4
4
 
5
- This software and associated documentation files (the "Software") may not be
6
- reproduced, distributed, or used without express written permission from
7
- Interweb, Inc.
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/animations.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import { CannonTarget } from './grid';
2
- export type AnimationFn = (grid: CannonTarget[], tick: number, attack: number) => void;
2
+ export type AnimationFn = (grid: CannonTarget[], tick: number, attack: number, gridColumns?: number) => void;
3
3
  export declare const animations: Record<string, AnimationFn>;
4
4
  export declare function getAnimationNames(): string[];
package/animations.js CHANGED
@@ -4,9 +4,9 @@ exports.animations = void 0;
4
4
  exports.getAnimationNames = getAnimationNames;
5
5
  const grid_1 = require("./grid");
6
6
  exports.animations = {
7
- wave: (grid, tick, attack) => {
8
- for (let i = 0; i < grid_1.NUM_CANNONS; i++) {
9
- const col = i % grid_1.GRID_SIZE;
7
+ wave: (grid, tick, attack, cols = grid_1.DEFAULT_GRID_COLUMNS) => {
8
+ for (let i = 0; i < grid.length; i++) {
9
+ const col = i % cols;
10
10
  const hue = (tick * 2 + col * 40) % 360;
11
11
  const bright = 60 + Math.sin(tick * 0.05 + col * 0.8) * 20;
12
12
  (0, grid_1.setCannonTarget)(grid, i, hue, 85, bright, attack);
@@ -14,91 +14,87 @@ exports.animations = {
14
14
  },
15
15
  breathe: (grid, tick, attack) => {
16
16
  const brightness = 40 + Math.sin(tick * 0.03) * 35;
17
- for (let i = 0; i < grid_1.NUM_CANNONS; i++) {
17
+ for (let i = 0; i < grid.length; i++) {
18
18
  (0, grid_1.setCannonTarget)(grid, i, 220, 80, brightness, attack);
19
19
  }
20
20
  },
21
- rainbow: (grid, tick, attack) => {
22
- for (let i = 0; i < grid_1.NUM_CANNONS; i++) {
23
- const row = Math.floor(i / grid_1.GRID_SIZE);
24
- const col = i % grid_1.GRID_SIZE;
21
+ rainbow: (grid, tick, attack, cols = grid_1.DEFAULT_GRID_COLUMNS) => {
22
+ for (let i = 0; i < grid.length; i++) {
23
+ const row = Math.floor(i / cols);
24
+ const col = i % cols;
25
25
  const hue = (tick * 1.5 + (row + col) * 25) % 360;
26
26
  (0, grid_1.setCannonTarget)(grid, i, hue, 90, 80, attack);
27
27
  }
28
28
  },
29
- pacman: (grid, tick, attack) => {
30
- // Bright dot chasing around the perimeter
31
- const perimeter = getPerimeterIndices();
29
+ pacman: (grid, tick, attack, cols = grid_1.DEFAULT_GRID_COLUMNS) => {
30
+ const perimeter = getPerimeterIndices(grid.length, cols);
32
31
  const pos = Math.floor(tick * 0.3) % perimeter.length;
33
- for (let i = 0; i < grid_1.NUM_CANNONS; i++) {
32
+ for (let i = 0; i < grid.length; i++) {
34
33
  (0, grid_1.setCannonTarget)(grid, i, 220, 60, 15, attack);
35
34
  }
36
- // Pac-man (bright yellow)
37
35
  const pacIdx = perimeter[pos];
38
36
  (0, grid_1.setCannonTarget)(grid, pacIdx, 55, 95, 95, 1.0);
39
- // Trail (fading)
40
37
  for (let t = 1; t <= 3; t++) {
41
38
  const trailPos = (pos - t + perimeter.length) % perimeter.length;
42
39
  const trailIdx = perimeter[trailPos];
43
40
  (0, grid_1.setCannonTarget)(grid, trailIdx, 55, 80, 70 - t * 18, 1.0);
44
41
  }
45
42
  },
46
- spiral: (grid, tick, attack) => {
47
- for (let i = 0; i < grid_1.NUM_CANNONS; i++) {
48
- const row = Math.floor(i / grid_1.GRID_SIZE);
49
- const col = i % grid_1.GRID_SIZE;
50
- const cx = col - 3, cy = row - 3;
51
- const angle = Math.atan2(cy, cx);
52
- const dist = Math.sqrt(cx * cx + cy * cy);
43
+ spiral: (grid, tick, attack, cols = grid_1.DEFAULT_GRID_COLUMNS) => {
44
+ const rows = Math.ceil(grid.length / cols);
45
+ const cx = (cols - 1) / 2;
46
+ const cy = (rows - 1) / 2;
47
+ for (let i = 0; i < grid.length; i++) {
48
+ const row = Math.floor(i / cols);
49
+ const col = i % cols;
50
+ const dx = col - cx, dy = row - cy;
51
+ const angle = Math.atan2(dy, dx);
52
+ const dist = Math.sqrt(dx * dx + dy * dy);
53
53
  const hue = (angle * 57.3 + dist * 40 + tick * 3) % 360;
54
54
  (0, grid_1.setCannonTarget)(grid, i, (hue + 360) % 360, 85, 75, attack);
55
55
  }
56
56
  },
57
- rain: (grid, tick, attack) => {
58
- for (let i = 0; i < grid_1.NUM_CANNONS; i++) {
59
- const row = Math.floor(i / grid_1.GRID_SIZE);
60
- const col = i % grid_1.GRID_SIZE;
61
- // Each column has a different phase offset (pseudo-random via prime)
62
- const phase = (tick * 0.15 + col * 2.3 + col * col * 0.7) % grid_1.GRID_SIZE;
57
+ rain: (grid, tick, attack, cols = grid_1.DEFAULT_GRID_COLUMNS) => {
58
+ const rows = Math.ceil(grid.length / cols);
59
+ for (let i = 0; i < grid.length; i++) {
60
+ const row = Math.floor(i / cols);
61
+ const col = i % cols;
62
+ const phase = (tick * 0.15 + col * 2.3 + col * col * 0.7) % rows;
63
63
  const dist = Math.abs(row - phase);
64
64
  const bright = dist < 1.5 ? 90 - dist * 30 : 10;
65
65
  (0, grid_1.setCannonTarget)(grid, i, 200 + col * 8, 70, bright, attack);
66
66
  }
67
67
  },
68
68
  heartbeat: (grid, tick, attack) => {
69
- // Double-pulse then rest (period ~120 ticks at 60fps = 2s)
70
69
  const phase = tick % 120;
71
70
  let brightness;
72
71
  if (phase < 10)
73
- brightness = 40 + phase * 5; // first pulse up
72
+ brightness = 40 + phase * 5;
74
73
  else if (phase < 20)
75
- brightness = 90 - (phase - 10) * 5; // first pulse down
74
+ brightness = 90 - (phase - 10) * 5;
76
75
  else if (phase < 30)
77
- brightness = 40 + (phase - 20) * 4; // second pulse up
76
+ brightness = 40 + (phase - 20) * 4;
78
77
  else if (phase < 40)
79
- brightness = 80 - (phase - 30) * 4; // second pulse down
78
+ brightness = 80 - (phase - 30) * 4;
80
79
  else
81
- brightness = 40; // rest
82
- for (let i = 0; i < grid_1.NUM_CANNONS; i++) {
80
+ brightness = 40;
81
+ for (let i = 0; i < grid.length; i++) {
83
82
  (0, grid_1.setCannonTarget)(grid, i, 0, 90, brightness, attack);
84
83
  }
85
84
  }
86
85
  };
87
- function getPerimeterIndices() {
86
+ function getPerimeterIndices(numCannons, cols) {
87
+ const rows = Math.ceil(numCannons / cols);
88
88
  const indices = [];
89
- // Top row
90
- for (let c = 0; c < grid_1.GRID_SIZE; c++)
89
+ for (let c = 0; c < cols; c++)
91
90
  indices.push(c);
92
- // Right column (skip top-right corner)
93
- for (let r = 1; r < grid_1.GRID_SIZE; r++)
94
- indices.push(r * grid_1.GRID_SIZE + (grid_1.GRID_SIZE - 1));
95
- // Bottom row reversed (skip bottom-right corner)
96
- for (let c = grid_1.GRID_SIZE - 2; c >= 0; c--)
97
- indices.push((grid_1.GRID_SIZE - 1) * grid_1.GRID_SIZE + c);
98
- // Left column reversed (skip corners)
99
- for (let r = grid_1.GRID_SIZE - 2; r >= 1; r--)
100
- indices.push(r * grid_1.GRID_SIZE);
101
- return indices;
91
+ for (let r = 1; r < rows; r++)
92
+ indices.push(r * cols + (cols - 1));
93
+ for (let c = cols - 2; c >= 0; c--)
94
+ indices.push((rows - 1) * cols + c);
95
+ for (let r = rows - 2; r >= 1; r--)
96
+ indices.push(r * cols);
97
+ return indices.filter(i => i < numCannons);
102
98
  }
103
99
  function getAnimationNames() {
104
100
  return Object.keys(exports.animations);
package/esm/animations.js CHANGED
@@ -1,8 +1,8 @@
1
- import { GRID_SIZE, NUM_CANNONS, setCannonTarget } from './grid';
1
+ import { DEFAULT_GRID_COLUMNS, setCannonTarget } from './grid';
2
2
  export const animations = {
3
- wave: (grid, tick, attack) => {
4
- for (let i = 0; i < NUM_CANNONS; i++) {
5
- const col = i % GRID_SIZE;
3
+ wave: (grid, tick, attack, cols = DEFAULT_GRID_COLUMNS) => {
4
+ for (let i = 0; i < grid.length; i++) {
5
+ const col = i % cols;
6
6
  const hue = (tick * 2 + col * 40) % 360;
7
7
  const bright = 60 + Math.sin(tick * 0.05 + col * 0.8) * 20;
8
8
  setCannonTarget(grid, i, hue, 85, bright, attack);
@@ -10,91 +10,87 @@ export const animations = {
10
10
  },
11
11
  breathe: (grid, tick, attack) => {
12
12
  const brightness = 40 + Math.sin(tick * 0.03) * 35;
13
- for (let i = 0; i < NUM_CANNONS; i++) {
13
+ for (let i = 0; i < grid.length; i++) {
14
14
  setCannonTarget(grid, i, 220, 80, brightness, attack);
15
15
  }
16
16
  },
17
- rainbow: (grid, tick, attack) => {
18
- for (let i = 0; i < NUM_CANNONS; i++) {
19
- const row = Math.floor(i / GRID_SIZE);
20
- const col = i % GRID_SIZE;
17
+ rainbow: (grid, tick, attack, cols = DEFAULT_GRID_COLUMNS) => {
18
+ for (let i = 0; i < grid.length; i++) {
19
+ const row = Math.floor(i / cols);
20
+ const col = i % cols;
21
21
  const hue = (tick * 1.5 + (row + col) * 25) % 360;
22
22
  setCannonTarget(grid, i, hue, 90, 80, attack);
23
23
  }
24
24
  },
25
- pacman: (grid, tick, attack) => {
26
- // Bright dot chasing around the perimeter
27
- const perimeter = getPerimeterIndices();
25
+ pacman: (grid, tick, attack, cols = DEFAULT_GRID_COLUMNS) => {
26
+ const perimeter = getPerimeterIndices(grid.length, cols);
28
27
  const pos = Math.floor(tick * 0.3) % perimeter.length;
29
- for (let i = 0; i < NUM_CANNONS; i++) {
28
+ for (let i = 0; i < grid.length; i++) {
30
29
  setCannonTarget(grid, i, 220, 60, 15, attack);
31
30
  }
32
- // Pac-man (bright yellow)
33
31
  const pacIdx = perimeter[pos];
34
32
  setCannonTarget(grid, pacIdx, 55, 95, 95, 1.0);
35
- // Trail (fading)
36
33
  for (let t = 1; t <= 3; t++) {
37
34
  const trailPos = (pos - t + perimeter.length) % perimeter.length;
38
35
  const trailIdx = perimeter[trailPos];
39
36
  setCannonTarget(grid, trailIdx, 55, 80, 70 - t * 18, 1.0);
40
37
  }
41
38
  },
42
- spiral: (grid, tick, attack) => {
43
- for (let i = 0; i < NUM_CANNONS; i++) {
44
- const row = Math.floor(i / GRID_SIZE);
45
- const col = i % GRID_SIZE;
46
- const cx = col - 3, cy = row - 3;
47
- const angle = Math.atan2(cy, cx);
48
- const dist = Math.sqrt(cx * cx + cy * cy);
39
+ spiral: (grid, tick, attack, cols = DEFAULT_GRID_COLUMNS) => {
40
+ const rows = Math.ceil(grid.length / cols);
41
+ const cx = (cols - 1) / 2;
42
+ const cy = (rows - 1) / 2;
43
+ for (let i = 0; i < grid.length; i++) {
44
+ const row = Math.floor(i / cols);
45
+ const col = i % cols;
46
+ const dx = col - cx, dy = row - cy;
47
+ const angle = Math.atan2(dy, dx);
48
+ const dist = Math.sqrt(dx * dx + dy * dy);
49
49
  const hue = (angle * 57.3 + dist * 40 + tick * 3) % 360;
50
50
  setCannonTarget(grid, i, (hue + 360) % 360, 85, 75, attack);
51
51
  }
52
52
  },
53
- rain: (grid, tick, attack) => {
54
- for (let i = 0; i < NUM_CANNONS; i++) {
55
- const row = Math.floor(i / GRID_SIZE);
56
- const col = i % GRID_SIZE;
57
- // Each column has a different phase offset (pseudo-random via prime)
58
- const phase = (tick * 0.15 + col * 2.3 + col * col * 0.7) % GRID_SIZE;
53
+ rain: (grid, tick, attack, cols = DEFAULT_GRID_COLUMNS) => {
54
+ const rows = Math.ceil(grid.length / cols);
55
+ for (let i = 0; i < grid.length; i++) {
56
+ const row = Math.floor(i / cols);
57
+ const col = i % cols;
58
+ const phase = (tick * 0.15 + col * 2.3 + col * col * 0.7) % rows;
59
59
  const dist = Math.abs(row - phase);
60
60
  const bright = dist < 1.5 ? 90 - dist * 30 : 10;
61
61
  setCannonTarget(grid, i, 200 + col * 8, 70, bright, attack);
62
62
  }
63
63
  },
64
64
  heartbeat: (grid, tick, attack) => {
65
- // Double-pulse then rest (period ~120 ticks at 60fps = 2s)
66
65
  const phase = tick % 120;
67
66
  let brightness;
68
67
  if (phase < 10)
69
- brightness = 40 + phase * 5; // first pulse up
68
+ brightness = 40 + phase * 5;
70
69
  else if (phase < 20)
71
- brightness = 90 - (phase - 10) * 5; // first pulse down
70
+ brightness = 90 - (phase - 10) * 5;
72
71
  else if (phase < 30)
73
- brightness = 40 + (phase - 20) * 4; // second pulse up
72
+ brightness = 40 + (phase - 20) * 4;
74
73
  else if (phase < 40)
75
- brightness = 80 - (phase - 30) * 4; // second pulse down
74
+ brightness = 80 - (phase - 30) * 4;
76
75
  else
77
- brightness = 40; // rest
78
- for (let i = 0; i < NUM_CANNONS; i++) {
76
+ brightness = 40;
77
+ for (let i = 0; i < grid.length; i++) {
79
78
  setCannonTarget(grid, i, 0, 90, brightness, attack);
80
79
  }
81
80
  }
82
81
  };
83
- function getPerimeterIndices() {
82
+ function getPerimeterIndices(numCannons, cols) {
83
+ const rows = Math.ceil(numCannons / cols);
84
84
  const indices = [];
85
- // Top row
86
- for (let c = 0; c < GRID_SIZE; c++)
85
+ for (let c = 0; c < cols; c++)
87
86
  indices.push(c);
88
- // Right column (skip top-right corner)
89
- for (let r = 1; r < GRID_SIZE; r++)
90
- indices.push(r * GRID_SIZE + (GRID_SIZE - 1));
91
- // Bottom row reversed (skip bottom-right corner)
92
- for (let c = GRID_SIZE - 2; c >= 0; c--)
93
- indices.push((GRID_SIZE - 1) * GRID_SIZE + c);
94
- // Left column reversed (skip corners)
95
- for (let r = GRID_SIZE - 2; r >= 1; r--)
96
- indices.push(r * GRID_SIZE);
97
- return indices;
87
+ for (let r = 1; r < rows; r++)
88
+ indices.push(r * cols + (cols - 1));
89
+ for (let c = cols - 2; c >= 0; c--)
90
+ indices.push((rows - 1) * cols + c);
91
+ for (let r = rows - 2; r >= 1; r--)
92
+ indices.push(r * cols);
93
+ return indices.filter(i => i < numCannons);
98
94
  }
99
95
  export function getAnimationNames() {
100
96
  return Object.keys(animations);
package/esm/grid.js CHANGED
@@ -1,13 +1,16 @@
1
- export const NUM_CANNONS = 49;
2
- export const GRID_SIZE = 7;
1
+ export const DEFAULT_NUM_CANNONS = 49;
2
+ export const DEFAULT_GRID_COLUMNS = 7;
3
+ // Legacy aliases for backwards compatibility
4
+ export const NUM_CANNONS = DEFAULT_NUM_CANNONS;
5
+ export const GRID_SIZE = DEFAULT_GRID_COLUMNS;
3
6
  /**
4
7
  * Smoothing factor per tick (0–1).
5
8
  * Lower = smoother/slower transitions (more low-pass filtering).
6
9
  * At 60fps with alpha=0.08, a full transition takes ~1.5s to settle.
7
10
  */
8
11
  export const DEFAULT_ALPHA = 0.08;
9
- export function createGrid() {
10
- return Array.from({ length: NUM_CANNONS }, () => ({
12
+ export function createGrid(numCannons = DEFAULT_NUM_CANNONS) {
13
+ return Array.from({ length: numCannons }, () => ({
11
14
  h: 220,
12
15
  s: 90,
13
16
  b: 80,
package/esm/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { animations, getAnimationNames } from './animations';
2
- export { createGrid, DEFAULT_ALPHA, GRID_SIZE, NUM_CANNONS, setAllTargets, setCannonTarget, tickGrid } from './grid';
2
+ export { createGrid, DEFAULT_ALPHA, DEFAULT_GRID_COLUMNS, DEFAULT_NUM_CANNONS, GRID_SIZE, NUM_CANNONS, setAllTargets, setCannonTarget, tickGrid } from './grid';
3
3
  export { applyScene, scenes } from './scenes';
package/esm/scenes.js CHANGED
@@ -1,31 +1,31 @@
1
- import { NUM_CANNONS, setCannonTarget } from './grid';
1
+ import { DEFAULT_GRID_COLUMNS, setCannonTarget } from './grid';
2
2
  export const scenes = {
3
3
  civic: () => ({ h: 220, s: 90, b: 80 }),
4
4
  pride: (i, total) => ({ h: Math.round((i / total) * 360), s: 90, b: 80 }),
5
5
  gold: () => ({ h: 45, s: 95, b: 80 }),
6
6
  white: () => ({ h: 0, s: 0, b: 80 }),
7
- solstice: (i) => {
8
- const row = Math.floor(i / 7);
9
- const col = i % 7;
7
+ solstice: (i, _total, cols) => {
8
+ const row = Math.floor(i / cols);
9
+ const col = i % cols;
10
10
  return { h: 40 + row * 5 + col * 4, s: 85, b: 80 };
11
11
  },
12
- ocean: (i) => {
13
- const row = Math.floor(i / 7);
14
- const col = i % 7;
12
+ ocean: (i, _total, cols) => {
13
+ const row = Math.floor(i / cols);
14
+ const col = i % cols;
15
15
  return { h: 180 + row * 8 + col * 3, s: 75, b: 70 };
16
16
  },
17
- sunset: (i) => {
18
- const row = Math.floor(i / 7);
17
+ sunset: (i, _total, cols) => {
18
+ const row = Math.floor(i / cols);
19
19
  return { h: 10 + row * 5, s: 90, b: 85 - row * 5 };
20
20
  },
21
21
  off: () => ({ h: 0, s: 0, b: 0 })
22
22
  };
23
- export function applyScene(grid, sceneName) {
23
+ export function applyScene(grid, sceneName, gridColumns = DEFAULT_GRID_COLUMNS) {
24
24
  const generator = scenes[sceneName];
25
25
  if (!generator)
26
26
  return;
27
- for (let i = 0; i < NUM_CANNONS; i++) {
28
- const { h, s, b } = generator(i, NUM_CANNONS);
27
+ for (let i = 0; i < grid.length; i++) {
28
+ const { h, s, b } = generator(i, grid.length, gridColumns);
29
29
  setCannonTarget(grid, i, h, s, b);
30
30
  }
31
31
  }
package/esm/server.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import http from 'http';
2
2
  import { WebSocket, WebSocketServer } from 'ws';
3
3
  import { animations } from './animations';
4
- import { createGrid, DEFAULT_ALPHA, setAllTargets, setCannonTarget, tickGrid } from './grid';
4
+ import { createGrid, DEFAULT_ALPHA, DEFAULT_GRID_COLUMNS, DEFAULT_NUM_CANNONS, setAllTargets, setCannonTarget, tickGrid } from './grid';
5
5
  import { applyScene, scenes } from './scenes';
6
6
  import { getHTML } from './ui';
7
7
  const PORT = parseInt(process.env.PORT || '3000', 10);
8
8
  const TICK_MS = 1000 / 60; // 60fps interpolation
9
- const grid = createGrid();
9
+ const NUM_CANNONS = process.env.NUM_CANNONS ? parseInt(process.env.NUM_CANNONS, 10) : DEFAULT_NUM_CANNONS;
10
+ const GRID_COLUMNS = process.env.GRID_COLUMNS ? parseInt(process.env.GRID_COLUMNS, 10) : DEFAULT_GRID_COLUMNS;
11
+ const grid = createGrid(NUM_CANNONS);
10
12
  let currentAlpha = DEFAULT_ALPHA;
11
13
  let currentAttack = 1.0;
12
14
  let currentAnimation = null;
@@ -22,7 +24,7 @@ const server = http.createServer((req, res) => {
22
24
  return;
23
25
  }
24
26
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
25
- res.end(getHTML());
27
+ res.end(getHTML(NUM_CANNONS, GRID_COLUMNS));
26
28
  });
27
29
  const wss = new WebSocketServer({ server });
28
30
  function broadcastState() {
@@ -63,7 +65,7 @@ function handleMessage(msg) {
63
65
  case 'scene':
64
66
  if (msg.name && scenes[msg.name]) {
65
67
  currentAnimation = null;
66
- applyScene(grid, msg.name);
68
+ applyScene(grid, msg.name, GRID_COLUMNS);
67
69
  }
68
70
  break;
69
71
  case 'animation':
@@ -99,7 +101,7 @@ function handleMessage(msg) {
99
101
  // Animation loop: tick interpolation and broadcast
100
102
  setInterval(() => {
101
103
  if (currentAnimation && animations[currentAnimation]) {
102
- animations[currentAnimation](grid, animationTick, currentAttack);
104
+ animations[currentAnimation](grid, animationTick, currentAttack, GRID_COLUMNS);
103
105
  animationTick++;
104
106
  }
105
107
  const changed = tickGrid(grid, currentAlpha);
@@ -107,14 +109,16 @@ setInterval(() => {
107
109
  broadcastState();
108
110
  }
109
111
  }, TICK_MS);
112
+ const GRID_ROWS = Math.ceil(NUM_CANNONS / GRID_COLUMNS);
110
113
  server.listen(PORT, '0.0.0.0', () => {
111
114
  console.log('');
112
115
  console.log('╔══════════════════════════════════════════╗');
113
- console.log('║ Illuminate · 7×7 Grid Simulator');
114
- console.log('║ 49 virtual cannons ready');
116
+ console.log(`║ Wavegrid · ${GRID_COLUMNS}×${GRID_ROWS} Grid Simulator${' '.repeat(Math.max(0, 16 - String(GRID_COLUMNS).length - String(GRID_ROWS).length))}║`);
117
+ console.log(`║ ${NUM_CANNONS} virtual cannons ready${' '.repeat(Math.max(0, 21 - String(NUM_CANNONS).length))}║`);
115
118
  console.log('╚══════════════════════════════════════════╝');
116
119
  console.log('');
117
120
  console.log(` → http://localhost:${PORT}`);
121
+ console.log(` → Grid: ${NUM_CANNONS} cannons (${GRID_COLUMNS} columns)`);
118
122
  console.log('');
119
123
  });
120
124
  export { grid, server };
package/esm/ui.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { animations } from './animations';
2
2
  import { scenes } from './scenes';
3
- export function getHTML() {
3
+ export function getHTML(numCannons = 49, gridColumns = 7) {
4
4
  const sceneNames = Object.keys(scenes);
5
5
  const animationNames = Object.keys(animations);
6
+ const gridRows = Math.ceil(numCannons / gridColumns);
6
7
  return `<!DOCTYPE html>
7
8
  <html>
8
9
  <head>
@@ -99,7 +100,7 @@ export function getHTML() {
99
100
 
100
101
  .grid {
101
102
  display: grid;
102
- grid-template-columns: repeat(7, 1fr);
103
+ grid-template-columns: repeat(${gridColumns}, 1fr);
103
104
  gap: 4px;
104
105
  max-width: 420px;
105
106
  }
@@ -168,7 +169,7 @@ export function getHTML() {
168
169
  <body>
169
170
 
170
171
  <h1>Wavegrid · Master Controller</h1>
171
- <div class="subtitle">7×7 Civic Center Plaza · 49 cannons</div>
172
+ <div class="subtitle">${gridColumns}×${gridRows} Grid · ${numCannons} cannons</div>
172
173
 
173
174
  <div class="columns">
174
175
  <div class="col-left">
@@ -272,7 +273,8 @@ export function getHTML() {
272
273
  <div class="status" id="status">Connecting...</div>
273
274
 
274
275
  <script>
275
- const NUM = 49;
276
+ const NUM = ${numCannons};
277
+ const COLS = ${gridColumns};
276
278
  const ws = new WebSocket('ws://' + location.host);
277
279
  const status = document.getElementById('status');
278
280
  const selected = new Set();
@@ -286,7 +288,7 @@ let idleTimeout = 0;
286
288
  let idleSeconds = 0;
287
289
  let lastInputTime = Date.now();
288
290
 
289
- ws.onopen = () => { status.textContent = 'Connected · 49 cannons · master controller'; };
291
+ ws.onopen = () => { status.textContent = 'Connected · ' + NUM + ' cannons · master controller'; };
290
292
  ws.onclose = () => { status.textContent = 'Disconnected — reload page'; };
291
293
 
292
294
  ws.onmessage = (e) => {
@@ -328,22 +330,23 @@ for (let i = 0; i < NUM; i++) {
328
330
 
329
331
  // Build row/col buttons
330
332
  const rcEl = document.getElementById('rc-btns');
331
- for (let r = 0; r < 7; r++) {
333
+ const ROWS = Math.ceil(NUM / COLS);
334
+ for (let r = 0; r < ROWS; r++) {
332
335
  const btn = document.createElement('button');
333
336
  btn.className = 'rc-btn';
334
337
  btn.textContent = 'R' + (r + 1);
335
338
  btn.addEventListener('click', () => {
336
- for (let c = 0; c < 7; c++) selectOn(r * 7 + c);
339
+ for (let c = 0; c < COLS; c++) { const idx = r * COLS + c; if (idx < NUM) selectOn(idx); }
337
340
  updatePanel();
338
341
  });
339
342
  rcEl.appendChild(btn);
340
343
  }
341
- for (let c = 0; c < 7; c++) {
344
+ for (let c = 0; c < COLS; c++) {
342
345
  const btn = document.createElement('button');
343
346
  btn.className = 'rc-btn';
344
347
  btn.textContent = 'C' + (c + 1);
345
348
  btn.addEventListener('click', () => {
346
- for (let r = 0; r < 7; r++) selectOn(r * 7 + c);
349
+ for (let r = 0; r < ROWS; r++) { const idx = r * COLS + c; if (idx < NUM) selectOn(idx); }
347
350
  updatePanel();
348
351
  });
349
352
  rcEl.appendChild(btn);
package/grid.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface CannonTarget extends CannonState {
8
8
  targetS: number;
9
9
  targetB: number;
10
10
  }
11
+ export declare const DEFAULT_NUM_CANNONS = 49;
12
+ export declare const DEFAULT_GRID_COLUMNS = 7;
11
13
  export declare const NUM_CANNONS = 49;
12
14
  export declare const GRID_SIZE = 7;
13
15
  /**
@@ -16,7 +18,7 @@ export declare const GRID_SIZE = 7;
16
18
  * At 60fps with alpha=0.08, a full transition takes ~1.5s to settle.
17
19
  */
18
20
  export declare const DEFAULT_ALPHA = 0.08;
19
- export declare function createGrid(): CannonTarget[];
21
+ export declare function createGrid(numCannons?: number): CannonTarget[];
20
22
  /**
21
23
  * Exponential low-pass filter (lerp toward target).
22
24
  * Called once per animation frame to smoothly converge current → target.
package/grid.js CHANGED
@@ -1,20 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_ALPHA = exports.GRID_SIZE = exports.NUM_CANNONS = void 0;
3
+ exports.DEFAULT_ALPHA = exports.GRID_SIZE = exports.NUM_CANNONS = exports.DEFAULT_GRID_COLUMNS = exports.DEFAULT_NUM_CANNONS = void 0;
4
4
  exports.createGrid = createGrid;
5
5
  exports.tickGrid = tickGrid;
6
6
  exports.setCannonTarget = setCannonTarget;
7
7
  exports.setAllTargets = setAllTargets;
8
- exports.NUM_CANNONS = 49;
9
- exports.GRID_SIZE = 7;
8
+ exports.DEFAULT_NUM_CANNONS = 49;
9
+ exports.DEFAULT_GRID_COLUMNS = 7;
10
+ // Legacy aliases for backwards compatibility
11
+ exports.NUM_CANNONS = exports.DEFAULT_NUM_CANNONS;
12
+ exports.GRID_SIZE = exports.DEFAULT_GRID_COLUMNS;
10
13
  /**
11
14
  * Smoothing factor per tick (0–1).
12
15
  * Lower = smoother/slower transitions (more low-pass filtering).
13
16
  * At 60fps with alpha=0.08, a full transition takes ~1.5s to settle.
14
17
  */
15
18
  exports.DEFAULT_ALPHA = 0.08;
16
- function createGrid() {
17
- return Array.from({ length: exports.NUM_CANNONS }, () => ({
19
+ function createGrid(numCannons = exports.DEFAULT_NUM_CANNONS) {
20
+ return Array.from({ length: numCannons }, () => ({
18
21
  h: 220,
19
22
  s: 90,
20
23
  b: 80,
package/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type { AnimationFn } from './animations';
2
2
  export { animations, getAnimationNames } from './animations';
3
3
  export type { CannonState, CannonTarget } from './grid';
4
- export { createGrid, DEFAULT_ALPHA, GRID_SIZE, NUM_CANNONS, setAllTargets, setCannonTarget, tickGrid } from './grid';
4
+ export { createGrid, DEFAULT_ALPHA, DEFAULT_GRID_COLUMNS, DEFAULT_NUM_CANNONS, GRID_SIZE, NUM_CANNONS, setAllTargets, setCannonTarget, tickGrid } from './grid';
5
5
  export type { SceneColor, SceneGenerator } from './scenes';
6
6
  export { applyScene, scenes } from './scenes';
package/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.scenes = exports.applyScene = exports.tickGrid = exports.setCannonTarget = exports.setAllTargets = exports.NUM_CANNONS = exports.GRID_SIZE = exports.DEFAULT_ALPHA = exports.createGrid = exports.getAnimationNames = exports.animations = void 0;
3
+ exports.scenes = exports.applyScene = exports.tickGrid = exports.setCannonTarget = exports.setAllTargets = exports.NUM_CANNONS = exports.GRID_SIZE = exports.DEFAULT_NUM_CANNONS = exports.DEFAULT_GRID_COLUMNS = exports.DEFAULT_ALPHA = exports.createGrid = exports.getAnimationNames = exports.animations = void 0;
4
4
  var animations_1 = require("./animations");
5
5
  Object.defineProperty(exports, "animations", { enumerable: true, get: function () { return animations_1.animations; } });
6
6
  Object.defineProperty(exports, "getAnimationNames", { enumerable: true, get: function () { return animations_1.getAnimationNames; } });
7
7
  var grid_1 = require("./grid");
8
8
  Object.defineProperty(exports, "createGrid", { enumerable: true, get: function () { return grid_1.createGrid; } });
9
9
  Object.defineProperty(exports, "DEFAULT_ALPHA", { enumerable: true, get: function () { return grid_1.DEFAULT_ALPHA; } });
10
+ Object.defineProperty(exports, "DEFAULT_GRID_COLUMNS", { enumerable: true, get: function () { return grid_1.DEFAULT_GRID_COLUMNS; } });
11
+ Object.defineProperty(exports, "DEFAULT_NUM_CANNONS", { enumerable: true, get: function () { return grid_1.DEFAULT_NUM_CANNONS; } });
10
12
  Object.defineProperty(exports, "GRID_SIZE", { enumerable: true, get: function () { return grid_1.GRID_SIZE; } });
11
13
  Object.defineProperty(exports, "NUM_CANNONS", { enumerable: true, get: function () { return grid_1.NUM_CANNONS; } });
12
14
  Object.defineProperty(exports, "setAllTargets", { enumerable: true, get: function () { return grid_1.setAllTargets; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavegrid/simulator",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "author": "Dan Lynch <pyramation@gmail.com>",
5
5
  "description": "7×7 RGB grid simulator with smooth transitions",
6
6
  "main": "index.js",
@@ -43,5 +43,5 @@
43
43
  "@types/ws": "^8.5.13",
44
44
  "makage": "^0.3.0"
45
45
  },
46
- "gitHead": "579bfca2fe84231a6722d3b4bfd44d1f42a0c9ef"
46
+ "gitHead": "1fc162ccd34d4b7e3594d26ee20043fb24a14ec6"
47
47
  }
package/scenes.d.ts CHANGED
@@ -4,6 +4,6 @@ export interface SceneColor {
4
4
  s: number;
5
5
  b: number;
6
6
  }
7
- export type SceneGenerator = (index: number, total: number) => SceneColor;
7
+ export type SceneGenerator = (index: number, total: number, gridColumns: number) => SceneColor;
8
8
  export declare const scenes: Record<string, SceneGenerator>;
9
- export declare function applyScene(grid: CannonTarget[], sceneName: string): void;
9
+ export declare function applyScene(grid: CannonTarget[], sceneName: string, gridColumns?: number): void;
package/scenes.js CHANGED
@@ -8,28 +8,28 @@ exports.scenes = {
8
8
  pride: (i, total) => ({ h: Math.round((i / total) * 360), s: 90, b: 80 }),
9
9
  gold: () => ({ h: 45, s: 95, b: 80 }),
10
10
  white: () => ({ h: 0, s: 0, b: 80 }),
11
- solstice: (i) => {
12
- const row = Math.floor(i / 7);
13
- const col = i % 7;
11
+ solstice: (i, _total, cols) => {
12
+ const row = Math.floor(i / cols);
13
+ const col = i % cols;
14
14
  return { h: 40 + row * 5 + col * 4, s: 85, b: 80 };
15
15
  },
16
- ocean: (i) => {
17
- const row = Math.floor(i / 7);
18
- const col = i % 7;
16
+ ocean: (i, _total, cols) => {
17
+ const row = Math.floor(i / cols);
18
+ const col = i % cols;
19
19
  return { h: 180 + row * 8 + col * 3, s: 75, b: 70 };
20
20
  },
21
- sunset: (i) => {
22
- const row = Math.floor(i / 7);
21
+ sunset: (i, _total, cols) => {
22
+ const row = Math.floor(i / cols);
23
23
  return { h: 10 + row * 5, s: 90, b: 85 - row * 5 };
24
24
  },
25
25
  off: () => ({ h: 0, s: 0, b: 0 })
26
26
  };
27
- function applyScene(grid, sceneName) {
27
+ function applyScene(grid, sceneName, gridColumns = grid_1.DEFAULT_GRID_COLUMNS) {
28
28
  const generator = exports.scenes[sceneName];
29
29
  if (!generator)
30
30
  return;
31
- for (let i = 0; i < grid_1.NUM_CANNONS; i++) {
32
- const { h, s, b } = generator(i, grid_1.NUM_CANNONS);
31
+ for (let i = 0; i < grid.length; i++) {
32
+ const { h, s, b } = generator(i, grid.length, gridColumns);
33
33
  (0, grid_1.setCannonTarget)(grid, i, h, s, b);
34
34
  }
35
35
  }
package/server.js CHANGED
@@ -12,7 +12,9 @@ const scenes_1 = require("./scenes");
12
12
  const ui_1 = require("./ui");
13
13
  const PORT = parseInt(process.env.PORT || '3000', 10);
14
14
  const TICK_MS = 1000 / 60; // 60fps interpolation
15
- const grid = (0, grid_1.createGrid)();
15
+ const NUM_CANNONS = process.env.NUM_CANNONS ? parseInt(process.env.NUM_CANNONS, 10) : grid_1.DEFAULT_NUM_CANNONS;
16
+ const GRID_COLUMNS = process.env.GRID_COLUMNS ? parseInt(process.env.GRID_COLUMNS, 10) : grid_1.DEFAULT_GRID_COLUMNS;
17
+ const grid = (0, grid_1.createGrid)(NUM_CANNONS);
16
18
  exports.grid = grid;
17
19
  let currentAlpha = grid_1.DEFAULT_ALPHA;
18
20
  let currentAttack = 1.0;
@@ -29,7 +31,7 @@ const server = http_1.default.createServer((req, res) => {
29
31
  return;
30
32
  }
31
33
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
32
- res.end((0, ui_1.getHTML)());
34
+ res.end((0, ui_1.getHTML)(NUM_CANNONS, GRID_COLUMNS));
33
35
  });
34
36
  exports.server = server;
35
37
  const wss = new ws_1.WebSocketServer({ server });
@@ -71,7 +73,7 @@ function handleMessage(msg) {
71
73
  case 'scene':
72
74
  if (msg.name && scenes_1.scenes[msg.name]) {
73
75
  currentAnimation = null;
74
- (0, scenes_1.applyScene)(grid, msg.name);
76
+ (0, scenes_1.applyScene)(grid, msg.name, GRID_COLUMNS);
75
77
  }
76
78
  break;
77
79
  case 'animation':
@@ -107,7 +109,7 @@ function handleMessage(msg) {
107
109
  // Animation loop: tick interpolation and broadcast
108
110
  setInterval(() => {
109
111
  if (currentAnimation && animations_1.animations[currentAnimation]) {
110
- animations_1.animations[currentAnimation](grid, animationTick, currentAttack);
112
+ animations_1.animations[currentAnimation](grid, animationTick, currentAttack, GRID_COLUMNS);
111
113
  animationTick++;
112
114
  }
113
115
  const changed = (0, grid_1.tickGrid)(grid, currentAlpha);
@@ -115,13 +117,15 @@ setInterval(() => {
115
117
  broadcastState();
116
118
  }
117
119
  }, TICK_MS);
120
+ const GRID_ROWS = Math.ceil(NUM_CANNONS / GRID_COLUMNS);
118
121
  server.listen(PORT, '0.0.0.0', () => {
119
122
  console.log('');
120
123
  console.log('╔══════════════════════════════════════════╗');
121
- console.log('║ Illuminate · 7×7 Grid Simulator');
122
- console.log('║ 49 virtual cannons ready');
124
+ console.log(`║ Wavegrid · ${GRID_COLUMNS}×${GRID_ROWS} Grid Simulator${' '.repeat(Math.max(0, 16 - String(GRID_COLUMNS).length - String(GRID_ROWS).length))}║`);
125
+ console.log(`║ ${NUM_CANNONS} virtual cannons ready${' '.repeat(Math.max(0, 21 - String(NUM_CANNONS).length))}║`);
123
126
  console.log('╚══════════════════════════════════════════╝');
124
127
  console.log('');
125
128
  console.log(` → http://localhost:${PORT}`);
129
+ console.log(` → Grid: ${NUM_CANNONS} cannons (${GRID_COLUMNS} columns)`);
126
130
  console.log('');
127
131
  });
package/ui.d.ts CHANGED
@@ -1 +1 @@
1
- export declare function getHTML(): string;
1
+ export declare function getHTML(numCannons?: number, gridColumns?: number): string;
package/ui.js CHANGED
@@ -3,9 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getHTML = getHTML;
4
4
  const animations_1 = require("./animations");
5
5
  const scenes_1 = require("./scenes");
6
- function getHTML() {
6
+ function getHTML(numCannons = 49, gridColumns = 7) {
7
7
  const sceneNames = Object.keys(scenes_1.scenes);
8
8
  const animationNames = Object.keys(animations_1.animations);
9
+ const gridRows = Math.ceil(numCannons / gridColumns);
9
10
  return `<!DOCTYPE html>
10
11
  <html>
11
12
  <head>
@@ -102,7 +103,7 @@ function getHTML() {
102
103
 
103
104
  .grid {
104
105
  display: grid;
105
- grid-template-columns: repeat(7, 1fr);
106
+ grid-template-columns: repeat(${gridColumns}, 1fr);
106
107
  gap: 4px;
107
108
  max-width: 420px;
108
109
  }
@@ -171,7 +172,7 @@ function getHTML() {
171
172
  <body>
172
173
 
173
174
  <h1>Wavegrid · Master Controller</h1>
174
- <div class="subtitle">7×7 Civic Center Plaza · 49 cannons</div>
175
+ <div class="subtitle">${gridColumns}×${gridRows} Grid · ${numCannons} cannons</div>
175
176
 
176
177
  <div class="columns">
177
178
  <div class="col-left">
@@ -275,7 +276,8 @@ function getHTML() {
275
276
  <div class="status" id="status">Connecting...</div>
276
277
 
277
278
  <script>
278
- const NUM = 49;
279
+ const NUM = ${numCannons};
280
+ const COLS = ${gridColumns};
279
281
  const ws = new WebSocket('ws://' + location.host);
280
282
  const status = document.getElementById('status');
281
283
  const selected = new Set();
@@ -289,7 +291,7 @@ let idleTimeout = 0;
289
291
  let idleSeconds = 0;
290
292
  let lastInputTime = Date.now();
291
293
 
292
- ws.onopen = () => { status.textContent = 'Connected · 49 cannons · master controller'; };
294
+ ws.onopen = () => { status.textContent = 'Connected · ' + NUM + ' cannons · master controller'; };
293
295
  ws.onclose = () => { status.textContent = 'Disconnected — reload page'; };
294
296
 
295
297
  ws.onmessage = (e) => {
@@ -331,22 +333,23 @@ for (let i = 0; i < NUM; i++) {
331
333
 
332
334
  // Build row/col buttons
333
335
  const rcEl = document.getElementById('rc-btns');
334
- for (let r = 0; r < 7; r++) {
336
+ const ROWS = Math.ceil(NUM / COLS);
337
+ for (let r = 0; r < ROWS; r++) {
335
338
  const btn = document.createElement('button');
336
339
  btn.className = 'rc-btn';
337
340
  btn.textContent = 'R' + (r + 1);
338
341
  btn.addEventListener('click', () => {
339
- for (let c = 0; c < 7; c++) selectOn(r * 7 + c);
342
+ for (let c = 0; c < COLS; c++) { const idx = r * COLS + c; if (idx < NUM) selectOn(idx); }
340
343
  updatePanel();
341
344
  });
342
345
  rcEl.appendChild(btn);
343
346
  }
344
- for (let c = 0; c < 7; c++) {
347
+ for (let c = 0; c < COLS; c++) {
345
348
  const btn = document.createElement('button');
346
349
  btn.className = 'rc-btn';
347
350
  btn.textContent = 'C' + (c + 1);
348
351
  btn.addEventListener('click', () => {
349
- for (let r = 0; r < 7; r++) selectOn(r * 7 + c);
352
+ for (let r = 0; r < ROWS; r++) { const idx = r * COLS + c; if (idx < NUM) selectOn(idx); }
350
353
  updatePanel();
351
354
  });
352
355
  rcEl.appendChild(btn);