@wavegrid/canvas 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/server.js +10 -1
- package/esm/ui.js +109 -2
- package/package.json +2 -2
- package/server.js +10 -1
- package/ui.js +109 -2
package/esm/server.js
CHANGED
|
@@ -3,7 +3,16 @@ import { WebSocket, WebSocketServer } from 'ws';
|
|
|
3
3
|
import { getCanvasHTML } from './ui';
|
|
4
4
|
const PORT = parseInt(process.env.PORT || '3001', 10);
|
|
5
5
|
const SIMULATOR_URL = process.env.SIMULATOR_URL || 'ws://localhost:3000';
|
|
6
|
-
|
|
6
|
+
// constructive.io brand mark — served as the favicon
|
|
7
|
+
const FAVICON_SVG = `<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
8
|
+
<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"/>
|
|
9
|
+
</svg>`;
|
|
10
|
+
const server = http.createServer((req, res) => {
|
|
11
|
+
if (req.url === '/favicon.svg' || req.url === '/favicon.ico') {
|
|
12
|
+
res.writeHead(200, { 'Content-Type': 'image/svg+xml; charset=utf-8' });
|
|
13
|
+
res.end(FAVICON_SVG);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
7
16
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
8
17
|
res.end(getCanvasHTML());
|
|
9
18
|
});
|
package/esm/ui.js
CHANGED
|
@@ -6,6 +6,7 @@ export function getCanvasHTML() {
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
|
7
7
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
8
8
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
9
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
9
10
|
<title>Illuminate</title>
|
|
10
11
|
<style>
|
|
11
12
|
:root {
|
|
@@ -36,6 +37,9 @@ export function getCanvasHTML() {
|
|
|
36
37
|
padding: 12px 20px; flex-shrink: 0;
|
|
37
38
|
background: var(--surface); border-bottom: 1px solid var(--border);
|
|
38
39
|
}
|
|
40
|
+
.scene-label-wrap {
|
|
41
|
+
min-width: 120px; flex-shrink: 0;
|
|
42
|
+
}
|
|
39
43
|
.scene-label {
|
|
40
44
|
font-size: 13px; font-weight: 500; color: var(--text2);
|
|
41
45
|
letter-spacing: 0.04em;
|
|
@@ -55,6 +59,27 @@ export function getCanvasHTML() {
|
|
|
55
59
|
box-shadow: 0 0 12px var(--glow); cursor: pointer;
|
|
56
60
|
}
|
|
57
61
|
.energy-val { font-size: 12px; color: var(--text2); min-width: 32px; text-align: right; }
|
|
62
|
+
.global-controls {
|
|
63
|
+
display: flex; align-items: center; gap: 16px;
|
|
64
|
+
}
|
|
65
|
+
.ctrl-wrap {
|
|
66
|
+
display: flex; align-items: center; gap: 6px;
|
|
67
|
+
}
|
|
68
|
+
.ctrl-icon { font-size: 12px; opacity: 0.5; }
|
|
69
|
+
.ctrl-label { font-size: 9px; color: var(--text2); letter-spacing: 0.05em; text-transform: uppercase; }
|
|
70
|
+
.ctrl-slider {
|
|
71
|
+
width: 80px; height: 4px; -webkit-appearance: none; appearance: none;
|
|
72
|
+
background: linear-gradient(to right, var(--accent), #1a1a2a);
|
|
73
|
+
border-radius: 2px; outline: none;
|
|
74
|
+
}
|
|
75
|
+
.ctrl-slider::-webkit-slider-thumb {
|
|
76
|
+
-webkit-appearance: none; width: 16px; height: 16px;
|
|
77
|
+
border-radius: 50%; background: var(--text);
|
|
78
|
+
box-shadow: 0 0 6px rgba(255,255,255,0.2); cursor: pointer;
|
|
79
|
+
}
|
|
80
|
+
.ctrl-slider.attack {
|
|
81
|
+
background: linear-gradient(to right, #1a1a2a, var(--accent));
|
|
82
|
+
}
|
|
58
83
|
|
|
59
84
|
/* ─── Sculpture Canvas ─── */
|
|
60
85
|
.sculpture-wrap {
|
|
@@ -276,7 +301,7 @@ export function getCanvasHTML() {
|
|
|
276
301
|
|
|
277
302
|
<!-- ─── Top Bar ─── -->
|
|
278
303
|
<div class="top-bar">
|
|
279
|
-
<div style="display:flex;align-items:center;gap:8px">
|
|
304
|
+
<div class="scene-label-wrap" style="display:flex;align-items:center;gap:8px">
|
|
280
305
|
<div class="status-dot" id="status-dot"></div>
|
|
281
306
|
<span class="scene-label" id="scene-label">Civic Blue</span>
|
|
282
307
|
</div>
|
|
@@ -285,6 +310,16 @@ export function getCanvasHTML() {
|
|
|
285
310
|
<input type="range" class="energy-slider" id="energy" min="0" max="100" value="80">
|
|
286
311
|
<span class="energy-val" id="energy-val">80</span>
|
|
287
312
|
</div>
|
|
313
|
+
<div class="global-controls">
|
|
314
|
+
<div class="ctrl-wrap">
|
|
315
|
+
<span class="ctrl-label">Smooth</span>
|
|
316
|
+
<input type="range" class="ctrl-slider" id="smoothness" min="0" max="100" value="50">
|
|
317
|
+
</div>
|
|
318
|
+
<div class="ctrl-wrap">
|
|
319
|
+
<span class="ctrl-label">Attack</span>
|
|
320
|
+
<input type="range" class="ctrl-slider attack" id="attack" min="0" max="100" value="100">
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
288
323
|
</div>
|
|
289
324
|
|
|
290
325
|
<!-- ─── Sculpture Canvas ─── -->
|
|
@@ -300,6 +335,7 @@ export function getCanvasHTML() {
|
|
|
300
335
|
<div class="mode-tab" data-mode="brush">Brush</div>
|
|
301
336
|
<div class="mode-tab" data-mode="energy">Energy</div>
|
|
302
337
|
<div class="mode-tab" data-mode="scenes">Scenes</div>
|
|
338
|
+
<div class="mode-tab" data-mode="animations">Animations</div>
|
|
303
339
|
<div class="mode-tab" data-mode="motion">Motion</div>
|
|
304
340
|
<div class="mode-tab" data-mode="drops">Drops</div>
|
|
305
341
|
<div class="mode-tab" data-mode="symmetry">Symmetry</div>
|
|
@@ -365,6 +401,11 @@ export function getCanvasHTML() {
|
|
|
365
401
|
<div class="scene-palette" id="scene-palette"></div>
|
|
366
402
|
</div>
|
|
367
403
|
|
|
404
|
+
<!-- Animations -->
|
|
405
|
+
<div class="tool-panel" id="panel-animations">
|
|
406
|
+
<div class="scene-palette" id="animation-palette"></div>
|
|
407
|
+
</div>
|
|
408
|
+
|
|
368
409
|
<!-- Motion -->
|
|
369
410
|
<div class="tool-panel" id="panel-motion">
|
|
370
411
|
<div class="motion-controls">
|
|
@@ -432,6 +473,7 @@ let currentBright = 80;
|
|
|
432
473
|
let brushSize = 1;
|
|
433
474
|
let brushFalloff = false;
|
|
434
475
|
let activeScene = 'civic';
|
|
476
|
+
let activeAnimation = null;
|
|
435
477
|
let symmetry = { h: false, v: false, radial: false, kaleidoscope: false };
|
|
436
478
|
|
|
437
479
|
// Motion painter state
|
|
@@ -912,6 +954,30 @@ document.getElementById('energy-full').addEventListener('input', function() {
|
|
|
912
954
|
send({ type: 'master_brightness', value: parseInt(this.value) / 100 });
|
|
913
955
|
});
|
|
914
956
|
|
|
957
|
+
// ═══════════════════════════════════════════════════
|
|
958
|
+
// Smoothness control (global low-pass filter amount)
|
|
959
|
+
// ═══════════════════════════════════════════════════
|
|
960
|
+
document.getElementById('smoothness').addEventListener('input', function() {
|
|
961
|
+
// Slider 0–100 maps to alpha 1.0 (instant) → 0.002 (glacially smooth)
|
|
962
|
+
// Exponential curve: 10^(-2.7 * pct) gives wide usable range
|
|
963
|
+
// pct=0 → alpha=1.0 (no filter, instant)
|
|
964
|
+
// pct=0.5 → alpha≈0.045 (smooth, ~2s transitions)
|
|
965
|
+
// pct=1.0 → alpha≈0.002 (ultra-smooth, ~8-10s transitions)
|
|
966
|
+
const pct = parseInt(this.value) / 100;
|
|
967
|
+
const alpha = Math.pow(10, -2.7 * pct);
|
|
968
|
+
send({ type: 'smoothness', value: Math.max(0.002, Math.min(1.0, alpha)) });
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
document.getElementById('attack').addEventListener('input', function() {
|
|
972
|
+
// Slider 0–100 maps to attack 0.05 (soft) → 1.0 (full/instant)
|
|
973
|
+
// Controls how much of a new input overrides the current target per event
|
|
974
|
+
// Low attack = new inputs blend in gradually (soft onset)
|
|
975
|
+
// High attack = new inputs snap immediately to full target
|
|
976
|
+
const pct = parseInt(this.value) / 100;
|
|
977
|
+
const attack = 0.05 + pct * 0.95;
|
|
978
|
+
send({ type: 'attack', value: attack });
|
|
979
|
+
});
|
|
980
|
+
|
|
915
981
|
// ═══════════════════════════════════════════════════
|
|
916
982
|
// Scene Palette
|
|
917
983
|
// ═══════════════════════════════════════════════════
|
|
@@ -936,15 +1002,56 @@ for (const [key, scene] of Object.entries(SCENES)) {
|
|
|
936
1002
|
swatch.style.background = gradient;
|
|
937
1003
|
swatch.innerHTML = '<div class="scene-swatch-label">' + scene.label + '</div>';
|
|
938
1004
|
swatch.addEventListener('click', () => {
|
|
939
|
-
document.querySelectorAll('.scene-swatch').forEach(s => s.classList.remove('active'));
|
|
1005
|
+
document.querySelectorAll('#scene-palette .scene-swatch').forEach(s => s.classList.remove('active'));
|
|
940
1006
|
swatch.classList.add('active');
|
|
941
1007
|
activeScene = key;
|
|
1008
|
+
activeAnimation = null;
|
|
1009
|
+
if (animPalette) animPalette.querySelectorAll('.scene-swatch').forEach(s => s.classList.remove('active'));
|
|
942
1010
|
document.getElementById('scene-label').textContent = scene.label;
|
|
943
1011
|
send({ type: 'scene', name: key });
|
|
944
1012
|
});
|
|
945
1013
|
scenePalette.appendChild(swatch);
|
|
946
1014
|
}
|
|
947
1015
|
|
|
1016
|
+
// ═══════════════════════════════════════════════════
|
|
1017
|
+
// Animation Palette
|
|
1018
|
+
// ═══════════════════════════════════════════════════
|
|
1019
|
+
const ANIMATIONS = {
|
|
1020
|
+
wave: { label: 'Wave', colors: ['#1a5acc', '#3af5e8', '#1a5acc'] },
|
|
1021
|
+
breathe: { label: 'Breathe', colors: ['#1a3a7a', '#4a7cff', '#1a3a7a'] },
|
|
1022
|
+
rainbow: { label: 'Rainbow', colors: ['#e33', '#f90', '#ee0', '#3a5', '#35e', '#a3e'] },
|
|
1023
|
+
pacman: { label: 'Pac-Man', colors: ['#111', '#fc0', '#111'] },
|
|
1024
|
+
spiral: { label: 'Spiral', colors: ['#e33', '#3ae', '#e3a', '#3ea'] },
|
|
1025
|
+
rain: { label: 'Rain', colors: ['#0a1525', '#2a6acc', '#0a1525'] },
|
|
1026
|
+
heartbeat: { label: 'Heartbeat', colors: ['#200', '#e22', '#200'] },
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
const animPalette = document.getElementById('animation-palette');
|
|
1030
|
+
for (const [key, anim] of Object.entries(ANIMATIONS)) {
|
|
1031
|
+
const swatch = document.createElement('div');
|
|
1032
|
+
swatch.className = 'scene-swatch';
|
|
1033
|
+
const gradient = anim.colors.length > 1
|
|
1034
|
+
? 'linear-gradient(135deg, ' + anim.colors.join(', ') + ')'
|
|
1035
|
+
: anim.colors[0];
|
|
1036
|
+
swatch.style.background = gradient;
|
|
1037
|
+
swatch.innerHTML = '<div class="scene-swatch-label">' + anim.label + '</div>';
|
|
1038
|
+
swatch.addEventListener('click', () => {
|
|
1039
|
+
animPalette.querySelectorAll('.scene-swatch').forEach(s => s.classList.remove('active'));
|
|
1040
|
+
if (activeAnimation === key) {
|
|
1041
|
+
// Tap again to stop
|
|
1042
|
+
activeAnimation = null;
|
|
1043
|
+
send({ type: 'animation', name: 'stop' });
|
|
1044
|
+
document.getElementById('scene-label').textContent = 'Stopped';
|
|
1045
|
+
} else {
|
|
1046
|
+
swatch.classList.add('active');
|
|
1047
|
+
activeAnimation = key;
|
|
1048
|
+
document.getElementById('scene-label').textContent = anim.label;
|
|
1049
|
+
send({ type: 'animation', name: key });
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
animPalette.appendChild(swatch);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
948
1055
|
// ═══════════════════════════════════════════════════
|
|
949
1056
|
// Brush controls
|
|
950
1057
|
// ═══════════════════════════════════════════════════
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wavegrid/canvas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"author": "Dan Lynch <pyramation@gmail.com>",
|
|
5
5
|
"description": "Artist-facing creative canvas for painting with light on the 7×7 grid",
|
|
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": "579bfca2fe84231a6722d3b4bfd44d1f42a0c9ef"
|
|
47
47
|
}
|
package/server.js
CHANGED
|
@@ -9,7 +9,16 @@ const ws_1 = require("ws");
|
|
|
9
9
|
const ui_1 = require("./ui");
|
|
10
10
|
const PORT = parseInt(process.env.PORT || '3001', 10);
|
|
11
11
|
const SIMULATOR_URL = process.env.SIMULATOR_URL || 'ws://localhost:3000';
|
|
12
|
-
|
|
12
|
+
// constructive.io brand mark — served as the favicon
|
|
13
|
+
const FAVICON_SVG = `<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
14
|
+
<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"/>
|
|
15
|
+
</svg>`;
|
|
16
|
+
const server = http_1.default.createServer((req, res) => {
|
|
17
|
+
if (req.url === '/favicon.svg' || req.url === '/favicon.ico') {
|
|
18
|
+
res.writeHead(200, { 'Content-Type': 'image/svg+xml; charset=utf-8' });
|
|
19
|
+
res.end(FAVICON_SVG);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
13
22
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
14
23
|
res.end((0, ui_1.getCanvasHTML)());
|
|
15
24
|
});
|
package/ui.js
CHANGED
|
@@ -9,6 +9,7 @@ function getCanvasHTML() {
|
|
|
9
9
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
|
10
10
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
11
11
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
12
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
12
13
|
<title>Illuminate</title>
|
|
13
14
|
<style>
|
|
14
15
|
:root {
|
|
@@ -39,6 +40,9 @@ function getCanvasHTML() {
|
|
|
39
40
|
padding: 12px 20px; flex-shrink: 0;
|
|
40
41
|
background: var(--surface); border-bottom: 1px solid var(--border);
|
|
41
42
|
}
|
|
43
|
+
.scene-label-wrap {
|
|
44
|
+
min-width: 120px; flex-shrink: 0;
|
|
45
|
+
}
|
|
42
46
|
.scene-label {
|
|
43
47
|
font-size: 13px; font-weight: 500; color: var(--text2);
|
|
44
48
|
letter-spacing: 0.04em;
|
|
@@ -58,6 +62,27 @@ function getCanvasHTML() {
|
|
|
58
62
|
box-shadow: 0 0 12px var(--glow); cursor: pointer;
|
|
59
63
|
}
|
|
60
64
|
.energy-val { font-size: 12px; color: var(--text2); min-width: 32px; text-align: right; }
|
|
65
|
+
.global-controls {
|
|
66
|
+
display: flex; align-items: center; gap: 16px;
|
|
67
|
+
}
|
|
68
|
+
.ctrl-wrap {
|
|
69
|
+
display: flex; align-items: center; gap: 6px;
|
|
70
|
+
}
|
|
71
|
+
.ctrl-icon { font-size: 12px; opacity: 0.5; }
|
|
72
|
+
.ctrl-label { font-size: 9px; color: var(--text2); letter-spacing: 0.05em; text-transform: uppercase; }
|
|
73
|
+
.ctrl-slider {
|
|
74
|
+
width: 80px; height: 4px; -webkit-appearance: none; appearance: none;
|
|
75
|
+
background: linear-gradient(to right, var(--accent), #1a1a2a);
|
|
76
|
+
border-radius: 2px; outline: none;
|
|
77
|
+
}
|
|
78
|
+
.ctrl-slider::-webkit-slider-thumb {
|
|
79
|
+
-webkit-appearance: none; width: 16px; height: 16px;
|
|
80
|
+
border-radius: 50%; background: var(--text);
|
|
81
|
+
box-shadow: 0 0 6px rgba(255,255,255,0.2); cursor: pointer;
|
|
82
|
+
}
|
|
83
|
+
.ctrl-slider.attack {
|
|
84
|
+
background: linear-gradient(to right, #1a1a2a, var(--accent));
|
|
85
|
+
}
|
|
61
86
|
|
|
62
87
|
/* ─── Sculpture Canvas ─── */
|
|
63
88
|
.sculpture-wrap {
|
|
@@ -279,7 +304,7 @@ function getCanvasHTML() {
|
|
|
279
304
|
|
|
280
305
|
<!-- ─── Top Bar ─── -->
|
|
281
306
|
<div class="top-bar">
|
|
282
|
-
<div style="display:flex;align-items:center;gap:8px">
|
|
307
|
+
<div class="scene-label-wrap" style="display:flex;align-items:center;gap:8px">
|
|
283
308
|
<div class="status-dot" id="status-dot"></div>
|
|
284
309
|
<span class="scene-label" id="scene-label">Civic Blue</span>
|
|
285
310
|
</div>
|
|
@@ -288,6 +313,16 @@ function getCanvasHTML() {
|
|
|
288
313
|
<input type="range" class="energy-slider" id="energy" min="0" max="100" value="80">
|
|
289
314
|
<span class="energy-val" id="energy-val">80</span>
|
|
290
315
|
</div>
|
|
316
|
+
<div class="global-controls">
|
|
317
|
+
<div class="ctrl-wrap">
|
|
318
|
+
<span class="ctrl-label">Smooth</span>
|
|
319
|
+
<input type="range" class="ctrl-slider" id="smoothness" min="0" max="100" value="50">
|
|
320
|
+
</div>
|
|
321
|
+
<div class="ctrl-wrap">
|
|
322
|
+
<span class="ctrl-label">Attack</span>
|
|
323
|
+
<input type="range" class="ctrl-slider attack" id="attack" min="0" max="100" value="100">
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
291
326
|
</div>
|
|
292
327
|
|
|
293
328
|
<!-- ─── Sculpture Canvas ─── -->
|
|
@@ -303,6 +338,7 @@ function getCanvasHTML() {
|
|
|
303
338
|
<div class="mode-tab" data-mode="brush">Brush</div>
|
|
304
339
|
<div class="mode-tab" data-mode="energy">Energy</div>
|
|
305
340
|
<div class="mode-tab" data-mode="scenes">Scenes</div>
|
|
341
|
+
<div class="mode-tab" data-mode="animations">Animations</div>
|
|
306
342
|
<div class="mode-tab" data-mode="motion">Motion</div>
|
|
307
343
|
<div class="mode-tab" data-mode="drops">Drops</div>
|
|
308
344
|
<div class="mode-tab" data-mode="symmetry">Symmetry</div>
|
|
@@ -368,6 +404,11 @@ function getCanvasHTML() {
|
|
|
368
404
|
<div class="scene-palette" id="scene-palette"></div>
|
|
369
405
|
</div>
|
|
370
406
|
|
|
407
|
+
<!-- Animations -->
|
|
408
|
+
<div class="tool-panel" id="panel-animations">
|
|
409
|
+
<div class="scene-palette" id="animation-palette"></div>
|
|
410
|
+
</div>
|
|
411
|
+
|
|
371
412
|
<!-- Motion -->
|
|
372
413
|
<div class="tool-panel" id="panel-motion">
|
|
373
414
|
<div class="motion-controls">
|
|
@@ -435,6 +476,7 @@ let currentBright = 80;
|
|
|
435
476
|
let brushSize = 1;
|
|
436
477
|
let brushFalloff = false;
|
|
437
478
|
let activeScene = 'civic';
|
|
479
|
+
let activeAnimation = null;
|
|
438
480
|
let symmetry = { h: false, v: false, radial: false, kaleidoscope: false };
|
|
439
481
|
|
|
440
482
|
// Motion painter state
|
|
@@ -915,6 +957,30 @@ document.getElementById('energy-full').addEventListener('input', function() {
|
|
|
915
957
|
send({ type: 'master_brightness', value: parseInt(this.value) / 100 });
|
|
916
958
|
});
|
|
917
959
|
|
|
960
|
+
// ═══════════════════════════════════════════════════
|
|
961
|
+
// Smoothness control (global low-pass filter amount)
|
|
962
|
+
// ═══════════════════════════════════════════════════
|
|
963
|
+
document.getElementById('smoothness').addEventListener('input', function() {
|
|
964
|
+
// Slider 0–100 maps to alpha 1.0 (instant) → 0.002 (glacially smooth)
|
|
965
|
+
// Exponential curve: 10^(-2.7 * pct) gives wide usable range
|
|
966
|
+
// pct=0 → alpha=1.0 (no filter, instant)
|
|
967
|
+
// pct=0.5 → alpha≈0.045 (smooth, ~2s transitions)
|
|
968
|
+
// pct=1.0 → alpha≈0.002 (ultra-smooth, ~8-10s transitions)
|
|
969
|
+
const pct = parseInt(this.value) / 100;
|
|
970
|
+
const alpha = Math.pow(10, -2.7 * pct);
|
|
971
|
+
send({ type: 'smoothness', value: Math.max(0.002, Math.min(1.0, alpha)) });
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
document.getElementById('attack').addEventListener('input', function() {
|
|
975
|
+
// Slider 0–100 maps to attack 0.05 (soft) → 1.0 (full/instant)
|
|
976
|
+
// Controls how much of a new input overrides the current target per event
|
|
977
|
+
// Low attack = new inputs blend in gradually (soft onset)
|
|
978
|
+
// High attack = new inputs snap immediately to full target
|
|
979
|
+
const pct = parseInt(this.value) / 100;
|
|
980
|
+
const attack = 0.05 + pct * 0.95;
|
|
981
|
+
send({ type: 'attack', value: attack });
|
|
982
|
+
});
|
|
983
|
+
|
|
918
984
|
// ═══════════════════════════════════════════════════
|
|
919
985
|
// Scene Palette
|
|
920
986
|
// ═══════════════════════════════════════════════════
|
|
@@ -939,15 +1005,56 @@ for (const [key, scene] of Object.entries(SCENES)) {
|
|
|
939
1005
|
swatch.style.background = gradient;
|
|
940
1006
|
swatch.innerHTML = '<div class="scene-swatch-label">' + scene.label + '</div>';
|
|
941
1007
|
swatch.addEventListener('click', () => {
|
|
942
|
-
document.querySelectorAll('.scene-swatch').forEach(s => s.classList.remove('active'));
|
|
1008
|
+
document.querySelectorAll('#scene-palette .scene-swatch').forEach(s => s.classList.remove('active'));
|
|
943
1009
|
swatch.classList.add('active');
|
|
944
1010
|
activeScene = key;
|
|
1011
|
+
activeAnimation = null;
|
|
1012
|
+
if (animPalette) animPalette.querySelectorAll('.scene-swatch').forEach(s => s.classList.remove('active'));
|
|
945
1013
|
document.getElementById('scene-label').textContent = scene.label;
|
|
946
1014
|
send({ type: 'scene', name: key });
|
|
947
1015
|
});
|
|
948
1016
|
scenePalette.appendChild(swatch);
|
|
949
1017
|
}
|
|
950
1018
|
|
|
1019
|
+
// ═══════════════════════════════════════════════════
|
|
1020
|
+
// Animation Palette
|
|
1021
|
+
// ═══════════════════════════════════════════════════
|
|
1022
|
+
const ANIMATIONS = {
|
|
1023
|
+
wave: { label: 'Wave', colors: ['#1a5acc', '#3af5e8', '#1a5acc'] },
|
|
1024
|
+
breathe: { label: 'Breathe', colors: ['#1a3a7a', '#4a7cff', '#1a3a7a'] },
|
|
1025
|
+
rainbow: { label: 'Rainbow', colors: ['#e33', '#f90', '#ee0', '#3a5', '#35e', '#a3e'] },
|
|
1026
|
+
pacman: { label: 'Pac-Man', colors: ['#111', '#fc0', '#111'] },
|
|
1027
|
+
spiral: { label: 'Spiral', colors: ['#e33', '#3ae', '#e3a', '#3ea'] },
|
|
1028
|
+
rain: { label: 'Rain', colors: ['#0a1525', '#2a6acc', '#0a1525'] },
|
|
1029
|
+
heartbeat: { label: 'Heartbeat', colors: ['#200', '#e22', '#200'] },
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
const animPalette = document.getElementById('animation-palette');
|
|
1033
|
+
for (const [key, anim] of Object.entries(ANIMATIONS)) {
|
|
1034
|
+
const swatch = document.createElement('div');
|
|
1035
|
+
swatch.className = 'scene-swatch';
|
|
1036
|
+
const gradient = anim.colors.length > 1
|
|
1037
|
+
? 'linear-gradient(135deg, ' + anim.colors.join(', ') + ')'
|
|
1038
|
+
: anim.colors[0];
|
|
1039
|
+
swatch.style.background = gradient;
|
|
1040
|
+
swatch.innerHTML = '<div class="scene-swatch-label">' + anim.label + '</div>';
|
|
1041
|
+
swatch.addEventListener('click', () => {
|
|
1042
|
+
animPalette.querySelectorAll('.scene-swatch').forEach(s => s.classList.remove('active'));
|
|
1043
|
+
if (activeAnimation === key) {
|
|
1044
|
+
// Tap again to stop
|
|
1045
|
+
activeAnimation = null;
|
|
1046
|
+
send({ type: 'animation', name: 'stop' });
|
|
1047
|
+
document.getElementById('scene-label').textContent = 'Stopped';
|
|
1048
|
+
} else {
|
|
1049
|
+
swatch.classList.add('active');
|
|
1050
|
+
activeAnimation = key;
|
|
1051
|
+
document.getElementById('scene-label').textContent = anim.label;
|
|
1052
|
+
send({ type: 'animation', name: key });
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
animPalette.appendChild(swatch);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
951
1058
|
// ═══════════════════════════════════════════════════
|
|
952
1059
|
// Brush controls
|
|
953
1060
|
// ═══════════════════════════════════════════════════
|