@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 +18 -5
- package/animations.d.ts +1 -1
- package/animations.js +43 -47
- package/esm/animations.js +44 -48
- package/esm/grid.js +7 -4
- package/esm/index.js +1 -1
- package/esm/scenes.js +12 -12
- package/esm/server.js +11 -7
- package/esm/ui.js +12 -9
- package/grid.d.ts +3 -1
- package/grid.js +8 -5
- package/index.d.ts +1 -1
- package/index.js +3 -1
- package/package.json +2 -2
- package/scenes.d.ts +2 -2
- package/scenes.js +11 -11
- package/server.js +10 -6
- package/ui.d.ts +1 -1
- package/ui.js +12 -9
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/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 <
|
|
9
|
-
const col = i %
|
|
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 <
|
|
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 <
|
|
23
|
-
const row = Math.floor(i /
|
|
24
|
-
const col = i %
|
|
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
|
-
|
|
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 <
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const phase = (tick * 0.15 + col * 2.3 + col * col * 0.7) %
|
|
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;
|
|
72
|
+
brightness = 40 + phase * 5;
|
|
74
73
|
else if (phase < 20)
|
|
75
|
-
brightness = 90 - (phase - 10) * 5;
|
|
74
|
+
brightness = 90 - (phase - 10) * 5;
|
|
76
75
|
else if (phase < 30)
|
|
77
|
-
brightness = 40 + (phase - 20) * 4;
|
|
76
|
+
brightness = 40 + (phase - 20) * 4;
|
|
78
77
|
else if (phase < 40)
|
|
79
|
-
brightness = 80 - (phase - 30) * 4;
|
|
78
|
+
brightness = 80 - (phase - 30) * 4;
|
|
80
79
|
else
|
|
81
|
-
brightness = 40;
|
|
82
|
-
for (let i = 0; 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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
for (let
|
|
97
|
-
indices.push(
|
|
98
|
-
|
|
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 {
|
|
1
|
+
import { DEFAULT_GRID_COLUMNS, setCannonTarget } from './grid';
|
|
2
2
|
export const animations = {
|
|
3
|
-
wave: (grid, tick, attack) => {
|
|
4
|
-
for (let i = 0; i <
|
|
5
|
-
const col = i %
|
|
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 <
|
|
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 <
|
|
19
|
-
const row = Math.floor(i /
|
|
20
|
-
const col = i %
|
|
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
|
-
|
|
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 <
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
const
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
const phase = (tick * 0.15 + col * 2.3 + col * col * 0.7) %
|
|
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;
|
|
68
|
+
brightness = 40 + phase * 5;
|
|
70
69
|
else if (phase < 20)
|
|
71
|
-
brightness = 90 - (phase - 10) * 5;
|
|
70
|
+
brightness = 90 - (phase - 10) * 5;
|
|
72
71
|
else if (phase < 30)
|
|
73
|
-
brightness = 40 + (phase - 20) * 4;
|
|
72
|
+
brightness = 40 + (phase - 20) * 4;
|
|
74
73
|
else if (phase < 40)
|
|
75
|
-
brightness = 80 - (phase - 30) * 4;
|
|
74
|
+
brightness = 80 - (phase - 30) * 4;
|
|
76
75
|
else
|
|
77
|
-
brightness = 40;
|
|
78
|
-
for (let i = 0; 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
|
-
|
|
86
|
-
for (let c = 0; c < GRID_SIZE; c++)
|
|
85
|
+
for (let c = 0; c < cols; c++)
|
|
87
86
|
indices.push(c);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
for (let
|
|
93
|
-
indices.push(
|
|
94
|
-
|
|
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
|
|
2
|
-
export const
|
|
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:
|
|
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 {
|
|
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 /
|
|
9
|
-
const col = i %
|
|
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 /
|
|
14
|
-
const col = i %
|
|
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 /
|
|
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 <
|
|
28
|
-
const { h, s, b } = generator(i,
|
|
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
|
|
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(
|
|
114
|
-
console.log(
|
|
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(
|
|
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"
|
|
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 =
|
|
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 ·
|
|
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
|
-
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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.
|
|
9
|
-
exports.
|
|
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:
|
|
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.
|
|
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": "
|
|
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 /
|
|
13
|
-
const col = i %
|
|
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 /
|
|
18
|
-
const col = i %
|
|
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 /
|
|
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 <
|
|
32
|
-
const { h, s, b } = generator(i,
|
|
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
|
|
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(
|
|
122
|
-
console.log(
|
|
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(
|
|
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"
|
|
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 =
|
|
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 ·
|
|
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
|
-
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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);
|