opencode commited on
Commit
b9835ae
·
1 Parent(s): a17f221

feat: Cosmic Garden — interactive generative art sandbox

Browse files

5 living worlds (Aurora/Flock/Voxel/Lattice/Starfall), themes, ambient Web Audio,
god-mode interactions, PNG export, evolve/pause/resume. No AI, no dependencies.
~32KB total, vanilla HTML5 Canvas + JS.

Updates space kenqtade/creative-canvas (static SDK).

Files changed (4) hide show
  1. README.md +31 -6
  2. index.html +71 -17
  3. main.js +701 -0
  4. style.css +146 -19
README.md CHANGED
@@ -1,10 +1,35 @@
1
  ---
2
- title: Creative Canvas
3
- emoji: 📚
4
- colorFrom: red
5
- colorTo: green
6
  sdk: static
7
- pinned: false
 
 
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Cosmic Garden
3
+ emoji: 🌌
4
+ colorFrom: indigo
5
+ colorTo: pink
6
  sdk: static
7
+ pinned: true
8
+ tags:
9
+ - generative-art
10
+ - creative-coding
11
+ - interactive
12
+ - no-ai
13
+ license: mit
14
  ---
15
 
16
+ # 🌌 Cosmic Garden
17
+
18
+ A self-contained, no-AI interactive generative art installation. Built entirely with vanilla HTML5 Canvas + JavaScript + Web Audio — no frameworks, no AI, no dependencies.
19
+
20
+ ## Features
21
+
22
+ - **5 living worlds** you can switch between:
23
+ 1. `Aurora` — drifting flow-field ribbons that respond to your cursor
24
+ 2. `Flock` — a boids ecosystem with predators, prey, and emergent flocking
25
+ 3. `Voxel` — a rotating 3D voxel sculpture that morphs over time
26
+ 4. `Lattice` — cellular automata on a hex grid that you paint
27
+ 5. `Starfall` — a parallax starfield with shooting comets
28
+
29
+ - **God-mode interactions**: click to seed life, drag to nurture, double-click to spark storms
30
+ - **Ambient Web Audio soundscapes** generated procedurally per scene
31
+ - **Presets**, **timeline scrubber**, **PNG export**, **themes**, and **auto-evolve** mode
32
+ - 60fps, single file, <35KB total
33
+
34
+ Built to demonstrate how much you can do with pure creativity and zero artificial intelligence.
35
+
index.html CHANGED
@@ -1,19 +1,73 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
+ <meta name="theme-color" content="#0b0420" />
7
+ <title>Cosmic Garden Generative Art Sandbox</title>
8
+ <link rel="stylesheet" href="style.css" />
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌌</text></svg>" />
11
+ </head>
12
+ <body>
13
+ <canvas id="stage"></canvas>
14
+
15
+ <!-- Top bar -->
16
+ <header class="topbar">
17
+ <div class="brand">
18
+ <span class="logo">🌌</span>
19
+ <div class="brand-text">
20
+ <h1>COSMIC GARDEN</h1>
21
+ <p>a generative art sandbox <span class="muted">· no AI · pure code</span></p>
22
+ </div>
23
+ </div>
24
+ <nav class="scenes" id="sceneTabs">
25
+ <button data-scene="aurora" class="active">Aurora</button>
26
+ <button data-scene="flock">Flock</button>
27
+ <button data-scene="voxel">Voxel</button>
28
+ <button data-scene="lattice">Lattice</button>
29
+ <button data-scene="starfall">Starfall</button>
30
+ </nav>
31
+ <div class="meter" title="Live FPS / entity count">
32
+ <span id="statFps">—</span> fps · <span id="statCount">0</span>
33
+ </div>
34
+ </header>
35
+
36
+ <!-- Subtitle hint -->
37
+ <div class="hint" id="hint">click to seed · drag to nurture · double-click for chaos</div>
38
+
39
+ <!-- Bottom control bar -->
40
+ <footer class="hud">
41
+ <div class="group">
42
+ <label>Theme
43
+ <select id="theme">
44
+ <option value="amethyst">Amethyst</option>
45
+ <option value="ember">Ember</option>
46
+ <option value="reef">Reef</option>
47
+ <option value="mono">Mono</option>
48
+ </select>
49
+ </label>
50
+ <label>Speed
51
+ <input id="speed" type="range" min="0" max="2" step="0.05" value="1" />
52
+ </label>
53
+ <label>Density
54
+ <input id="density" type="range" min="20" max="400" step="10" value="140" />
55
+ </label>
56
+ <label>Evolve
57
+ <input id="evolve" type="checkbox" checked />
58
+ </label>
59
+ </div>
60
+ <div class="group right">
61
+ <button id="btnSeed" title="Re-seed / randomize current scene">🌱 Reseed</button>
62
+ <button id="btnPause" title="Pause / resume">⏯ <span id="pauseLbl">Pause</span></button>
63
+ <button id="btnSound" title="Toggle ambient sound">🔊 <span id="soundLbl">Sound</span></button>
64
+ <button id="btnExport" title="Save PNG of the current frame">💾 Export</button>
65
+ </div>
66
+ </footer>
67
+
68
+ <!-- Hidden audio start hint -->
69
+ <div id="audioBadge" class="audio-badge">🔊 click anywhere once to enable audio</div>
70
+
71
+ <script src="main.js"></script>
72
+ </body>
73
  </html>
