Spaces:
Running
Running
opencode commited on
Commit ·
b9835ae
1
Parent(s): a17f221
feat: Cosmic Garden — interactive generative art sandbox
Browse files5 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).
README.md
CHANGED
|
@@ -1,10 +1,35 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
-
pinned:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
}
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
}
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
| 16 |
}
|
|
|
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
}
|
|
|
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
| 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 |
}
|