@wavegrid/simulator 0.1.1 → 0.2.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/esm/ui.js CHANGED
@@ -1,22 +1,27 @@
1
+ import { animations } from './animations';
1
2
  import { scenes } from './scenes';
2
3
  export function getHTML() {
3
4
  const sceneNames = Object.keys(scenes);
5
+ const animationNames = Object.keys(animations);
4
6
  return `<!DOCTYPE html>
5
7
  <html>
6
8
  <head>
7
9
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
8
- <title>Illuminate · 7×7 Simulator</title>
10
+ <title>Wavegrid · Master Controller</title>
11
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
9
12
  <style>
10
13
  * { box-sizing: border-box; margin: 0; padding: 0; }
11
14
  body {
12
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
15
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Mono', 'Fira Code', monospace;
13
16
  background: #0a0a0a; color: #eee; padding: 1rem;
14
17
  min-height: 100vh;
15
18
  }
16
19
  h1 {
17
- font-size: 1.1rem; font-weight: 600; margin-bottom: 1rem;
20
+ font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem;
18
21
  color: #ccc; letter-spacing: 0.03em;
19
22
  }
23
+ .subtitle { font-size: 0.7rem; color: #555; margin-bottom: 1rem; }
24
+
20
25
  .master {
21
26
  background: #141414; border: 1px solid #2a2a2a; border-radius: 12px;
22
27
  padding: 1rem; margin-bottom: 1.25rem;
@@ -33,11 +38,22 @@ export function getHTML() {
33
38
  }
34
39
  .scene-btn:hover { border-color: #555; color: #ddd; }
35
40
  .scene-btn.active { background: #1a3a6a; border-color: #3a6acc; color: #fff; }
41
+
42
+ .anim-btn {
43
+ padding: 6px 14px; border-radius: 20px; font-size: 0.75rem;
44
+ cursor: pointer; border: 1px solid #333; background: #1a1a1a;
45
+ color: #aaa; transition: all 0.2s;
46
+ }
47
+ .anim-btn:hover { border-color: #555; color: #ddd; }
48
+ .anim-btn.active { background: #1a4a2a; border-color: #3acc5a; color: #fff; }
49
+ .anim-btn.stop { border-color: #5a2222; color: #c44; }
50
+ .anim-btn.stop:hover { border-color: #8a3333; color: #e66; }
51
+
36
52
  .slider-row {
37
53
  display: flex; align-items: center; gap: 10px; margin-bottom: 0.6rem;
38
54
  }
39
55
  .slider-row label {
40
- font-size: 0.8rem; color: #777; min-width: 80px;
56
+ font-size: 0.75rem; color: #777; min-width: 90px;
41
57
  }
42
58
  .slider-row input[type=range] {
43
59
  flex: 1; height: 6px; -webkit-appearance: none; appearance: none;
@@ -48,7 +64,8 @@ export function getHTML() {
48
64
  border-radius: 50%; background: #4a8cde; cursor: pointer;
49
65
  }
50
66
  .slider-row .val {
51
- font-size: 0.8rem; font-weight: 500; min-width: 38px; text-align: right; color: #888;
67
+ font-size: 0.75rem; font-weight: 500; min-width: 50px; text-align: right; color: #888;
68
+ font-family: 'SF Mono', 'Fira Code', monospace;
52
69
  }
53
70
  .all-off {
54
71
  width: 100%; padding: 10px; border-radius: 8px; margin-top: 0.5rem;
@@ -58,6 +75,10 @@ export function getHTML() {
58
75
  }
59
76
  .all-off:hover { background: #2a1010; }
60
77
 
78
+ .columns { display: flex; gap: 1.25rem; flex-wrap: wrap; }
79
+ .col-left { flex: 1; min-width: 300px; }
80
+ .col-right { flex: 1; min-width: 300px; }
81
+
61
82
  .grid-section { margin-bottom: 1.25rem; }
62
83
  .sel-actions { display: flex; gap: 6px; margin-bottom: 0.5rem; }
63
84
  .sel-btn {
@@ -94,7 +115,7 @@ export function getHTML() {
94
115
  .cell.selected { border-color: #4a8cde; }
95
116
  .cell-beam {
96
117
  width: 60%; height: 60%; border-radius: 50%;
97
- transition: none; /* driven by animation frame */
118
+ transition: none;
98
119
  }
99
120
  .cell-num {
100
121
  font-size: 7px; color: #444; margin-top: 2px;
@@ -117,6 +138,28 @@ export function getHTML() {
117
138
  background: #1a1a1a; color: #aaa; cursor: pointer;
118
139
  }
119
140
 
141
+ .telemetry {
142
+ background: #0d0d0d; border: 1px solid #1a1a1a; border-radius: 8px;
143
+ padding: 0.75rem; font-size: 0.65rem; color: #555;
144
+ font-family: 'SF Mono', 'Fira Code', monospace;
145
+ line-height: 1.6;
146
+ }
147
+ .telemetry .val-live { color: #4a8cde; }
148
+ .telemetry .val-anim { color: #3acc5a; }
149
+
150
+ .ambient-section {
151
+ background: #141414; border: 1px solid #2a2a2a; border-radius: 12px;
152
+ padding: 1rem; margin-bottom: 1.25rem;
153
+ }
154
+ .preset-row { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 0.75rem; }
155
+ .preset-btn {
156
+ padding: 8px 16px; border-radius: 20px; font-size: 0.75rem;
157
+ cursor: pointer; border: 1px solid #2a3a2a; background: #0d1a0d;
158
+ color: #7aaa7a; transition: all 0.2s;
159
+ }
160
+ .preset-btn:hover { border-color: #3a5a3a; color: #aad; }
161
+ .preset-btn.active { background: #1a3a1a; border-color: #3acc5a; color: #fff; }
162
+
120
163
  .status {
121
164
  font-size: 0.65rem; color: #444; text-align: center; padding-top: 0.5rem;
122
165
  }
@@ -124,22 +167,64 @@ export function getHTML() {
124
167
  </head>
125
168
  <body>
126
169
 
127
- <h1>Illuminate · 7×7 Civic Center</h1>
170
+ <h1>Wavegrid · Master Controller</h1>
171
+ <div class="subtitle">7×7 Civic Center Plaza · 49 cannons</div>
172
+
173
+ <div class="columns">
174
+ <div class="col-left">
128
175
 
129
176
  <div class="master">
130
177
  <div class="section-title">Scenes</div>
131
178
  <div class="scene-row" id="scene-row">
132
179
  ${sceneNames.map((name, i) => `<button class="scene-btn${i === 0 ? ' active' : ''}" data-scene="${name}">${name}</button>`).join('\n ')}
133
180
  </div>
134
- <div class="section-title" style="margin-top:0.75rem">Master</div>
181
+
182
+ <div class="section-title" style="margin-top:0.75rem">Animations</div>
183
+ <div class="scene-row" id="anim-row">
184
+ ${animationNames.map(name => `<button class="anim-btn" data-anim="${name}">${name}</button>`).join('\n ')}
185
+ <button class="anim-btn stop" id="anim-stop">stop</button>
186
+ </div>
187
+
188
+ <div class="section-title" style="margin-top:0.75rem">Envelope</div>
135
189
  <div class="slider-row">
136
190
  <label>Brightness</label>
137
191
  <input type="range" min="0" max="100" value="80" id="master-bright">
138
192
  <span class="val" id="master-bright-val">80%</span>
139
193
  </div>
194
+ <div class="slider-row">
195
+ <label>Smoothness</label>
196
+ <input type="range" min="0" max="100" value="50" id="master-smooth">
197
+ <span class="val" id="master-smooth-val">α 0.08</span>
198
+ </div>
199
+ <div class="slider-row">
200
+ <label>Attack</label>
201
+ <input type="range" min="0" max="100" value="100" id="master-attack">
202
+ <span class="val" id="master-attack-val">1.00</span>
203
+ </div>
204
+ <div class="slider-row">
205
+ <label>Idle timeout</label>
206
+ <input type="range" min="0" max="30" value="0" id="idle-timeout">
207
+ <span class="val" id="idle-timeout-val">off</span>
208
+ </div>
140
209
  <div class="all-off" id="all-off">All Off</div>
141
210
  </div>
142
211
 
212
+ <div class="ambient-section">
213
+ <div class="section-title">Ambient Presets — walk away mode</div>
214
+ <div class="preset-row" id="preset-row">
215
+ <button class="preset-btn" data-preset="civic-breathe">Civic Breathe</button>
216
+ <button class="preset-btn" data-preset="ocean-wave">Ocean Wave</button>
217
+ <button class="preset-btn" data-preset="sunset-spiral">Sunset Spiral</button>
218
+ <button class="preset-btn" data-preset="pride-rainbow">Pride Rainbow</button>
219
+ <button class="preset-btn" data-preset="night-rain">Night Rain</button>
220
+ <button class="preset-btn" data-preset="heartbeat">Heartbeat</button>
221
+ </div>
222
+ <div style="font-size:0.65rem;color:#555">Tap a preset → scene + animation + envelope are all set. Safe to walk away.</div>
223
+ </div>
224
+
225
+ </div>
226
+ <div class="col-right">
227
+
143
228
  <div class="grid-section">
144
229
  <div class="section-title">Cannon Grid — tap to select</div>
145
230
  <div class="sel-actions">
@@ -172,6 +257,18 @@ export function getHTML() {
172
257
  </div>
173
258
  </div>
174
259
 
260
+ <div class="telemetry" id="telemetry">
261
+ <div>state: <span class="val-live" id="t-state">idle</span></div>
262
+ <div>animation: <span class="val-anim" id="t-anim">none</span></div>
263
+ <div>alpha (smooth): <span class="val-live" id="t-alpha">0.080</span></div>
264
+ <div>attack: <span class="val-live" id="t-attack">1.000</span></div>
265
+ <div>idle: <span class="val-live" id="t-idle">0s</span> / timeout: <span class="val-live" id="t-idle-max">off</span></div>
266
+ <div>clients: <span class="val-live" id="t-clients">1</span></div>
267
+ </div>
268
+
269
+ </div>
270
+ </div>
271
+
175
272
  <div class="status" id="status">Connecting...</div>
176
273
 
177
274
  <script>
@@ -180,10 +277,16 @@ const ws = new WebSocket('ws://' + location.host);
180
277
  const status = document.getElementById('status');
181
278
  const selected = new Set();
182
279
 
183
- // Local display state (smoothly updated from server)
184
280
  const display = Array.from({length: NUM}, () => ({ h: 220, s: 90, b: 80 }));
185
281
 
186
- ws.onopen = () => { status.textContent = 'Connected · 49 cannons · smooth mode'; };
282
+ let currentAlpha = 0.08;
283
+ let currentAttack = 1.0;
284
+ let currentAnim = null;
285
+ let idleTimeout = 0;
286
+ let idleSeconds = 0;
287
+ let lastInputTime = Date.now();
288
+
289
+ ws.onopen = () => { status.textContent = 'Connected · 49 cannons · master controller'; };
187
290
  ws.onclose = () => { status.textContent = 'Disconnected — reload page'; };
188
291
 
189
292
  ws.onmessage = (e) => {
@@ -199,6 +302,7 @@ ws.onmessage = (e) => {
199
302
 
200
303
  function send(data) {
201
304
  if (ws.readyState === 1) ws.send(JSON.stringify(data));
305
+ lastInputTime = Date.now();
202
306
  }
203
307
 
204
308
  function hslToHex(h, s, l) {
@@ -290,21 +394,136 @@ document.getElementById('done-btn').addEventListener('click', () => {
290
394
  updatePanel();
291
395
  });
292
396
 
293
- // Master brightness
397
+ // ═══════════════════════════════════════════════════
398
+ // Master controls
399
+ // ═══════════════════════════════════════════════════
294
400
  document.getElementById('master-bright').addEventListener('input', function() {
295
401
  const v = parseInt(this.value);
296
402
  document.getElementById('master-bright-val').textContent = v + '%';
297
403
  send({ type: 'master_brightness', value: v / 100 });
298
404
  });
299
405
 
406
+ document.getElementById('master-smooth').addEventListener('input', function() {
407
+ const pct = parseInt(this.value) / 100;
408
+ const alpha = Math.pow(10, -2.7 * pct);
409
+ currentAlpha = Math.max(0.002, Math.min(1.0, alpha));
410
+ document.getElementById('master-smooth-val').textContent = 'α ' + currentAlpha.toFixed(3);
411
+ send({ type: 'smoothness', value: currentAlpha });
412
+ });
413
+
414
+ document.getElementById('master-attack').addEventListener('input', function() {
415
+ const pct = parseInt(this.value) / 100;
416
+ currentAttack = 0.05 + pct * 0.95;
417
+ document.getElementById('master-attack-val').textContent = currentAttack.toFixed(2);
418
+ send({ type: 'attack', value: currentAttack });
419
+ });
420
+
421
+ document.getElementById('idle-timeout').addEventListener('input', function() {
422
+ idleTimeout = parseInt(this.value);
423
+ document.getElementById('idle-timeout-val').textContent = idleTimeout === 0 ? 'off' : idleTimeout + 'min';
424
+ document.getElementById('t-idle-max').textContent = idleTimeout === 0 ? 'off' : idleTimeout + 'min';
425
+ });
426
+
300
427
  // All off
301
428
  document.getElementById('all-off').addEventListener('click', () => {
302
429
  document.getElementById('master-bright').value = 0;
303
430
  document.getElementById('master-bright-val').textContent = '0%';
304
431
  send({ type: 'master_brightness', value: 0 });
432
+ currentAnim = null;
433
+ document.querySelectorAll('.anim-btn').forEach(b => b.classList.remove('active'));
434
+ send({ type: 'animation', name: 'stop' });
435
+ });
436
+
437
+ // ═══════════════════════════════════════════════════
438
+ // Scenes
439
+ // ═══════════════════════════════════════════════════
440
+ document.getElementById('scene-row').addEventListener('click', (e) => {
441
+ const btn = e.target.closest('.scene-btn');
442
+ if (!btn) return;
443
+ document.querySelectorAll('.scene-btn').forEach(b => b.classList.remove('active'));
444
+ btn.classList.add('active');
445
+ send({ type: 'scene', name: btn.dataset.scene });
446
+ });
447
+
448
+ // ═══════════════════════════════════════════════════
449
+ // Animations
450
+ // ═══════════════════════════════════════════════════
451
+ document.getElementById('anim-row').addEventListener('click', (e) => {
452
+ const btn = e.target.closest('.anim-btn');
453
+ if (!btn) return;
454
+
455
+ if (btn.id === 'anim-stop' || btn.dataset.anim === currentAnim) {
456
+ // Stop
457
+ document.querySelectorAll('.anim-btn').forEach(b => b.classList.remove('active'));
458
+ currentAnim = null;
459
+ send({ type: 'animation', name: 'stop' });
460
+ document.getElementById('t-anim').textContent = 'none';
461
+ document.getElementById('t-state').textContent = 'idle';
462
+ } else {
463
+ document.querySelectorAll('.anim-btn').forEach(b => b.classList.remove('active'));
464
+ btn.classList.add('active');
465
+ currentAnim = btn.dataset.anim;
466
+ send({ type: 'animation', name: currentAnim });
467
+ document.getElementById('t-anim').textContent = currentAnim;
468
+ document.getElementById('t-state').textContent = 'animating';
469
+ }
470
+ });
471
+
472
+ // ═══════════════════════════════════════════════════
473
+ // Ambient Presets
474
+ // ═══════════════════════════════════════════════════
475
+ const PRESETS = {
476
+ 'civic-breathe': { scene: 'civic', anim: 'breathe', smooth: 75, attack: 30 },
477
+ 'ocean-wave': { scene: 'ocean', anim: 'wave', smooth: 70, attack: 40 },
478
+ 'sunset-spiral': { scene: 'sunset', anim: 'spiral', smooth: 60, attack: 50 },
479
+ 'pride-rainbow': { scene: 'pride', anim: 'rainbow', smooth: 55, attack: 60 },
480
+ 'night-rain': { scene: 'ocean', anim: 'rain', smooth: 80, attack: 25 },
481
+ 'heartbeat': { scene: 'off', anim: 'heartbeat', smooth: 40, attack: 80 },
482
+ };
483
+
484
+ let activePreset = null;
485
+ document.getElementById('preset-row').addEventListener('click', (e) => {
486
+ const btn = e.target.closest('.preset-btn');
487
+ if (!btn) return;
488
+ const key = btn.dataset.preset;
489
+ const preset = PRESETS[key];
490
+ if (!preset) return;
491
+
492
+ document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active'));
493
+ btn.classList.add('active');
494
+ activePreset = key;
495
+
496
+ // Apply scene
497
+ send({ type: 'scene', name: preset.scene });
498
+ document.querySelectorAll('.scene-btn').forEach(b => b.classList.remove('active'));
499
+ const sceneBtn = document.querySelector('[data-scene="' + preset.scene + '"]');
500
+ if (sceneBtn) sceneBtn.classList.add('active');
501
+
502
+ // Apply smoothness
503
+ const smoothSlider = document.getElementById('master-smooth');
504
+ smoothSlider.value = preset.smooth;
505
+ smoothSlider.dispatchEvent(new Event('input'));
506
+
507
+ // Apply attack
508
+ const attackSlider = document.getElementById('master-attack');
509
+ attackSlider.value = preset.attack;
510
+ attackSlider.dispatchEvent(new Event('input'));
511
+
512
+ // Start animation (slight delay so scene applies first)
513
+ setTimeout(() => {
514
+ send({ type: 'animation', name: preset.anim });
515
+ currentAnim = preset.anim;
516
+ document.querySelectorAll('.anim-btn').forEach(b => b.classList.remove('active'));
517
+ const animBtn = document.querySelector('[data-anim="' + preset.anim + '"]');
518
+ if (animBtn) animBtn.classList.add('active');
519
+ document.getElementById('t-anim').textContent = preset.anim;
520
+ document.getElementById('t-state').textContent = 'ambient: ' + key;
521
+ }, 100);
305
522
  });
306
523
 
524
+ // ═══════════════════════════════════════════════════
307
525
  // Per-cannon controls
526
+ // ═══════════════════════════════════════════════════
308
527
  document.getElementById('panel-bright').addEventListener('input', function() {
309
528
  const v = parseInt(this.value);
310
529
  document.getElementById('panel-bright-val').textContent = v + '%';
@@ -321,16 +540,28 @@ document.getElementById('panel-sat').addEventListener('input', function() {
321
540
  send({ type: 'selection', indices: [...selected], s: v });
322
541
  });
323
542
 
324
- // Scenes
325
- document.getElementById('scene-row').addEventListener('click', (e) => {
326
- const btn = e.target.closest('.scene-btn');
327
- if (!btn) return;
328
- document.querySelectorAll('.scene-btn').forEach(b => b.classList.remove('active'));
329
- btn.classList.add('active');
330
- send({ type: 'scene', name: btn.dataset.scene });
331
- });
543
+ // ═══════════════════════════════════════════════════
544
+ // Idle timeout — auto-switch to ambient
545
+ // ═══════════════════════════════════════════════════
546
+ setInterval(() => {
547
+ if (idleTimeout <= 0) {
548
+ idleSeconds = 0;
549
+ document.getElementById('t-idle').textContent = '0s';
550
+ return;
551
+ }
552
+ idleSeconds = Math.floor((Date.now() - lastInputTime) / 1000);
553
+ document.getElementById('t-idle').textContent = idleSeconds + 's';
554
+
555
+ if (idleSeconds >= idleTimeout * 60 && activePreset === null) {
556
+ // Auto-activate first preset on timeout
557
+ const firstBtn = document.querySelector('.preset-btn');
558
+ if (firstBtn) firstBtn.click();
559
+ }
560
+ }, 1000);
332
561
 
333
- // Render loop — updates beams from display state
562
+ // ═══════════════════════════════════════════════════
563
+ // Render loop
564
+ // ═══════════════════════════════════════════════════
334
565
  function render() {
335
566
  for (let i = 0; i < NUM; i++) {
336
567
  const c = display[i];
package/grid.d.ts CHANGED
@@ -22,5 +22,11 @@ export declare function createGrid(): CannonTarget[];
22
22
  * Called once per animation frame to smoothly converge current → target.
23
23
  */
24
24
  export declare function tickGrid(grid: CannonTarget[], alpha?: number): boolean;
25
- export declare function setCannonTarget(grid: CannonTarget[], index: number, h?: number, s?: number, b?: number): void;
26
- export declare function setAllTargets(grid: CannonTarget[], h?: number, s?: number, b?: number): void;
25
+ /**
26
+ * Set target for a single cannon.
27
+ * attack (0–1): how much of the new value to apply.
28
+ * 1.0 = full (instant snap to new target)
29
+ * 0.1 = soft (target blends 10% toward new value)
30
+ */
31
+ export declare function setCannonTarget(grid: CannonTarget[], index: number, h?: number, s?: number, b?: number, attack?: number): void;
32
+ export declare function setAllTargets(grid: CannonTarget[], h?: number, s?: number, b?: number, attack?: number): void;
package/grid.js CHANGED
@@ -55,17 +55,35 @@ function angleDelta(from, to) {
55
55
  let d = ((to - from + 540) % 360) - 180;
56
56
  return d;
57
57
  }
58
- function setCannonTarget(grid, index, h, s, b) {
58
+ /**
59
+ * Set target for a single cannon.
60
+ * attack (0–1): how much of the new value to apply.
61
+ * 1.0 = full (instant snap to new target)
62
+ * 0.1 = soft (target blends 10% toward new value)
63
+ */
64
+ function setCannonTarget(grid, index, h, s, b, attack = 1.0) {
59
65
  const c = grid[index];
60
- if (h !== undefined)
61
- c.targetH = h;
62
- if (s !== undefined)
63
- c.targetS = s;
64
- if (b !== undefined)
65
- c.targetB = b;
66
+ if (attack >= 1.0) {
67
+ if (h !== undefined)
68
+ c.targetH = h;
69
+ if (s !== undefined)
70
+ c.targetS = s;
71
+ if (b !== undefined)
72
+ c.targetB = b;
73
+ }
74
+ else {
75
+ if (h !== undefined) {
76
+ const dh = angleDelta(c.targetH, h);
77
+ c.targetH = (c.targetH + dh * attack + 360) % 360;
78
+ }
79
+ if (s !== undefined)
80
+ c.targetS = c.targetS + (s - c.targetS) * attack;
81
+ if (b !== undefined)
82
+ c.targetB = c.targetB + (b - c.targetB) * attack;
83
+ }
66
84
  }
67
- function setAllTargets(grid, h, s, b) {
85
+ function setAllTargets(grid, h, s, b, attack = 1.0) {
68
86
  for (let i = 0; i < grid.length; i++) {
69
- setCannonTarget(grid, i, h, s, b);
87
+ setCannonTarget(grid, i, h, s, b, attack);
70
88
  }
71
89
  }
package/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- export { createGrid, tickGrid, setCannonTarget, setAllTargets, NUM_CANNONS, GRID_SIZE, DEFAULT_ALPHA } from './grid';
2
- export { applyScene, scenes } from './scenes';
1
+ export type { AnimationFn } from './animations';
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
5
  export type { SceneColor, SceneGenerator } from './scenes';
6
+ export { applyScene, scenes } from './scenes';
package/index.js CHANGED
@@ -1,14 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.scenes = exports.applyScene = exports.DEFAULT_ALPHA = exports.GRID_SIZE = exports.NUM_CANNONS = exports.setAllTargets = exports.setCannonTarget = exports.tickGrid = exports.createGrid = void 0;
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;
4
+ var animations_1 = require("./animations");
5
+ Object.defineProperty(exports, "animations", { enumerable: true, get: function () { return animations_1.animations; } });
6
+ Object.defineProperty(exports, "getAnimationNames", { enumerable: true, get: function () { return animations_1.getAnimationNames; } });
4
7
  var grid_1 = require("./grid");
5
8
  Object.defineProperty(exports, "createGrid", { enumerable: true, get: function () { return grid_1.createGrid; } });
6
- Object.defineProperty(exports, "tickGrid", { enumerable: true, get: function () { return grid_1.tickGrid; } });
7
- Object.defineProperty(exports, "setCannonTarget", { enumerable: true, get: function () { return grid_1.setCannonTarget; } });
8
- Object.defineProperty(exports, "setAllTargets", { enumerable: true, get: function () { return grid_1.setAllTargets; } });
9
- Object.defineProperty(exports, "NUM_CANNONS", { enumerable: true, get: function () { return grid_1.NUM_CANNONS; } });
10
- Object.defineProperty(exports, "GRID_SIZE", { enumerable: true, get: function () { return grid_1.GRID_SIZE; } });
11
9
  Object.defineProperty(exports, "DEFAULT_ALPHA", { enumerable: true, get: function () { return grid_1.DEFAULT_ALPHA; } });
10
+ Object.defineProperty(exports, "GRID_SIZE", { enumerable: true, get: function () { return grid_1.GRID_SIZE; } });
11
+ Object.defineProperty(exports, "NUM_CANNONS", { enumerable: true, get: function () { return grid_1.NUM_CANNONS; } });
12
+ Object.defineProperty(exports, "setAllTargets", { enumerable: true, get: function () { return grid_1.setAllTargets; } });
13
+ Object.defineProperty(exports, "setCannonTarget", { enumerable: true, get: function () { return grid_1.setCannonTarget; } });
14
+ Object.defineProperty(exports, "tickGrid", { enumerable: true, get: function () { return grid_1.tickGrid; } });
12
15
  var scenes_1 = require("./scenes");
13
16
  Object.defineProperty(exports, "applyScene", { enumerable: true, get: function () { return scenes_1.applyScene; } });
14
17
  Object.defineProperty(exports, "scenes", { enumerable: true, get: function () { return scenes_1.scenes; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavegrid/simulator",
3
- "version": "0.1.1",
3
+ "version": "0.2.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": "6f651b8d0dfbb6538c667dd3905ce6989ae18e46"
46
+ "gitHead": "579bfca2fe84231a6722d3b4bfd44d1f42a0c9ef"
47
47
  }
package/server.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import http from 'http';
2
- import { CannonTarget } from './grid';
3
- declare const grid: CannonTarget[];
2
+ declare const grid: import("./grid").CannonTarget[];
4
3
  declare const server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
5
- export { server, grid };
4
+ export { grid, server };
package/server.js CHANGED
@@ -3,9 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.grid = exports.server = void 0;
6
+ exports.server = exports.grid = void 0;
7
7
  const http_1 = __importDefault(require("http"));
8
8
  const ws_1 = require("ws");
9
+ const animations_1 = require("./animations");
9
10
  const grid_1 = require("./grid");
10
11
  const scenes_1 = require("./scenes");
11
12
  const ui_1 = require("./ui");
@@ -13,7 +14,20 @@ const PORT = parseInt(process.env.PORT || '3000', 10);
13
14
  const TICK_MS = 1000 / 60; // 60fps interpolation
14
15
  const grid = (0, grid_1.createGrid)();
15
16
  exports.grid = grid;
16
- const server = http_1.default.createServer((_req, res) => {
17
+ let currentAlpha = grid_1.DEFAULT_ALPHA;
18
+ let currentAttack = 1.0;
19
+ let currentAnimation = null;
20
+ let animationTick = 0;
21
+ // constructive.io brand mark — served as the favicon
22
+ const FAVICON_SVG = `<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
23
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M23.3315 21.7046V28.9348L26.2354 27.2232L29.8206 25.1157L36.9909 29.3307V37.761L30.1657 41.7731V41.785L22.9955 46L19.4102 43.8925L15.8343 41.7848V41.7749L12.5759 39.8595L9 37.7518V21.2924L12.5759 19.1847L15.8343 17.2694V9.25722L19.4102 7.14956L22.6685 5.23418V5.21521L26.2445 3.10755L29.8297 1L37 5.21499V13.6453L30.1657 17.6628V17.6873L23.3315 21.7046ZM16.16 17.8789L12.9168 19.7854L10.0443 21.4784L16.0542 25.0113L22.2948 21.4903L19.4101 19.7945L16.16 17.8789ZM23.6598 5.43249L29.7813 9.0309L35.955 5.40169L33.0743 3.70829L29.8297 1.80095L26.5853 3.70818L23.6598 5.43249ZM22.5139 38.2327L16.8333 41.5721L19.7511 43.2918L22.5185 44.9187L22.5196 38.2427L22.5139 38.2327ZM29.0399 33.6349L29.0153 33.5916L29.1105 33.5357L26.24 31.8482L23.3405 30.1438V33.546V36.9854L29.0399 33.6349ZM29.0998 9.43154L26.24 7.75041L22.9953 5.84307L19.7509 7.7503L16.8486 9.461L22.97 13.0595L29.0348 9.49437L29.0244 9.47595L29.0998 9.43154ZM16.5153 10.0661V13.4722V17.2854L22.5224 20.8167L22.5236 13.598L16.5153 10.0661ZM35.9458 29.5176L33.0651 27.8242L29.8205 25.9168L26.5761 27.8241L23.6705 29.5367L29.7919 33.1352L35.9458 29.5176ZM15.794 33.7218L12.5758 31.8299L9.68105 30.1237V33.5369V37.3517L12.9167 39.2589L15.7928 40.9496L15.794 33.7218ZM15.7954 25.7332L9.68116 22.1389V25.5074V29.3222L12.9168 31.2293L15.7943 32.9208L15.7954 25.7332Z" fill="#01A1FF"/>
24
+ </svg>`;
25
+ const server = http_1.default.createServer((req, res) => {
26
+ if (req.url === '/favicon.svg' || req.url === '/favicon.ico') {
27
+ res.writeHead(200, { 'Content-Type': 'image/svg+xml; charset=utf-8' });
28
+ res.end(FAVICON_SVG);
29
+ return;
30
+ }
17
31
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
18
32
  res.end((0, ui_1.getHTML)());
19
33
  });
@@ -49,30 +63,54 @@ wss.on('connection', (ws) => {
49
63
  function handleMessage(msg) {
50
64
  switch (msg.type) {
51
65
  case 'cannon':
52
- (0, grid_1.setCannonTarget)(grid, msg.index, msg.h ?? undefined, msg.s ?? undefined, msg.b ?? undefined);
66
+ (0, grid_1.setCannonTarget)(grid, msg.index, msg.h ?? undefined, msg.s ?? undefined, msg.b ?? undefined, currentAttack);
53
67
  break;
54
68
  case 'master_brightness':
55
- (0, grid_1.setAllTargets)(grid, undefined, undefined, msg.value * 100);
69
+ (0, grid_1.setAllTargets)(grid, undefined, undefined, msg.value * 100, currentAttack);
56
70
  break;
57
71
  case 'scene':
58
72
  if (msg.name && scenes_1.scenes[msg.name]) {
73
+ currentAnimation = null;
59
74
  (0, scenes_1.applyScene)(grid, msg.name);
60
75
  }
61
76
  break;
77
+ case 'animation':
78
+ if (msg.name && animations_1.animations[msg.name]) {
79
+ currentAnimation = msg.name;
80
+ animationTick = 0;
81
+ }
82
+ else if (msg.name === 'stop') {
83
+ currentAnimation = null;
84
+ }
85
+ break;
62
86
  case 'selection':
63
87
  if (Array.isArray(msg.indices)) {
64
88
  for (const idx of msg.indices) {
65
89
  if (idx >= 0 && idx < grid.length) {
66
- (0, grid_1.setCannonTarget)(grid, idx, msg.h ?? undefined, msg.s ?? undefined, msg.b ?? undefined);
90
+ (0, grid_1.setCannonTarget)(grid, idx, msg.h ?? undefined, msg.s ?? undefined, msg.b ?? undefined, currentAttack);
67
91
  }
68
92
  }
69
93
  }
70
94
  break;
95
+ case 'smoothness':
96
+ if (typeof msg.value === 'number') {
97
+ currentAlpha = msg.value;
98
+ }
99
+ break;
100
+ case 'attack':
101
+ if (typeof msg.value === 'number') {
102
+ currentAttack = msg.value;
103
+ }
104
+ break;
71
105
  }
72
106
  }
73
107
  // Animation loop: tick interpolation and broadcast
74
108
  setInterval(() => {
75
- const changed = (0, grid_1.tickGrid)(grid);
109
+ if (currentAnimation && animations_1.animations[currentAnimation]) {
110
+ animations_1.animations[currentAnimation](grid, animationTick, currentAttack);
111
+ animationTick++;
112
+ }
113
+ const changed = (0, grid_1.tickGrid)(grid, currentAlpha);
76
114
  if (changed) {
77
115
  broadcastState();
78
116
  }