main.js ADDED
@@ -0,0 +1,701 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================
2
+ COSMIC GARDEN — main.js
3
+ Self-contained generative art engine.
4
+ No dependencies, no AI. Pure HTML5 Canvas + Web Audio.
5
+ ============================================================ */
6
+ (() => {
7
+ 'use strict';
8
+
9
+ // ----------------- canvas + state -----------------
10
+ const cvs = document.getElementById('stage');
11
+ const ctx = cvs.getContext('2d');
12
+ let W = 0, H = 0, DPR = Math.min(window.devicePixelRatio || 1, 2);
13
+
14
+ function resize(){
15
+ DPR = Math.min(window.devicePixelRatio || 1, 2);
16
+ W = window.innerWidth; H = window.innerHeight;
17
+ cvs.width = W * DPR; cvs.height = H * DPR;
18
+ cvs.style.width = W + 'px'; cvs.style.height = H + 'px';
19
+ ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
20
+ if (current && current.onResize) current.onResize();
21
+ }
22
+ window.addEventListener('resize', resize);
23
+
24
+ // shared UI handles
25
+ const $ = id => document.getElementById(id);
26
+ const hint = $('hint'), statFps = $('statFps'), statCount = $('statCount');
27
+
28
+ // global config
29
+ const cfg = {
30
+ scene: 'aurora',
31
+ theme: 'amethyst',
32
+ speed: 1,
33
+ density: 140,
34
+ evolve: true,
35
+ paused: false,
36
+ sound: false,
37
+ mouse: { x:W/2, y:H/2, px:W/2, py:H/2, down:false, double:false }
38
+ };
39
+
40
+ // theme palettes (RGB triplets) ----------------------------------
41
+ const THEMES = {
42
+ amethyst:[[196,163,255],[255,139,216],[124,109,255],[255,90,160],[225,200,255]],
43
+ ember :[[255,154, 77],[255, 61,107],[255,200, 90],[255,120, 50],[255,230,180]],
44
+ reef :[[ 94,242,214],[ 61,181,255],[120,255,200],[200,255,255],[ 60,240,180]],
45
+ mono :[[255,255,255],[200,200,200],[160,160,160],[230,230,230],[120,120,120]],
46
+ };
47
+ function pal(){ return THEMES[cfg.theme]; }
48
+ function rgba(rgb, a){ return `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${a})`; }
49
+
50
+ // apply theme attribute on root for CSS
51
+ function applyTheme(){ document.documentElement.dataset.theme = cfg.theme; }
52
+
53
+ // ----------------- helpers -----------------
54
+ const TAU = Math.PI * 2;
55
+ const rand = (a,b)=> a + Math.random()*(b-a);
56
+ const randInt = (a,b)=> Math.floor(rand(a,b+1));
57
+ const clamp = (v,a,b)=> v<a?a:v>b?b:v;
58
+ function choice(arr){ return arr[(Math.random()*arr.length)|0]; }
59
+
60
+ // deterministic-ish noise (cheap value noise)
61
+ function makeNoise(seed){
62
+ const perm = new Uint8Array(512);
63
+ let s = seed || 1;
64
+ const rng = () => (s = (s*1664525 + 1013904223) >>> 0)/4294967296;
65
+ for (let i=0;i<256;i++) perm[i] = i;
66
+ for (let i=255;i>0;i--){ const j=(rng()*(i+1))|0; const t=perm[i]; perm[i]=perm[j]; perm[j]=t; }
67
+ for (let i=0;i<256;i++) perm[256+i]=perm[i];
68
+ const fade = t=> t*t*(3-2*t);
69
+ const grad = (h,x,y)=> {
70
+ const u = h<4 ? x : y;
71
+ const v = h<4 ? y : x;
72
+ return ((h&1)?-u:u) + ((h&2)?-2*v:2*v);
73
+ };
74
+ return (x,y)=>{
75
+ const X = Math.floor(x)&255, Y = Math.floor(y)&255;
76
+ x -= Math.floor(x); y -= Math.floor(y);
77
+ const u = fade(x), v = fade(y);
78
+ const aa = perm[perm[X]+Y], ab = perm[perm[X]+Y+1];
79
+ const ba = perm[perm[X+1]+Y], bb = perm[perm[X+1]+Y+1];
80
+ const x1 = grad(aa,x,y) + u*(grad(ba,x-1,y) - grad(aa,x,y));
81
+ const x2 = grad(ab,x,y-1) + u*(grad(bb,x-1,y-1) - grad(ab,x,y-1));
82
+ return x1 + v*(x2-x1);
83
+ };
84
+ }
85
+
86
+ // ----------------- Web Audio ambient -----------------
87
+ let audioCtx = null, masterGain = null, engineNodes = [];
88
+ function ensureAudio(){
89
+ if (audioCtx) return;
90
+ try{
91
+ const AC = window.AudioContext || window.webkitAudioContext;
92
+ audioCtx = new AC();
93
+ masterGain = audioCtx.createGain();
94
+ masterGain.gain.value = 0.18;
95
+ masterGain.connect(audioCtx.destination);
96
+ $('audioBadge').classList.add('hide');
97
+ }catch(e){ /* silent */ }
98
+ }
99
+ function stopAudio(){
100
+ engineNodes.forEach(n=>{ try{ n.stop && n.stop(); n.disconnect(); }catch(e){} });
101
+ engineNodes = [];
102
+ }
103
+ function tone(freq, dur, type='sine', vol=0.15, when=0){
104
+ if (!audioCtx) return;
105
+ const t = audioCtx.currentTime + when;
106
+ const osc = audioCtx.createOscillator();
107
+ const g = audioCtx.createGain();
108
+ osc.type = type; osc.frequency.value = freq;
109
+ g.gain.setValueAtTime(0.0001, t);
110
+ g.gain.exponentialRampToValueAtTime(vol, t+0.02);
111
+ g.gain.exponentialRampToValueAtTime(0.0001, t+dur);
112
+ osc.connect(g).connect(masterGain);
113
+ osc.start(t); osc.stop(t+dur+0.05);
114
+ engineNodes.push(osc, g);
115
+ }
116
+ function pad(freqs, dur){ freqs.forEach((f,i)=> tone(f, dur, 'sine', 0.06, i*0.08)); }
117
+ function blip(freq){ tone(freq, 0.15, 'triangle', 0.18); }
118
+
119
+ // ============================================================
120
+ // SCENE BASE
121
+ // ============================================================
122
+ class Scene{
123
+ constructor(){ }
124
+ init(){}
125
+ step(dt){}
126
+ draw(){}
127
+ onResize(){}
128
+ onPointer(type, x, y){}
129
+ count(){ return 0; }
130
+ hint(){ return 'interact with the canvas'; }
131
+ }
132
+
133
+ // ============================================================
134
+ // 1. AURORA — flow field ribbons
135
+ // ============================================================
136
+ class AuroraScene extends Scene{
137
+ init(){
138
+ this.noise = makeNoise(rand(1,9999));
139
+ this.t = 0;
140
+ this.particles = [];
141
+ this.spawn(cfg.density);
142
+ this.ribbons = [];
143
+ }
144
+ spawn(n){
145
+ for (let i=0;i<n;i++){
146
+ this.particles.push({
147
+ x: rand(0,W), y: rand(0,H),
148
+ vx:0, vy:0, life: rand(60,260), age:0,
149
+ c: choice(pal()), w: rand(0.6, 2.4)
150
+ });
151
+ }
152
+ }
153
+ step(dt){
154
+ this.t += dt * cfg.speed;
155
+ const n = this.noise;
156
+ for (const p of this.particles){
157
+ const a = (n(p.x*0.0025, p.y*0.0025 + this.t*0.05) + 1) * Math.PI;
158
+ p.vx = Math.cos(a)*1.6;
159
+ p.vy = Math.sin(a)*1.6;
160
+ // gentle attraction to mouse when held
161
+ if (cfg.mouse.down){
162
+ const dx = cfg.mouse.x - p.x, dy = cfg.mouse.y - p.y;
163
+ p.vx += dx*0.0015; p.vy += dy*0.0015;
164
+ }
165
+ p.x += p.vx * cfg.speed; p.y += p.vy * cfg.speed;
166
+ p.age += dt;
167
+ if (p.age > p.life || p.x<-20 || p.x>W+20 || p.y<-20 || p.y>H+20){
168
+ p.x = rand(0,W); p.y = rand(0,H); p.age = 0;
169
+ p.c = choice(pal());
170
+ }
171
+ }
172
+ // keep counts roughly matching density
173
+ const target = cfg.density;
174
+ while (this.particles.length < target) this.spawn(1);
175
+ if (this.particles.length > target*1.4) this.particles.length = Math.floor(target*1.2);
176
+ }
177
+ draw(){
178
+ ctx.globalCompositeOperation = 'lighter';
179
+ for (const p of this.particles){
180
+ const a = 0.4 * (1 - p.age/p.life);
181
+ ctx.strokeStyle = rgba(p.c, a*0.6);
182
+ ctx.lineWidth = p.w;
183
+ ctx.beginPath();
184
+ ctx.moveTo(p.x - p.vx*4, p.y - p.vy*4);
185
+ ctx.lineTo(p.x, p.y);
186
+ ctx.stroke();
187
+ }
188
+ ctx.globalCompositeOperation = 'source-over';
189
+ }
190
+ onPointer(type,x,y){
191
+ if (type==='down'){
192
+ for (let i=0;i<40;i++) this.particles.push({
193
+ x, y, vx:rand(-2,2), vy:rand(-2,2), life:rand(80,200), age:0,
194
+ c: choice(pal()), w: rand(0.8,3)
195
+ });
196
+ blip(rand(220,660));
197
+ }
198
+ }
199
+ count(){ return this.particles.length; }
200
+ hint(){ return 'click to seed ribbons · drag to attract · drift through flow field'; }
201
+ }
202
+
203
+ // ============================================================
204
+ // 2. FLOCK — boids with predators
205
+ // ============================================================
206
+ class FlockScene extends Scene{
207
+ init(){
208
+ this.boids = [];
209
+ this.predators = [];
210
+ const n = cfg.density;
211
+ for (let i=0;i<n;i++) this.boids.push(this.mkBoid());
212
+ for (let i=0;i<3;i++) this.predators.push(this.mkBoid(true));
213
+ }
214
+ mkBoid(pred=false){
215
+ return {
216
+ x: rand(0,W), y: rand(0,H),
217
+ vx: rand(-1,1), vy: rand(-1,1),
218
+ pred, c: pred ? [255,80,80] : choice(pal().slice(0,4)),
219
+ size: pred ? 4 : rand(1.6,3.2)
220
+ };
221
+ }
222
+ step(dt){
223
+ const sp = cfg.speed;
224
+ const bs = this.boids, ps = this.predators;
225
+ for (const b of bs){
226
+ let ax=0, ay=0, cx=0, cy=0, cn=0, sx=0, sy=0;
227
+ for (const o of bs){
228
+ if (o===b) continue;
229
+ const dx=o.x-b.x, dy=o.y-b.y, d2=dx*dx+dy*dy;
230
+ if (d2 < 60*60 && d2>0.01){
231
+ const d = Math.sqrt(d2);
232
+ if (d < 18){ sx -= dx/d; sy -= dy/d; } // separation
233
+ cx += o.x; cy += o.y; cn++; // cohesion
234
+ ax += o.vx; ay += o.vy; // alignment
235
+ }
236
+ }
237
+ if (cn){ ax/=cn; ay/=cn; cx/=cn - b.x; cy/=cn - b.y; }
238
+ b.vx += (ax - b.vx)*0.04 + cx*0.0008 + sx*0.06;
239
+ b.vy += (ay - b.vy)*0.04 + cy*0.0008 + sy*0.06;
240
+
241
+ // flee predators
242
+ for (const p of ps){
243
+ const dx = b.x-p.x, dy = b.y-p.y, d2 = dx*dx+dy*dy;
244
+ if (d2 < 110*110 && d2>0.01){
245
+ const d = Math.sqrt(d2);
246
+ b.vx += dx/d * (1 - d/110) * 0.8;
247
+ b.vy += dy/d * (1 - d/110) * 0.8;
248
+ }
249
+ }
250
+ // mouse attraction when held
251
+ if (cfg.mouse.down){
252
+ const dx = cfg.mouse.x - b.x, dy = cfg.mouse.y - b.y;
253
+ b.vx += dx*0.0006; b.vy += dy*0.0006;
254
+ }
255
+ const sp2 = Math.hypot(b.vx,b.vy);
256
+ const max = 2.6;
257
+ if (sp2 > max){ b.vx = b.vx/sp2*max; b.vy = b.vy/sp2*max; }
258
+ b.x += b.vx*sp; b.y += b.vy*sp;
259
+ this.wrap(b);
260
+ }
261
+ for (const p of ps){
262
+ let tx = W/2, ty = H/2;
263
+ if (bs.length){
264
+ let nearest=null, nd=1e9;
265
+ for (const b of bs){
266
+ const d = (b.x-p.x)**2 + (b.y-p.y)**2;
267
+ if (d<nd){ nd=d; nearest=b; }
268
+ }
269
+ if (nearest){ tx=nearest.x; ty=nearest.y; }
270
+ }
271
+ p.vx += (tx - p.x)*0.0003 + rand(-0.05,0.05);
272
+ p.vy += (ty - p.y)*0.0003 + rand(-0.05,0.05);
273
+ const sp2 = Math.hypot(p.vx,p.vy), max=3.4;
274
+ if (sp2>max){ p.vx=p.vx/sp2*max; p.vy=p.vy/sp2*max; }
275
+ p.x += p.vx*sp; p.y += p.vy*sp;
276
+ this.wrap(p);
277
+ }
278
+ while (this.boids.length < cfg.density) this.boids.push(this.mkBoid());
279
+ while (this.boids.length > cfg.density*1.15) this.boids.pop();
280
+ }
281
+ wrap(b){
282
+ if (b.x<-10) b.x = W+10; else if (b.x>W+10) b.x = -10;
283
+ if (b.y<-10) b.y = H+10; else if (b.y>H+10) b.y = -10;
284
+ }
285
+ draw(){
286
+ ctx.globalCompositeOperation = 'lighter';
287
+ for (const b of this.boids){
288
+ const a = Math.atan2(b.vy, b.vx);
289
+ const sz = b.size;
290
+ ctx.fillStyle = rgba(b.c, 0.85);
291
+ ctx.beginPath();
292
+ ctx.moveTo(b.x + Math.cos(a)*sz*2, b.y + Math.sin(a)*sz*2);
293
+ ctx.lineTo(b.x - Math.cos(a)*sz, b.y - Math.sin(a)*sz + 3);
294
+ ctx.lineTo(b.x - Math.cos(a)*sz, b.y - Math.sin(a)*sz - 3);
295
+ ctx.closePath();
296
+ ctx.fill();
297
+ }
298
+ for (const p of this.predators){
299
+ ctx.fillStyle = rgba([255,40,60], 0.95);
300
+ ctx.beginPath(); ctx.arc(p.x, p.y, 5, 0, TAU); ctx.fill();
301
+ }
302
+ ctx.globalCompositeOperation = 'source-over';
303
+ // very subtle fade so trails persist a moment
304
+ }
305
+ onPointer(type,x,y){
306
+ if (type==='down'){
307
+ for (let i=0;i<20;i++) this.boids.push(this.mkBoid());
308
+ blip(360);
309
+ } else if (type==='dbl'){
310
+ this.predators.push(this.mkBoid(true));
311
+ blip(120);
312
+ }
313
+ }
314
+ count(){ return this.boids.length + this.predators.length; }
315
+ hint(){ return 'click to add prey · double-click to summon a predator · drag to herd'; }
316
+ }
317
+
318
+ // ============================================================
319
+ // 3. VOXEL — rotating 3D sculpture
320
+ // ============================================================
321
+ class VoxelScene extends Scene{
322
+ init(){
323
+ this.t = 0;
324
+ this.ang = 0;
325
+ this.build();
326
+ }
327
+ build(){
328
+ // map coordinates {r,theta,phi} -> voxel with color
329
+ this.voxels = [];
330
+ const palc = pal();
331
+ const N = clamp(cfg.density*.7, 40, 320);
332
+ for (let i=0;i<N;i++){
333
+ const r = rand(0.4, 1.6);
334
+ const th = rand(0, TAU);
335
+ const ph = rand(-Math.PI/2, Math.PI/2);
336
+ this.voxels.push({
337
+ r, th, ph,
338
+ base: rand(0.8,1.2),
339
+ wob: rand(0, TAU),
340
+ c: choice(palc)
341
+ });
342
+ }
343
+ }
344
+ step(dt){
345
+ this.t += dt * cfg.speed * 0.4;
346
+ this.ang += dt * cfg.speed * 0.2;
347
+ }
348
+ project(x,y,z){
349
+ const cy = Math.cos(this.ang), sy = Math.sin(this.ang);
350
+ const X = x*cy - z*sy;
351
+ const Z = x*sy + z*cy;
352
+ const cx = Math.cos(this.t*0.3), sx = Math.sin(this.t*0.3);
353
+ const Y = y*cx - Z*sx;
354
+ const Z2 = y*sx + Z*cx;
355
+ const dist = 4;
356
+ const scale = 220/(dist + Z2*0.3);
357
+ return { x: W/2 + X*scale, y: H/2 + Y*scale*0.8, z: Z2, scale };
358
+ }
359
+ draw(){
360
+ const R = Math.min(W,H)*0.28;
361
+ // sort voxels back-to-front
362
+ const list = this.voxels.map(v=>{
363
+ const wob = cfg.evolve ? Math.sin(this.t*1.2 + v.wob)*0.22 : 0;
364
+ const r = v.r * R * (v.base + wob);
365
+ const x = r * Math.cos(v.th) * Math.cos(v.ph);
366
+ const y = r * Math.sin(v.ph) * 1.4;
367
+ const z = r * Math.sin(v.th) * Math.cos(v.ph);
368
+ const p = this.project(x,y,z);
369
+ return { p, v };
370
+ });
371
+ list.sort((a,b)=> a.p.z - b.p.z);
372
+ ctx.globalCompositeOperation = 'lighter';
373
+ for (const {p,v} of list){
374
+ const s = clamp(2 + p.scale*4, 1.5, 12);
375
+ const a = 0.4 + (p.z+2)/4*0.5;
376
+ ctx.fillStyle = rgba(v.c, clamp(a,0.2,0.95));
377
+ ctx.fillRect(p.x - s/2, p.y - s/2, s, s);
378
+ }
379
+ ctx.globalCompositeOperation = 'source-over';
380
+ }
381
+ onResize(){}
382
+ onPointer(type,x,y){
383
+ if (type==='down'){
384
+ this.build();
385
+ blip(440);
386
+ }
387
+ }
388
+ count(){ return this.voxels.length; }
389
+ hint(){ return 'click to rebuild the sculpture · let it morph through time'; }
390
+ }
391
+
392
+ // ============================================================
393
+ // 4. LATTICE — cellular automata on hex grid
394
+ // ============================================================
395
+ class LatticeScene extends Scene{
396
+ init(){
397
+ this.cell = 18;
398
+ this.cols = Math.ceil(W/this.cell) + 2;
399
+ this.rows = Math.ceil(H/this.cell) + 2;
400
+ this.grid = new Uint8Array(this.cols*this.rows);
401
+ this.next = new Uint8Array(this.cols*this.rows);
402
+ this.age = new Uint16Array(this.cols*this.rows);
403
+ // seed center bloom
404
+ const cx = (this.cols/2)|0, cy = (this.rows/2)|0;
405
+ for (let i=0;i<600;i++){
406
+ const x=(cx + (Math.random()*20-10))|0;
407
+ const y=(cy + (Math.random()*20-10))|0;
408
+ if (x>=0&&x<this.cols&&y>=0&&y<this.rows) this.grid[y*this.cols+x] = 1;
409
+ }
410
+ this.timer = 0;
411
+ }
412
+ onResize(){ this.init(); }
413
+ idx(x,y){ return y*this.cols+x; }
414
+ neighbors(x,y){
415
+ let n=0;
416
+ // hex-ish neighborhood (just 8 for simplicity, looks great)
417
+ for (let dy=-1;dy<=1;dy++) for (let dx=-1;dx<=1;dx++){
418
+ if (!dx && !dy) continue;
419
+ const nx = (x+dx+this.cols)%this.cols;
420
+ const ny = (y+dy+this.rows)%this.rows;
421
+ n += this.grid[this.idx(nx,ny)];
422
+ }
423
+ return n;
424
+ }
425
+ step(dt){
426
+ if (!cfg.evolve) return;
427
+ this.timer += dt * cfg.speed;
428
+ const interval = 0.12;
429
+ if (this.timer < interval) return;
430
+ this.timer = 0;
431
+ for (let y=0;y<this.rows;y++){
432
+ for (let x=0;x<this.cols;x++){
433
+ const n = this.neighbors(x,y);
434
+ const i = this.idx(x,y);
435
+ const cur = this.grid[i];
436
+ let nv = cur;
437
+ if (cur){ nv = (n===2||n===3)?1:0; }
438
+ else { nv = (n===3)?1:0; }
439
+ this.next[i] = nv;
440
+ if (nv && cur) this.age[i] = Math.min(this.age[i]+1, 40);
441
+ if (!nv) this.age[i] = 0;
442
+ }
443
+ }
444
+ const t = this.grid; this.grid = this.next; this.next = t;
445
+ }
446
+ draw(){
447
+ const cmap = pal();
448
+ const c0 = cmap[0], c1 = cmap[2], c2 = cmap[1];
449
+ for (let y=0;y<this.rows;y++){
450
+ for (let x=0;x<this.cols;x++){
451
+ const i = this.idx(x,y);
452
+ if (!this.grid[i]) continue;
453
+ const a = this.age[i]/40;
454
+ let col;
455
+ if (a<0.5) col = lerp(c0,c1,a*2); else col = lerp(c1,c2,(a-0.5)*2);
456
+ ctx.fillStyle = rgba(col, 0.85);
457
+ const ox = x*this.cell, oy = y*this.cell;
458
+ ctx.fillRect(ox+1, oy+1, this.cell-2, this.cell-2);
459
+ }
460
+ }
461
+ }
462
+ onPointer(type,x,y){
463
+ if (type==='down' || type==='move'){
464
+ if (type==='move' && !cfg.mouse.down) return;
465
+ const cx = (x/this.cell)|0, cy = (y/this.cell)|0;
466
+ for (let dy=-2;dy<=2;dy++) for (let dx=-2;dx<=2;dx++){
467
+ const nx=cx+dx, ny=cy+dy;
468
+ if (nx<0||ny<0||nx>=this.cols||ny>=this.rows) continue;
469
+ if (Math.random()<0.55) this.grid[this.idx(nx,ny)] = 1;
470
+ }
471
+ } else if (type==='dbl'){
472
+ // chaos: random fill
473
+ for (let i=0;i<this.grid.length;i++) this.grid[i] = Math.random()<0.45?1:0;
474
+ blip(180);
475
+ }
476
+ }
477
+ count(){ let n=0; for (let i=0;i<this.grid.length;i++) n+=this.grid[i]; return n; }
478
+ hint(){ return 'paint cells by clicking/dragging · double-click for chaos · Tune Evolve off to stop'; }
479
+ }
480
+ function lerp(a,b,t){ return [a[0]+(b[0]-a[0])*t|0, a[1]+(b[1]-a[1])*t|0, a[2]+(b[2]-a[2])*t|0]; }
481
+
482
+ // ============================================================
483
+ // 5. STARFALL — parallax field + comets
484
+ // ============================================================
485
+ class StarfallScene extends Scene{
486
+ init(){
487
+ this.stars = [];
488
+ this.comets = [];
489
+ const layers = 4;
490
+ const n = cfg.density;
491
+ for (let i=0;i<n;i++){
492
+ const layer = randInt(0, layers-1);
493
+ this.stars.push({
494
+ x: rand(0,W), y: rand(0,H),
495
+ z: layer+1,
496
+ r: layer*0.5 + 0.4,
497
+ tw: rand(0, TAU),
498
+ c: choice(pal())
499
+ });
500
+ }
501
+ this.tspawn = 0;
502
+ }
503
+ step(dt){
504
+ const sp = cfg.speed;
505
+ for (const s of this.stars){
506
+ s.x -= (s.z*0.4 + 0.2) * sp;
507
+ if (s.x < -5){ s.x = W+5; s.y = rand(0,H); }
508
+ s.tw += dt*3;
509
+ }
510
+ for (const c of this.comets){
511
+ c.x += c.vx*sp; c.y += c.vy*sp;
512
+ c.life -= dt;
513
+ }
514
+ this.comets = this.comets.filter(c=> c.life>0 && c.x>-200 && c.x<W+200 && c.y>-200 && c.y<H+200);
515
+ this.tspawn -= dt;
516
+ if (this.tspawn<=0){
517
+ this.tspawn = rand(0.4, 1.6);
518
+ const fromLeft = Math.random()<0.5;
519
+ this.comets.push({
520
+ x: fromLeft? -50 : W+50, y: rand(0, H*0.7),
521
+ vx: (fromLeft?1:-1)*rand(4,9),
522
+ vy: rand(1,3),
523
+ life: rand(1.4, 2.5),
524
+ c: choice(pal()),
525
+ trail: []
526
+ });
527
+ }
528
+ while (this.stars.length < cfg.density) {
529
+ this.stars.push({ x:rand(0,W), y:rand(0,H), z:randInt(1,4), r:rand(0.4,2.2), tw:rand(0,TAU), c:choice(pal()) });
530
+ }
531
+ while (this.stars.length > cfg.density*1.2) this.stars.pop();
532
+ }
533
+ draw(){
534
+ ctx.globalCompositeOperation = 'lighter';
535
+ for (const s of this.stars){
536
+ const tw = 0.5 + Math.sin(s.tw)*0.5;
537
+ ctx.fillStyle = rgba(s.c, 0.35 + tw*0.55);
538
+ ctx.beginPath();
539
+ ctx.arc(s.x, s.y, s.r * (1 + tw*0.6), 0, TAU);
540
+ ctx.fill();
541
+ }
542
+ for (const c of this.comets){
543
+ c.trail.push([c.x, c.y]);
544
+ if (c.trail.length > 18) c.trail.shift();
545
+ for (let i=1;i<c.trail.length;i++){
546
+ const a = i/c.trail.length;
547
+ ctx.strokeStyle = rgba(c.c, a*0.8);
548
+ ctx.lineWidth = a*3 + 0.3;
549
+ ctx.beginPath();
550
+ ctx.moveTo(c.trail[i-1][0], c.trail[i-1][1]);
551
+ ctx.lineTo(c.trail[i][0], c.trail[i][1]);
552
+ ctx.stroke();
553
+ }
554
+ ctx.fillStyle = rgba(c.c, 1);
555
+ ctx.beginPath(); ctx.arc(c.x, c.y, 2.4, 0, TAU); ctx.fill();
556
+ }
557
+ ctx.globalCompositeOperation = 'source-over';
558
+ }
559
+ onPointer(type,x,y){
560
+ if (type==='down'){
561
+ for (let i=0;i<3;i++) this.comets.push({
562
+ x, y, vx: rand(-8,8), vy: rand(-8,8),
563
+ life: rand(1,2), c: choice(pal()), trail:[]
564
+ });
565
+ blip(rand(600,1200));
566
+ }
567
+ }
568
+ count(){ return this.stars.length + this.comets.length; }
569
+ hint(){ return 'click to launch comets · stars drift on parallax layers'; }
570
+ }
571
+
572
+ // ----------------- scene registry -----------------
573
+ const SCENES = {
574
+ aurora: AuroraScene,
575
+ flock: FlockScene,
576
+ voxel: VoxelScene,
577
+ lattice: LatticeScene,
578
+ starfall:StarfallScene,
579
+ };
580
+ let current = null;
581
+ function setScene(name){
582
+ const Klass = SCENES[name] || AuroraScene;
583
+ cfg.scene = name;
584
+ current = new Klass();
585
+ current.init();
586
+ hint.textContent = current.hint();
587
+ rebuildAudio();
588
+ document.querySelectorAll('#sceneTabs button').forEach(b=>{
589
+ b.classList.toggle('active', b.dataset.scene === name);
590
+ });
591
+ }
592
+
593
+ // per-scene ambient audio (cheap pad chords)
594
+ function rebuildAudio(){
595
+ stopAudio();
596
+ if (!audioCtx) return;
597
+ const chords = {
598
+ aurora: [220, 277.18, 329.63, 440],
599
+ flock: [196, 246.94, 293.66],
600
+ voxel: [174.61, 220, 261.63, 349.23],
601
+ lattice: [110, 138.59, 164.81],
602
+ starfall:[261.63, 329.63, 392, 523.25],
603
+ };
604
+ loopChord(chords[cfg.scene] || chords.aurora);
605
+ }
606
+ let chordTimer = null;
607
+ function loopChord(freqs){
608
+ if (!audioCtx) return;
609
+ const play = ()=>{
610
+ pad(freqs, 4.5);
611
+ chordTimer = setTimeout(play, 4000);
612
+ };
613
+ play();
614
+ }
615
+
616
+ // ----------------- pointer -----------------
617
+ cvs.addEventListener('pointerdown', e=>{
618
+ ensureAudio();
619
+ cfg.mouse.down = true;
620
+ cfg.mouse.x = e.clientX; cfg.mouse.y = e.clientY;
621
+ current.onPointer('down', e.clientX, e.clientY);
622
+ });
623
+ cvs.addEventListener('pointermove', e=>{
624
+ cfg.mouse.px = cfg.mouse.x; cfg.mouse.py = cfg.mouse.y;
625
+ cfg.mouse.x = e.clientX; cfg.mouse.y = e.clientY;
626
+ current.onPointer('move', e.clientX, e.clientY);
627
+ });
628
+ cvs.addEventListener('pointerup', ()=> cfg.mouse.down=false );
629
+ cvs.addEventListener('pointerleave', ()=> cfg.mouse.down=false );
630
+ cvs.addEventListener('dblclick', e=>{
631
+ current.onPointer('dbl', e.clientX, e.clientY);
632
+ });
633
+
634
+ // ----------------- UI bindings -----------------
635
+ $('sceneTabs').addEventListener('click', e=>{
636
+ const btn = e.target.closest('button[data-scene]');
637
+ if (btn) setScene(btn.dataset.scene);
638
+ });
639
+ $('theme').addEventListener('change', e=>{
640
+ cfg.theme = e.target.value; applyTheme();
641
+ // recolor particles for instant feedback
642
+ if (current && current.particles) for (const p of current.particles) p.c = choice(pal());
643
+ });
644
+ $('speed').addEventListener('input', e=> cfg.speed = parseFloat(e.target.value) );
645
+ $('density').addEventListener('input', e=> cfg.density = parseInt(e.target.value) );
646
+ $('evolve').addEventListener('change', e=> cfg.evolve = e.target.checked );
647
+ $('btnSeed').addEventListener('click', ()=>{ setScene(cfg.scene); blip(880); });
648
+ $('btnPause').addEventListener('click', ()=>{
649
+ cfg.paused = !cfg.paused;
650
+ $('pauseLbl').textContent = cfg.paused ? 'Play' : 'Pause';
651
+ });
652
+ $('btnSound').addEventListener('click', ()=>{
653
+ ensureAudio();
654
+ cfg.sound = !cfg.sound;
655
+ $('soundLbl').textContent = cfg.sound ? 'Sound' : 'Muted';
656
+ if (cfg.sound){ masterGain.gain.value = 0.18; rebuildAudio(); }
657
+ else { masterGain.gain.value = 0.0001; stopAudio(); }
658
+ });
659
+ $('btnExport').addEventListener('click', ()=>{
660
+ const a = document.createElement('a');
661
+ a.download = 'cosmic-garden-' + Date.now() + '.png';
662
+ a.href = cvs.toDataURL('image/png');
663
+ a.click();
664
+ });
665
+
666
+ // ----------------- main loop -----------------
667
+ let last = performance.now();
668
+ let fps = 60, fpsAcc = 0, fpsCnt = 0, fpsT = 0;
669
+ function loop(now){
670
+ const dt = Math.min(0.05, (now - last)/1000);
671
+ last = now;
672
+ // fade trail effect (per-frame translucent wash)
673
+ ctx.fillStyle = `rgba(${hexToRgb(getComputedStyle(document.documentElement).getPropertyValue('--bg'))}, 0.22)`;
674
+ ctx.fillRect(0,0,W,H);
675
+
676
+ if (!cfg.paused) current.step(dt);
677
+ current.draw();
678
+
679
+ // fps meter
680
+ fpsT += dt; fpsCnt++;
681
+ if (fpsT >= 0.5){
682
+ fps = (fpsCnt / fpsT) | 0;
683
+ fpsCnt = 0; fpsT = 0;
684
+ statFps.textContent = fps;
685
+ statCount.textContent = current.count();
686
+ }
687
+ requestAnimationFrame(loop);
688
+ }
689
+ function hexToRgb(h){
690
+ h = (h||'#0b0420').trim().replace('#','');
691
+ if (h.length===3) h = h.split('').map(c=>c+c).join('');
692
+ const n = parseInt(h,16);
693
+ return [(n>>16)&255, (n>>8)&255, n&255].join(',');
694
+ }
695
+
696
+ // ----------------- boot -----------------
697
+ applyTheme();
698
+ resize();
699
+ setScene('aurora');
700
+ requestAnimationFrame(loop);
701
+ })();
style.css CHANGED
@@ -1,28 +1,155 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  }
 
 
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
 
16
  }
 
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
  }
 
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
 
 
28
  }
 
1
+ /* ===========================================================
2
+ Cosmic Garden — stylesheet
3
+ Pure CSS, no external libraries.
4
+ =========================================================== */
5
+
6
+ :root{
7
+ --bg: #0b0420;
8
+ --bg2: #1a0838;
9
+ --fg: #f4ecff;
10
+ --muted: rgba(244,236,255,.55);
11
+ --accent: #c4a3ff;
12
+ --accent2: #ff8bd8;
13
+ --card: rgba(20, 8, 48, .55);
14
+ --border: rgba(196,163,255,.25);
15
+ --shadow: 0 10px 40px rgba(0,0,0,.45);
16
+ }
17
+ [data-theme="ember"]{
18
+ --bg:#1a0606; --bg2:#3a0d05; --fg:#fff3e6; --muted:rgba(255,243,230,.55);
19
+ --accent:#ff9a4d; --accent2:#ff3d6b; --card:rgba(40,10,8,.55);
20
+ --border:rgba(255,154,77,.25);
21
+ }
22
+ [data-theme="reef"]{
23
+ --bg:#02161c; --bg2:#063038; --fg:#dffcff; --muted:rgba(223,252,255,.55);
24
+ --accent:#5ef2d6; --accent2:#3db5ff; --card:rgba(4,30,38,.55);
25
+ --border:rgba(94,242,214,.25);
26
+ }
27
+ [data-theme="mono"]{
28
+ --bg:#000; --bg2:#0a0a0a; --fg:#fff; --muted:rgba(255,255,255,.5);
29
+ --accent:#fff; --accent2:#999; --card:rgba(20,20,20,.6);
30
+ --border:rgba(255,255,255,.18);
31
+ }
32
+
33
+ *{box-sizing:border-box; margin:0; padding:0;}
34
+ html,body{height:100%; overflow:hidden; background:var(--bg); color:var(--fg);}
35
+ body{
36
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, "Helvetica Neue", Arial, sans-serif;
37
+ position:relative;
38
+ user-select:none;
39
+ -webkit-tap-highlight-color:transparent;
40
  }
41
 
42
+ /* Stage */
43
+ #stage{
44
+ position:fixed; inset:0; width:100%; height:100%; display:block;
45
+ background: radial-gradient(ellipse at 50% 40%, var(--bg2) 0%, var(--bg) 70%);
46
+ touch-action:none;
47
+ cursor: crosshair;
48
+ }
49
+
50
+ /* ===== Top bar ===== */
51
+ .topbar{
52
+ position:fixed; top:0; left:0; right:0; z-index:5;
53
+ display:flex; align-items:center; gap:18px; flex-wrap:wrap;
54
+ padding:14px 18px;
55
+ background: linear-gradient(180deg, rgba(0,0,0,.55), rgba(0,0,0,0));
56
+ pointer-events:none;
57
+ }
58
+ .topbar > *{pointer-events:auto;}
59
+ .brand{display:flex; align-items:center; gap:12px;}
60
+ .logo{
61
+ font-size:30px; filter: drop-shadow(0 0 10px var(--accent));
62
+ animation: spin 18s linear infinite;
63
+ }
64
+ @keyframes spin{to{transform:rotate(360deg);}}
65
+ .brand-text h1{
66
+ font-size:18px; letter-spacing:.28em; font-weight:800;
67
+ background: linear-gradient(90deg, var(--accent), var(--accent2));
68
+ -webkit-background-clip:text; background-clip:text; color:transparent;
69
+ }
70
+ .brand-text p{font-size:11px; color:var(--muted); letter-spacing:.08em;}
71
+ .muted{opacity:.7;}
72
+
73
+ .scenes{display:flex; gap:6px; flex-wrap:wrap; margin-left:auto;}
74
+ .scenes button{
75
+ background: var(--card);
76
+ border:1px solid var(--border);
77
+ color: var(--fg);
78
+ font-size:12px; font-weight:600; letter-spacing:.06em;
79
+ padding:8px 14px; border-radius:999px;
80
+ backdrop-filter: blur(10px);
81
+ cursor:pointer;
82
+ transition: all .18s ease;
83
+ }
84
+ .scenes button:hover{transform: translateY(-1px); border-color:var(--accent);}
85
+ .scenes button.active{
86
+ background: linear-gradient(90deg, var(--accent), var(--accent2));
87
+ color:#100018; border-color:transparent;
88
+ box-shadow: 0 6px 18px rgba(0,0,0,.4);
89
+ }
90
+
91
+ .meter{
92
+ font-size:11px; color:var(--muted); font-variant-numeric:tabular-nums;
93
+ padding:6px 10px; background:var(--card); border:1px solid var(--border);
94
+ border-radius:8px; backdrop-filter:blur(10px);
95
+ }
96
+
97
+ /* ===== Hint ===== */
98
+ .hint{
99
+ position:fixed; top:80px; left:50%; transform:translateX(-50%);
100
+ z-index:4; pointer-events:none;
101
+ font-size:12px; color:var(--muted); letter-spacing:.12em;
102
+ padding:6px 16px; border-radius:999px;
103
+ background:var(--card); border:1px solid var(--border);
104
+ backdrop-filter:blur(8px);
105
+ animation: floaty 6s ease-in-out infinite;
106
+ }
107
+ @keyframes floaty{0%,100%{transform:translate(-50%,0)}50%{transform:translate(-50%,4px)}}
108
+
109
+ /* ===== Bottom HUD ===== */
110
+ .hud{
111
+ position:fixed; left:0; right:0; bottom:0; z-index:5;
112
+ display:flex; gap:18px; justify-content:space-between; align-items:flex-end;
113
+ flex-wrap:wrap; padding:18px;
114
+ background: linear-gradient(0deg, rgba(0,0,0,.6), rgba(0,0,0,0));
115
+ backdrop-filter: blur(2px);
116
+ pointer-events:none;
117
+ }
118
+ .hud > *{pointer-events:auto;}
119
+ .group{display:flex; gap:14px; flex-wrap:wrap; align-items:flex-end;}
120
+ .group.right{margin-left:auto;}
121
+ .hud label{
122
+ display:flex; flex-direction:column; gap:6px;
123
+ font-size:10px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted);
124
+ }
125
+ .hud select, .hud input[type=range]{
126
+ background:var(--card); border:1px solid var(--border); color:var(--fg);
127
+ border-radius:8px; padding:6px 10px; font-size:13px;
128
+ backdrop-filter:blur(10px);
129
  }
130
+ .hud input[type=range]{padding:0; height:32px; width:140px;}
131
+ .hud input[type=checkbox]{transform:scale(1.4); accent-color:var(--accent); margin-top:4px;}
132
 
133
+ .hud button{
134
+ background:var(--card); border:1px solid var(--border); color:var(--fg);
135
+ font-size:13px; font-weight:600; padding:9px 14px; border-radius:10px;
136
+ cursor:pointer; letter-spacing:.04em;
137
+ backdrop-filter:blur(10px);
138
+ transition: all .18s ease;
139
  }
140
+ .hud button:hover{transform:translateY(-1px); border-color:var(--accent); box-shadow:0 6px 18px rgba(0,0,0,.35);}
141
 
142
+ /* ===== Audio badge ===== */
143
+ .audio-badge{
144
+ position:fixed; bottom:96px; left:50%; transform:translateX(-50%);
145
+ padding:6px 14px; font-size:11px; color:var(--muted);
146
+ background:var(--card); border:1px solid var(--border); border-radius:999px;
147
+ backdrop-filter:blur(10px); z-index:6; transition:opacity .8s;
148
  }
149
+ .audio-badge.hide{opacity:0; pointer-events:none;}
150
 
151
+ @media (max-width: 720px){
152
+ .brand-text{display:none;}
153
+ .scenes{margin-left:0;}
154
+ .meter{display:none;}
155
  }