diff --git a/static/ambient/index.html b/static/ambient/index.html
index fd541e0..555c36a 100644
--- a/static/ambient/index.html
+++ b/static/ambient/index.html
@@ -16,6 +16,10 @@
overflow-x:hidden; overflow-y:auto; }
.embed header { display:none; }
.embed #share { display:none; }
+ .embed .state { padding-right:58px; } /* oben rechts liegt in diora ein Knopf */
+ .noise .m-music, .noise .meters { display:none !important; }
+ html:not(.noise) .m-noise { display:none; }
+ .noise:not(.rev) .m-hall { display:none; }
main > * { pointer-events:auto; }
h1 { margin:0; font-weight:300; letter-spacing:.35em; text-transform:uppercase; font-size:18px; }
.sub { color:var(--dim); font-size:13px; margin-top:4px; }
@@ -68,20 +72,24 @@
@@ -165,6 +173,7 @@ const S = {
timers: [], waves: [], loop: { deg:[], oct:[] }, vis: [],
ring: null, wp: 0, written: 0, grainBuf: null,
};
+let rafId = 0; // Zeichenschleife (siehe wake/draw)
const speed = () => 0.35 + S.drift * 2.2;
// Spannungsbogen: 0 = ruhig, 1 = Höhepunkt, über mehrere Minuten
@@ -293,6 +302,9 @@ function buildGraph() {
g.voiceBus.connect(proc); proc.connect(zero).connect(ctx.destination);
g.grainBus = ctx.createGain(); g.grainBus.connect(g.filter);
+ g.modeBus = ctx.createGain(); g.modeBus.connect(g.mix); // Klangwelten (Rauschen, Natur, …)
+ g.modeRev = ctx.createGain(); g.modeRev.gain.value = 0; g.modeBus.connect(g.modeRev).connect(g.conv);
+
g.analyser = ctx.createAnalyser(); g.analyser.fftSize = 2048; g.analyser.smoothingTimeConstant = .88;
g.master.connect(g.analyser); g.master.connect(ctx.destination);
}
@@ -562,6 +574,7 @@ function later(fn, minS, maxS, salt, first = .3) {
function tick() {
if (!S.running) return;
+ if (!MODES[S.mode].music) { tickMode(); return; }
const e = eff(), k = .024 * speed(), t = S.ctx.currentTime, G = S.g;
for (const key in S.cur) S.cur[key] += (S.tgt[key] - S.cur[key]) * k;
G.filter.frequency.setTargetAtTime(500 + S.cur.bright * 4500 * (.6 + .6 * e), t, 2);
@@ -583,6 +596,423 @@ function resetScene() {
changeKey(true); setupLoops();
}
+// ---------- Klangwelten (Modi) ----------
+// Musik ist nur ein Modus von vielen. Alle anderen sind ebenfalls komplett synthetisiert.
+S.mode = "ambient"; S.modeObj = null; S.mp = { a: .5, b: .5 }; S.flash = 0; S.tickId = 0;
+const T = () => S.ctx.currentTime;
+const sm = (p, v, tc = .6) => p.setTargetAtTime(v, T(), tc);
+const R = (a, b) => a + Math.random() * (b - a);
+function bq(type, f, Q = .7, gain = 0) {
+ const n = S.ctx.createBiquadFilter(); n.type = type; n.frequency.value = f; n.Q.value = Q; n.gain.value = gain; return n;
+}
+
+// Rauschfarben: 10 s Stereo-Loop pro Typ, Naht per Equal-Power-Crossfade, auf gleiche Lautheit normalisiert
+const NB = {};
+function noiseBuf(type) {
+ if (NB[type]) return NB[type];
+ const ctx = S.ctx, sr = ctx.sampleRate, len = sr * 10, fade = Math.floor(sr * .5), tot = len + fade;
+ const buf = ctx.createBuffer(2, len, sr);
+ for (let c = 0; c < 2; c++) {
+ const raw = new Float32Array(tot);
+ let b0=0,b1=0,b2=0,b3=0,b4=0,b5=0,b6=0, brown=0, prevP=0, prevW=0;
+ for (let i = 0; i < tot; i++) {
+ const w = Math.random() * 2 - 1;
+ if (type === "white") raw[i] = w;
+ else if (type === "violet") { raw[i] = w - prevW; prevW = w; }
+ else if (type === "brown") { brown = (brown + .02 * w) / 1.02; raw[i] = brown; }
+ else { // pink, blue (= Ableitung von pink)
+ b0=.99886*b0+w*.0555179; b1=.99332*b1+w*.0750759; b2=.969*b2+w*.153852;
+ b3=.8665*b3+w*.3104856; b4=.55*b4+w*.5329522; b5=-.7616*b5-w*.016898;
+ const p = b0+b1+b2+b3+b4+b5+b6+w*.5362; b6 = w*.115926;
+ if (type === "pink") raw[i] = p; else { raw[i] = p - prevP; prevP = p; }
+ }
+ }
+ let sum = 0; for (let i = 0; i < tot; i++) sum += raw[i] * raw[i];
+ const k = .2 / Math.sqrt(sum / tot + 1e-12), d = buf.getChannelData(c);
+ for (let i = 0; i < len; i++) d[i] = clamp(raw[i] * k, -1, 1);
+ for (let i = 0; i < fade; i++) {
+ const th = i / fade * Math.PI / 2;
+ d[i] = clamp((raw[i] * Math.sin(th) + raw[len + i] * Math.cos(th)) * k, -1, 1);
+ }
+ }
+ return NB[type] = buf;
+}
+
+// Einzelereignisse (Tropfen, Knistern, Blubbern), kreisförmig in 10 s geschrieben -> nahtloser Loop
+const DB = {};
+function dropBuf(kind, rate) {
+ const key = kind + rate; if (DB[key]) return DB[key];
+ const ctx = S.ctx, sr = ctx.sampleRate, len = sr * 10, buf = ctx.createBuffer(2, len, sr);
+ const target = { rain: .16, pop: .09, water: .14 }[kind];
+ for (let c = 0; c < 2; c++) {
+ const d = buf.getChannelData(c), N = Math.round(rate * 10);
+ if (kind === "water") fillWater(d, len, sr, rate);
+ else for (let e = 0; e < N; e++) {
+ const pos = Math.floor(Math.random() * len);
+ if (kind === "rain") {
+ const n = 40 + Math.floor(Math.random() * 220), amp = Math.pow(Math.random(), 2.2) * .9, k = .12 + Math.random() * .75;
+ let lp = 0;
+ for (let j = 0; j < n; j++) { lp += (Math.random() * 2 - 1 - lp) * k; d[(pos + j) % len] += lp * Math.exp(-j / (n * .25)) * amp; }
+ } else if (kind === "pop") {
+ const burst = Math.random() < .25 ? 2 + Math.floor(Math.random() * 5) : 1;
+ for (let b = 0; b < burst; b++) {
+ const p = (pos + b * Math.floor(sr * (.004 + Math.random() * .02))) % len;
+ const n = 10 + Math.floor(Math.random() * 70), amp = Math.pow(Math.random(), 3) * 1.2, k = .35 + Math.random() * .6;
+ let lp = 0;
+ for (let j = 0; j < n; j++) { lp += (Math.random() * 2 - 1 - lp) * k; d[(p + j) % len] += lp * Math.exp(-j / (n * .3)) * amp; }
+ }
+ }
+ }
+ let sum = 0; for (let i = 0; i < len; i++) sum += d[i] * d[i];
+ const k = target / Math.sqrt(sum / len + 1e-12);
+ for (let i = 0; i < len; i++) d[i] = clamp(d[i] * k, -1, 1);
+ }
+ return DB[key] = buf;
+}
+
+// Wasser: tausende Luftbläschen als gedämpfte, leicht ansteigende Sinus-Schwingungen nach dem Minnaert-Modell
+// (Frequenz ≈ 3 / Radius, Dämpfung wächst mit der Frequenz). Dichte, überlagerte Bläschen ergeben das Plätschern;
+// die Ereignisrate schwankt periodisch über die 10 s, damit der Bach "atmet".
+let SIN = null;
+function fillWater(d, len, sr, rate) {
+ if (!SIN) { SIN = new Float32Array(4096); for (let i = 0; i < 4096; i++) SIN[i] = Math.sin(i / 4096 * 2 * Math.PI); }
+ const N = Math.round(rate * 10), p1 = Math.random() * 6.28, p2 = Math.random() * 6.28, p3 = Math.random() * 6.28;
+ for (let e = 0; e < N; e++) {
+ let pos;
+ do {
+ pos = Math.random();
+ var lam = 1 + .45 * Math.sin(2 * Math.PI * 3 * pos + p1) + .3 * Math.sin(2 * Math.PI * 7 * pos + p2) + .2 * Math.sin(2 * Math.PI * 11 * pos + p3);
+ } while (Math.random() * 1.95 > lam);
+ const r = (0.7 + 7.3 * Math.pow(Math.random(), 2.2)) * 1e-3; // Radius 0,7–8 mm, kleine häufiger
+ const f0 = Math.min(4500, 3 / r), dmp = .043 * f0 + .0014 * Math.pow(f0, 1.5), xi = .1 * dmp;
+ const amp = Math.pow(r / .004, 1.3) * (.3 + Math.random());
+ const n = Math.min(Math.floor(sr * 3.5 / dmp), 4000), start = Math.floor(pos * len), dec = Math.exp(-dmp / sr), att = sr * .0008;
+ let env = 1, phase = 0;
+ for (let j = 0; j < n; j++) {
+ phase += f0 * (1 + xi * j / sr) / sr;
+ d[(start + j) % len] += SIN[((phase - Math.floor(phase)) * 4096) | 0] * env * amp * (j < att ? j / att : 1);
+ env *= dec;
+ }
+ }
+}
+
+// Grillen: je eine Grille pro Buffer (Chirp aus 3–4 Pulsen), Periode teilt die Loop-Länge exakt
+const CB = [], CRICKET_BASE = R(4400, 4800); // Chor: alle Tiere nahezu gleiche Tonhöhe (±1 %)
+function cricketBuf(i) {
+ if (CB[i]) return CB[i];
+ const ctx = S.ctx, sr = ctx.sampleRate, len = sr * 8, buf = ctx.createBuffer(1, len, sr), d = buf.getChannelData(0);
+ const fc = CRICKET_BASE * (1 + R(-.012, .012)), nCh = Math.floor(8 / R(.36, .42)), P = len / nCh, pulses = 3 + Math.floor(Math.random() * 2), off = Math.random() * P;
+ for (let k = 0; k < nCh; k++) for (let p = 0; p < pulses; p++) {
+ const start = Math.floor(k * P + off + p * sr * .024), n = Math.floor(sr * .016);
+ for (let j = 0; j < n; j++) d[(start + j) % len] += Math.sin(2 * Math.PI * fc * j / sr) * Math.pow(Math.sin(Math.PI * j / n), 2) * (p === 0 ? .7 : 1);
+ }
+ let sum = 0; for (let j = 0; j < len; j++) sum += d[j] * d[j];
+ const k = .16 / Math.sqrt(sum / len + 1e-12);
+ for (let j = 0; j < len; j++) d[j] = clamp(d[j] * k, -1, 1);
+ return CB[i] = buf;
+}
+
+// Baukasten pro Modus: eigener Ausgang, Quellen, Timer – alles lässt sich sauber ausblenden
+function newKit(gain) {
+ const ctx = S.ctx, K = { out: ctx.createGain(), srcs: [], timers: [], dead: false, a: .5, b: .5, set() {} };
+ K.out.gain.value = 0; K.out.gain.setTargetAtTime(gain, T(), 1.4);
+ K.out.connect(S.g.modeBus);
+ K.src = (buf, dest, rate = 1) => {
+ const s = ctx.createBufferSource(); s.buffer = buf; s.loop = true; s.playbackRate.value = rate;
+ s.connect(dest); s.start(0, Math.random() * buf.duration); K.srcs.push(s); return s;
+ };
+ K.osc = (type, f, dest) => { const o = ctx.createOscillator(); o.type = type; o.frequency.value = f; o.connect(dest); o.start(); K.srcs.push(o); return o; };
+ K.every = (fn, min, max, first = .1) => {
+ const slot = K.timers.length; K.timers.push(0);
+ const val = x => typeof x === "function" ? x() : x;
+ const loop = () => { if (K.dead) return; fn(); K.timers[slot] = setTimeout(loop, R(val(min), val(max)) * 1000); };
+ K.timers[slot] = setTimeout(loop, first * 1000);
+ };
+ K.apply = (a, b) => { K.a = a; K.b = b; K.set(a, b); };
+ K.stop = () => {
+ K.dead = true; K.timers.forEach(clearTimeout);
+ K.out.gain.cancelScheduledValues(T()); K.out.gain.setTargetAtTime(0, T(), .7);
+ setTimeout(() => { K.srcs.forEach(s => { try { s.stop(); } catch (e) {} }); try { K.out.disconnect(); } catch (e) {} }, 4000);
+ };
+ return K;
+}
+
+// ----- Bauteile der einzelnen Modi -----
+function buildColor(K, type) {
+ const ctx = S.ctx, head = ctx.createGain();
+ K.src(noiseBuf(type === "grey" ? "white" : type), head);
+ let n = head;
+ if (type === "grey") for (const f of [bq("lowshelf", 200, .7, 12), bq("peaking", 3200, .7, -10), bq("highshelf", 9000, .7, 9)]) { n.connect(f); n = f; }
+ const lp = bq("lowpass", 20000, .5), am = ctx.createGain(), lfoG = ctx.createGain();
+ n.connect(lp); lp.connect(am).connect(K.out);
+ K.osc("sine", .11, lfoG); lfoG.connect(am.gain);
+ K.set = (a, b) => { sm(lp.frequency, 500 * Math.pow(40, a), .4); const d = b * .8; sm(am.gain, 1 - d / 2); sm(lfoG.gain, d / 2); };
+}
+
+function buildRain(K, storm) {
+ const ctx = S.ctx, gl = ctx.createGain(), gh = ctx.createGain(), lp = bq("lowpass", 9000, .5);
+ const wash = ctx.createGain(), hp = bq("highpass", 350, .5), wl = bq("lowpass", 7000, .5);
+ K.src(dropBuf("rain", 60), gl); K.src(dropBuf("rain", 320), gh); gl.connect(lp); gh.connect(lp);
+ K.src(noiseBuf("pink"), hp); hp.connect(wl).connect(wash).connect(lp); lp.connect(K.out);
+ K.set = (a, b) => {
+ sm(gl.gain, 1 - a); sm(gh.gain, a * 1.1); sm(wash.gain, .05 + .25 * a);
+ sm(lp.frequency, storm ? 7000 : 2500 * Math.pow(5.5, b), .6);
+ };
+ if (storm) K.every(() => thunder(K), () => 9 / (.3 + K.a), () => 32 / (.3 + K.a), 4);
+}
+function thunder(K) {
+ const ctx = S.ctx, near = K.b, d = (1 - near) * 5 + .3 + Math.random() * 1.2, t0 = T() + d;
+ S.flash = .4 + .6 * near; setTimeout(() => { if (!K.dead) S.flash = Math.max(S.flash, .3 + .5 * near); }, 140);
+ const dur = R(6, 10), lp = bq("lowpass", 100 + 220 * near, .7), g = ctx.createGain(), s = ctx.createBufferSource();
+ s.buffer = noiseBuf("brown"); s.connect(lp).connect(g).connect(K.out);
+ const peak = .9 * (.35 + .65 * near);
+ lp.frequency.setValueAtTime(100 + 220 * near, t0); lp.frequency.exponentialRampToValueAtTime(60, t0 + dur);
+ g.gain.setValueAtTime(0, t0); g.gain.linearRampToValueAtTime(peak, t0 + .35);
+ let tt = t0 + .35, amp = peak;
+ for (let i = 0; i < 4; i++) { tt += R(.8, 1.8); amp *= R(.45, .85); g.gain.linearRampToValueAtTime(amp * R(.6, 1), tt); }
+ g.gain.linearRampToValueAtTime(0, t0 + dur);
+ s.start(t0, R(0, .5), dur + .2);
+ const cb = bq("bandpass", R(900, 2500), .8), cg = ctx.createGain(), cs = ctx.createBufferSource();
+ cs.buffer = noiseBuf("white"); cs.connect(cb).connect(cg).connect(K.out);
+ cg.gain.setValueAtTime(0, t0); cg.gain.linearRampToValueAtTime(.7 * near * near, t0 + .01); cg.gain.exponentialRampToValueAtTime(.001, t0 + .4);
+ cs.start(t0, R(0, 3), .5);
+}
+
+function buildOcean(K) {
+ const ctx = S.ctx, lp = bq("lowpass", 220, .6), bg = ctx.createGain(), hp = bq("highpass", 1800, .5), fl = bq("lowpass", 7500, .5), fg = ctx.createGain();
+ K.src(noiseBuf("brown"), lp); lp.connect(bg).connect(K.out); bg.gain.value = .12;
+ K.src(noiseBuf("white"), hp); hp.connect(fl).connect(fg).connect(K.out); fg.gain.value = 0;
+ const bed = ctx.createGain(), bl = bq("lowpass", 900, .5); bed.gain.value = .06; K.src(noiseBuf("pink"), bl); bl.connect(bed).connect(K.out);
+ K.waveEnd = T() + .1;
+ const sched = () => {
+ if (K.dead) return;
+ const a = K.a, b = K.b, Tw = R(7, 12) * (1.25 - .35 * a), A = (.35 + .65 * a) * R(.6, 1), t0 = K.waveEnd, rise = Tw * .42;
+ bg.gain.linearRampToValueAtTime(.12, t0); bg.gain.linearRampToValueAtTime(A, t0 + rise); bg.gain.linearRampToValueAtTime(.12, t0 + Tw);
+ lp.frequency.linearRampToValueAtTime(220, t0); lp.frequency.linearRampToValueAtTime(350 + 900 * b + 500 * A, t0 + rise); lp.frequency.linearRampToValueAtTime(220, t0 + Tw);
+ fg.gain.linearRampToValueAtTime(0, t0 + rise * .6); fg.gain.linearRampToValueAtTime(A * .55 * (.25 + b), t0 + rise * 1.15); fg.gain.linearRampToValueAtTime(0, t0 + Tw * .98);
+ K.waveEnd = t0 + Tw;
+ K.timers[0] = setTimeout(sched, Math.max(300, (K.waveEnd - T() - 2.5) * 1000));
+ };
+ K.timers.push(0); sched();
+}
+
+function buildWind(K) {
+ const ctx = S.ctx, bp = bq("bandpass", 500, 1.2), g = ctx.createGain(), wb = bq("bandpass", 900, 22), wg = ctx.createGain(), low = bq("lowpass", 250, .5), lg = ctx.createGain();
+ g.gain.value = 0; wg.gain.value = 0; lg.gain.value = .3;
+ K.src(noiseBuf("pink"), bp); bp.connect(g).connect(K.out);
+ K.src(noiseBuf("white"), wb); wb.connect(wg).connect(K.out);
+ K.src(noiseBuf("brown"), low); low.connect(lg).connect(K.out);
+ K.set = a => sm(lg.gain, .15 + .6 * a, 1);
+ K.every(() => {
+ const a = K.a, b = K.b;
+ sm(bp.frequency, R(250, 1100) * (.6 + .8 * a), 2.5); sm(bp.Q, R(.7, 2.2), 2.5);
+ sm(g.gain, (.12 + R(0, .8) * a) * .9, 2.2);
+ sm(wb.frequency, R(600, 1800), 3); sm(wg.gain, b * R(.3, 1) * 10, 2.5);
+ }, 2.5, 6, .1);
+}
+
+function buildStream(K) {
+ const ctx = S.ctx, g1 = ctx.createGain(), g2 = ctx.createGain(), hp = bq("highpass", 220, .5);
+ const s1 = K.src(dropBuf("water", 350), g1), s2 = K.src(dropBuf("water", 1300), g2);
+ g1.connect(hp); g2.connect(hp);
+ const fl = bq("bandpass", 1200, .5), fg = ctx.createGain(); K.src(noiseBuf("pink"), fl); fl.connect(fg).connect(hp); // verschmiert die Bläschen zu einem Fluss
+ const low = bq("lowpass", 220, .5), lg = ctx.createGain(); lg.gain.value = .12; K.src(noiseBuf("brown"), low); low.connect(lg).connect(K.out);
+ hp.connect(K.out);
+ K.set = (a, b) => {
+ sm(g1.gain, 1 - a * .8); sm(g2.gain, a * 1.2); sm(fg.gain, .04 + .12 * a);
+ const rt = .75 + .65 * b; sm(s1.playbackRate, rt); sm(s2.playbackRate, rt * 1.03);
+ };
+ K.every(() => sm(fl.frequency, R(800, 2400), 2), 2, 5);
+}
+
+function buildFire(K) {
+ const ctx = S.ctx, rl = bq("lowpass", 450, .6), rg = ctx.createGain(), hp = bq("highpass", 3500, .5), hg = ctx.createGain();
+ const g1 = ctx.createGain(), g2 = ctx.createGain(), ph = bq("highpass", 600, .5);
+ K.src(noiseBuf("brown"), rl); rl.connect(rg).connect(K.out); rg.gain.value = .5;
+ K.src(noiseBuf("pink"), hp); hp.connect(hg).connect(K.out); hg.gain.value = .03;
+ K.src(dropBuf("pop", 12), g1); K.src(dropBuf("pop", 45), g2); g1.connect(ph); g2.connect(ph); ph.connect(K.out);
+ K.set = (a, b) => { sm(g1.gain, (1 - b) * (.6 + .5 * a)); sm(g2.gain, b * (.6 + .5 * a)); };
+ K.every(() => sm(rg.gain, (.2 + .6 * K.a) * R(.55, 1), .35), .3, 1.2);
+}
+
+function buildCrickets(K) {
+ const ctx = S.ctx, cs = [];
+ for (let i = 0; i < 6; i++) {
+ const g = ctx.createGain(), pn = ctx.createStereoPanner(); g.gain.value = 0; pn.pan.value = R(-.85, .85);
+ const src = K.src(cricketBuf(i), g); g.connect(pn).connect(K.out); cs.push({ g, src });
+ }
+ const bed = ctx.createGain(), lp = bq("lowpass", 220, .5); bed.gain.value = .05; K.src(noiseBuf("brown"), lp); lp.connect(bed).connect(K.out);
+ K.set = (a, b) => { const n = 1 + a * 5; cs.forEach((c, i) => { sm(c.g.gain, clamp(n - i, 0, 1) * .8, 1.2); sm(c.src.playbackRate, .8 + .45 * b, 1); }); };
+}
+
+function buildFan(K, cabin) {
+ const ctx = S.ctx, lp = bq("lowpass", 900, .6), g = ctx.createGain(), hp = bq("highpass", 2000, .5), lp2 = bq("lowpass", 6000, .5), hg = ctx.createGain(), hg2 = ctx.createGain();
+ g.gain.value = 1; hg.gain.value = cabin ? .035 : .05; hg2.gain.value = 0;
+ K.src(noiseBuf("brown"), lp); lp.connect(g).connect(K.out);
+ K.src(noiseBuf("pink"), hp); hp.connect(lp2).connect(hg).connect(K.out);
+ const o1 = K.osc("sine", cabin ? 83 : 100, hg2), o2 = K.osc("sine", cabin ? 83.8 : 200, hg2), o3 = K.osc("sine", cabin ? 166 : 300, hg2);
+ hg2.connect(K.out);
+ K.set = (a, b) => {
+ if (cabin) { sm(lp.frequency, 400 + 1400 * a); sm(hg2.gain, .02 + .12 * b); }
+ else { sm(lp.frequency, 500 + 1500 * a); sm(o1.frequency, 70 + 90 * a); sm(o2.frequency, 140 + 180 * a); sm(o3.frequency, 210 + 270 * a); sm(hg2.gain, .01 + .1 * b); }
+ };
+}
+
+function buildBinaural(K) {
+ const ctx = S.ctx, l = ctx.createGain(), r = ctx.createGain(), l2 = ctx.createGain(), r2 = ctx.createGain();
+ const pl = ctx.createStereoPanner(), pr = ctx.createStereoPanner(); pl.pan.value = -1; pr.pan.value = 1;
+ l.gain.value = r.gain.value = .16; l2.gain.value = r2.gain.value = .04;
+ l.connect(pl); l2.connect(pl); r.connect(pr); r2.connect(pr); pl.connect(K.out); pr.connect(K.out);
+ const oL = K.osc("sine", 200, l), oR = K.osc("sine", 210, r), oL2 = K.osc("sine", 400, l2), oR2 = K.osc("sine", 410, r2);
+ const bed = ctx.createGain(), lp = bq("lowpass", 1800, .5); bed.gain.value = .22; K.src(noiseBuf("pink"), lp); lp.connect(bed).connect(K.out);
+ K.set = (a, b) => {
+ const beat = Math.pow(40, a), f0 = 100 * Math.pow(4, b);
+ sm(oL.frequency, f0 - beat / 2, .3); sm(oR.frequency, f0 + beat / 2, .3);
+ sm(oL2.frequency, 2 * f0 - beat / 2, .3); sm(oR2.frequency, 2 * f0 + beat / 2, .3);
+ };
+}
+
+function buildBowls(K) {
+ const ctx = S.ctx, ratios = [1, 2.71, 5.15, 8.2], amps = [1, .45, .22, .1], decs = [16, 10, 6, 3.5];
+ const strike = () => {
+ const t = T() + .05, midi = pick([48, 50, 52, 55, 57], Math.random) + 12 * Math.round(K.b * 1.5), f = mtof(midi);
+ const pn = ctx.createStereoPanner(); pn.pan.value = R(-.6, .6); pn.connect(K.out);
+ ratios.forEach((ra, i) => [-R(.3, 1.1), R(.3, 1.1)].forEach(det => {
+ const o = ctx.createOscillator(), g = ctx.createGain(), dec = decs[i] * R(.8, 1.2);
+ o.frequency.value = f * ra + det;
+ g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(amps[i] * .07, t + .008); g.gain.exponentialRampToValueAtTime(.0001, t + dec);
+ o.connect(g).connect(pn); o.start(t); o.stop(t + dec + .1);
+ }));
+ };
+ K.every(strike, () => 4 / (.35 + K.a * 1.4), () => 9 / (.35 + K.a * 1.4), .3);
+}
+
+function buildWomb(K) {
+ const ctx = S.ctx, lp = bq("lowpass", 500, .5), g = ctx.createGain(), sw = bq("bandpass", 900, .4), swg = ctx.createGain(), tl = bq("lowpass", 160, .7);
+ g.gain.value = .55; swg.gain.value = .12;
+ K.src(noiseBuf("brown"), lp); lp.connect(g).connect(K.out);
+ K.src(noiseBuf("pink"), sw); sw.connect(swg).connect(K.out); tl.connect(K.out);
+ const thump = (t, f0, amp) => {
+ const o = ctx.createOscillator(), og = ctx.createGain();
+ o.frequency.setValueAtTime(f0, t); o.frequency.exponentialRampToValueAtTime(f0 * .55, t + .14);
+ og.gain.setValueAtTime(0, t); og.gain.linearRampToValueAtTime(amp, t + .012); og.gain.exponentialRampToValueAtTime(.001, t + .25);
+ o.connect(og).connect(tl); o.start(t); o.stop(t + .3);
+ };
+ const per = () => 60 / (55 + K.a * 60);
+ K.every(() => { const t = T() + .05; thump(t, 64, .9); thump(t + .27, 72, .6); }, per, () => per() * 1.01, .3);
+ K.set = (a, b) => { sm(tl.frequency, 100 + 300 * b); sm(lp.frequency, 300 + 500 * b); };
+}
+
+// Zugfahrt: Rollgeräusch + Schienenstöße. Jeder Stoß wird von vier Achsen nacheinander angeschlagen
+// (zwei pro Drehgestell, Abstand = halbe Schienenlänge) -> gleichmäßig "tschuck-tschuck ... tschuck-tschuck", Takt folgt der Geschwindigkeit.
+function railClack(K, t, amp) {
+ const ctx = S.ctx, nb = noiseBuf("white");
+ const s1 = ctx.createBufferSource(), bp = bq("bandpass", R(500, 1100) * (.7 + .8 * K.b), .9), g1 = ctx.createGain();
+ s1.buffer = nb; s1.connect(bp).connect(g1).connect(K.out);
+ g1.gain.setValueAtTime(0, t); g1.gain.linearRampToValueAtTime(amp * .9, t + .003); g1.gain.exponentialRampToValueAtTime(.001, t + .07);
+ s1.start(t, R(0, 8), .1);
+ const o = ctx.createOscillator(), og = ctx.createGain(); // dumpfer Schlag im Wagenboden
+ o.frequency.setValueAtTime(110, t); o.frequency.exponentialRampToValueAtTime(55, t + .1);
+ og.gain.setValueAtTime(0, t); og.gain.linearRampToValueAtTime(amp * .7, t + .004); og.gain.exponentialRampToValueAtTime(.001, t + .16);
+ o.connect(og).connect(K.out); o.start(t); o.stop(t + .2);
+ const s2 = ctx.createBufferSource(), rb = bq("bandpass", R(190, 260), 8), g2 = ctx.createGain(); // Nachklingen der Karosserie
+ s2.buffer = nb; s2.connect(rb).connect(g2).connect(K.out);
+ g2.gain.setValueAtTime(0, t); g2.gain.linearRampToValueAtTime(amp * .9, t + .004); g2.gain.exponentialRampToValueAtTime(.001, t + .35);
+ s2.start(t, R(0, 8), .4);
+}
+function buildTrain(K) {
+ const ctx = S.ctx, lp = bq("lowpass", 700, .6), g = ctx.createGain(), bp = bq("bandpass", 900, .5), bg = ctx.createGain();
+ const hum = ctx.createGain(), sway = ctx.createGain(), swayG = ctx.createGain();
+ g.gain.value = .45; bg.gain.value = .12; hum.gain.value = .02;
+ K.src(noiseBuf("brown"), lp); lp.connect(g).connect(sway).connect(K.out);
+ K.src(noiseBuf("pink"), bp); bp.connect(bg).connect(sway);
+ const ho = K.osc("sine", 120, hum); hum.connect(K.out);
+ sway.gain.value = .9; K.osc("sine", .23, swayG); swayG.gain.value = .08; swayG.connect(sway.gain); // leichtes Schaukeln
+ K.v = 25; K.nextJoint = T() + .3; K.nextPoints = T() + R(25, 50);
+ K.set = (a, b) => {
+ K.v = 14 + 36 * a; // 50 … 180 km/h
+ sm(lp.frequency, 350 + 450 * a + 700 * b, 1); sm(bg.gain, .03 + .1 * a * (.4 + b), 1); sm(ho.frequency, 70 + 120 * a, 1); sm(hum.gain, .015 + .03 * a, 1);
+ };
+ K.every(() => {
+ const now = T();
+ while (K.nextJoint < now + .5) { // alle Achsen eines Schienenstoßes vorplanen
+ const v = K.v, t0 = K.nextJoint, amp = .8 * (.85 + Math.random() * .3);
+ [0, 2.5 / v, 12.5 / v, 15 / v].forEach((o, i) => railClack(K, t0 + o, amp * (i % 2 ? .8 : 1)));
+ K.nextJoint += 25 / v;
+ }
+ if (now > K.nextPoints) { // Weiche: kurzes schnelles Klapperbündel
+ const n = 7 + Math.floor(Math.random() * 5);
+ for (let i = 0; i < n; i++) railClack(K, now + .2 + i * R(.07, .13), R(.5, .9));
+ K.nextPoints = now + R(40, 90);
+ }
+ }, .12, .12, .05);
+}
+
+// ----- Modus-Tabelle -----
+const bandOf = hz => hz < 4 ? "Delta" : hz < 8 ? "Theta" : hz < 13 ? "Alpha" : hz < 30 ? "Beta" : "Gamma";
+const binInfo = (a, b) => { const hz = Math.pow(40, a); return `${hz.toFixed(1)} Hz · ${bandOf(hz)} · Träger ${Math.round(100 * Math.pow(4, b))} Hz · Kopfhörer!`; };
+const colorDesc = "Regler: Höhen-Dämpfung und langsames Atmen der Lautstärke.";
+const MODES = {
+ ambient: { group: "Musik", name: "Ambient-Musik", music: true, hue: 220 },
+ white: { group: "Rauschfarben", name: "Weißes Rauschen", desc: "Gleiche Energie in allen Frequenzen – hell und rauschig, maskiert Geräusche gut. " + colorDesc, labels: ["Höhen-Dämpfung", "Atmen"], def: [.9, 0], gain: 0.3, hue: 215, vis: "float", build: K => buildColor(K, "white") },
+ pink: { group: "Rauschfarben", name: "Pinkes Rauschen", desc: "Nimmt pro Oktave um 3 dB ab – ausgewogen, wie Regen und Blätterrauschen. " + colorDesc, labels: ["Höhen-Dämpfung", "Atmen"], def: [.9, 0], gain: 0.42, hue: 330, vis: "float", build: K => buildColor(K, "pink") },
+ brown: { group: "Rauschfarben", name: "Braunes Rauschen", desc: "Tief und dumpf (−6 dB pro Oktave), wie Donnergrollen oder ein Wasserfall. " + colorDesc, labels: ["Höhen-Dämpfung", "Atmen"], def: [.9, 0], gain: 0.6, hue: 28, vis: "float", build: K => buildColor(K, "brown") },
+ blue: { group: "Rauschfarben", name: "Blaues Rauschen", desc: "Zu den Höhen ansteigend, zischend und klar. " + colorDesc, labels: ["Höhen-Dämpfung", "Atmen"], def: [.9, 0], gain: 0.25, hue: 205, vis: "float", build: K => buildColor(K, "blue") },
+ violet: { group: "Rauschfarben", name: "Violettes Rauschen", desc: "Noch stärker zu den Höhen – sehr hell, fast nur Zischen. " + colorDesc, labels: ["Höhen-Dämpfung", "Atmen"], def: [.8, 0], gain: 0.25, hue: 275, vis: "float", build: K => buildColor(K, "violet") },
+ grey: { group: "Rauschfarben", name: "Graues Rauschen", desc: "Weißes Rauschen, an die Ohrempfindlichkeit angepasst – wirkt über alle Höhen gleich laut. " + colorDesc, labels: ["Höhen-Dämpfung", "Atmen"], def: [.9, 0], gain: 0.2, hue: 190, vis: "float", build: K => buildColor(K, "grey") },
+
+ rain: { group: "Natur", name: "Regen", desc: "Tausende einzeln synthetisierte Tropfen auf einem Rauschteppich.", labels: ["Stärke", "Dumpf ↔ Hell"], def: [.5, .55], gain: 1.3, hue: 210, vis: "rain", build: K => buildRain(K, false) },
+ storm: { group: "Natur", name: "Gewitter", desc: "Starker Regen mit Blitz und rollendem Donner in unregelmäßigen Abständen.", labels: ["Häufigkeit & Regen", "Entfernung (fern ↔ nah)"], def: [.6, .5], gain: .8, hue: 245, vis: "rain", rev: .5, build: K => buildRain(K, true) },
+ ocean: { group: "Natur", name: "Meer", desc: "Wellen, die anrollen, brechen und im Schaum auslaufen.", labels: ["Wellengang", "Brandung dumpf ↔ hell"], def: [.5, .5], gain: .9, hue: 195, vis: "float", build: buildOcean },
+ wind: { group: "Natur", name: "Wind", desc: "Böen mit wandernder Farbe, auf Wunsch mit feinem Pfeifen.", labels: ["Stärke", "Pfeifen"], def: [.5, .25], gain: .9, hue: 180, vis: "wind", build: buildWind },
+ stream: { group: "Natur", name: "Bach", desc: "Plätscherndes Wasser aus tausenden einzeln berechneten Luftbläschen.", labels: ["Strömung", "Tonhöhe"], def: [.5, .5], gain: 0.5, hue: 170, vis: "float", build: buildStream },
+ fire: { group: "Natur", name: "Lagerfeuer", desc: "Rollendes Brummen der Flamme und Knistern.", labels: ["Flammenstärke", "Knistern selten ↔ dicht"], def: [.5, .45], gain: .8, hue: 25, vis: "rise", build: buildFire },
+ crickets: { group: "Natur", name: "Sommernacht", desc: "Bis zu sechs Grillen, jede mit eigenem Rhythmus und eigener Tonhöhe.", labels: ["Anzahl", "Tonhöhe / Tempo"], def: [.5, .45], gain: 0.2, hue: 75, vis: "fly", build: buildCrickets },
+
+ fan: { group: "Technik", name: "Ventilator", desc: "Gleichmäßiger Luftstrom mit tiefem Motorbrummen.", labels: ["Drehzahl", "Brummen"], def: [.4, .35], gain: 0.3, hue: 200, vis: "float", build: K => buildFan(K, false) },
+ cabin: { group: "Technik", name: "Flugzeugkabine", desc: "Dumpfes Rauschen und tiefes Triebwerksdröhnen – beliebt zum Einschlafen.", labels: ["Dumpf ↔ offen", "Triebwerk"], def: [.3, .5], gain: 0.27, hue: 215, vis: "float", build: K => buildFan(K, true) },
+
+ train: { group: "Technik", name: "Zugfahrt", desc: "Gleichmäßiges Rollen mit dem Tschuck-tschuck der Schienenstöße, ab und zu rattert eine Weiche.", labels: ["Geschwindigkeit", "Abteil dumpf ↔ Fenster offen"], def: [.4, .35], gain: .45, hue: 35, vis: "wind", info: a => Math.round((14 + 36 * a) * 3.6) + " km/h", build: buildTrain },
+
+ delta: { group: "Fokus & Schlaf", name: "Binaurale Beats: Delta", desc: "Zwei leicht verstimmte Sinustöne links und rechts, Differenz ≈ 2 Hz (Tiefschlaf).", labels: ["Beat-Frequenz", "Trägerton"], def: [.19, .35], gain: 0.45, hue: 250, vis: "float", info: binInfo, build: buildBinaural },
+ theta: { group: "Fokus & Schlaf", name: "Binaurale Beats: Theta", desc: "Differenz ≈ 6 Hz (Meditation, Dämmerzustand).", labels: ["Beat-Frequenz", "Trägerton"], def: [.49, .35], gain: 0.45, hue: 280, vis: "float", info: binInfo, build: buildBinaural },
+ alpha: { group: "Fokus & Schlaf", name: "Binaurale Beats: Alpha", desc: "Differenz ≈ 10 Hz (entspannte Wachheit).", labels: ["Beat-Frequenz", "Trägerton"], def: [.62, .35], gain: 0.45, hue: 160, vis: "float", info: binInfo, build: buildBinaural },
+ beta: { group: "Fokus & Schlaf", name: "Binaurale Beats: Beta", desc: "Differenz ≈ 18 Hz (Konzentration).", labels: ["Beat-Frequenz", "Trägerton"], def: [.78, .35], gain: 0.45, hue: 45, vis: "float", info: binInfo, build: buildBinaural },
+ bowls: { group: "Fokus & Schlaf", name: "Klangschalen", desc: "Angeschlagene Schalen mit schwebenden Obertönen und langem Nachklang.", labels: ["Häufigkeit", "Tonhöhe"], def: [.4, .35], gain: 1.4, hue: 40, rev: .9, vis: "float", build: buildBowls },
+ womb: { group: "Fokus & Schlaf", name: "Herzschlag", desc: "Gedämpfter Herzschlag in tiefem Rauschen – wie im Mutterleib.", labels: ["Herzschlag-Tempo", "Dumpf ↔ nah"], def: [.2, .4], gain: 0.25, hue: 5, rev: .15, vis: "float", build: buildWomb },
+};
+
+function applyRev() {
+ if (!S.g) return;
+ const M = MODES[S.mode];
+ S.g.modeRev.gain.setTargetAtTime(M.music ? 0 : (M.rev || 0) * S.verb * 1.2, S.ctx.currentTime, .3);
+}
+function startSoundscape() {
+ const M = MODES[S.mode], K = newKit(M.gain || .7);
+ S.modeObj = K; M.build(K); K.apply(S.mp.a, S.mp.b); applyRev();
+}
+function tickMode() { // „Wandel": die beiden Regler wandern langsam von selbst
+ const K = S.modeObj; if (!K) return;
+ const t = S.ctx.currentTime, amp = S.drift * .3;
+ const w1 = Math.sin(t * .031 + 1) * .5 + Math.sin(t * .083 + 4) * .5, w2 = Math.sin(t * .027 + 2) * .5 + Math.sin(t * .067 + 5) * .5;
+ K.ea = clamp(S.mp.a + amp * w1, 0, 1); K.eb = clamp(S.mp.b + amp * w2, 0, 1);
+ K.apply(K.ea, K.eb); updateInfo();
+}
+function leaveMusic() {
+ S.timers.forEach(clearTimeout); S.timers = [];
+ const G = S.g, t = S.ctx.currentTime;
+ G.voiceBus.gain.setTargetAtTime(0, t, .7); G.grainBus.gain.setTargetAtTime(0, t, .7);
+ G.windGain.gain.setTargetAtTime(0, t, .7); G.seaGain.gain.setTargetAtTime(0, t, .7);
+}
+function enterMode() {
+ if (MODES[S.mode].music) {
+ const G = S.g, t = S.ctx.currentTime;
+ G.voiceBus.gain.setTargetAtTime(1, t, .3); G.grainBus.gain.setTargetAtTime(1, t, .3);
+ startMusic();
+ } else startSoundscape();
+ applyRev();
+}
+
// Kommunikation mit der einbettenden Seite (diora): Radio/Podcast ↔ Ambient
function post(state) {
try { if (window.parent !== window) window.parent.postMessage({ type: "ambient", state }, "*"); } catch (e) {}
@@ -593,17 +1023,11 @@ addEventListener("message", e => {
if (!d || d.type !== "ambient") return;
if (d.cmd === "start" && !S.running) start();
else if (d.cmd === "stop" && S.running) stop();
+ else if (d.cmd === "mode" && MODES[d.mode]) setMode(d.mode);
});
-function start() {
- if (!S.ctx) buildGraph();
- S.running = true; S.timers = [];
- // Ohne Nutzergeste (z. B. Befehl von außen) kann der Browser den Ton blockieren -> dann ehrlich "stopped" melden
- S.ctx.resume().then(() => { if (S.running && S.ctx.state !== "running") stop(); }, () => { if (S.running) stop(); });
+function startMusic() {
resetScene();
- const t = S.ctx.currentTime;
- S.g.master.gain.cancelScheduledValues(t);
- S.g.master.gain.setTargetAtTime(+$("vol").value, t, 2.5);
drone(); playChord();
later(playChord, 10, 18, 1);
later(playBells, 2, 6, 2);
@@ -614,13 +1038,26 @@ function start() {
later(grainCloud, 3, 8, 7);
later(snapGrains, 20, 30, 8, .3);
startLoops();
- S.timers.push(setInterval(tick, 1000));
+}
+
+function start() {
+ if (!S.ctx) buildGraph();
+ S.running = true; S.timers = [];
+ // Ohne Nutzergeste (z. B. Befehl von außen) kann der Browser den Ton blockieren -> dann ehrlich "stopped" melden
+ S.ctx.resume().then(() => { if (S.running && S.ctx.state !== "running") stop(); }, () => { if (S.running) stop(); });
+ const t = S.ctx.currentTime;
+ S.g.master.gain.cancelScheduledValues(t);
+ S.g.master.gain.setTargetAtTime(+$("vol").value, t, 2.5);
+ S.tickId = setInterval(tick, 1000);
+ enterMode();
$("toggle").textContent = "Stopp";
wake(); post("playing");
}
function stop() {
S.running = false;
S.timers.forEach(t => { clearTimeout(t); clearInterval(t); }); S.timers = [];
+ clearInterval(S.tickId);
+ if (S.modeObj) { S.modeObj.stop(); S.modeObj = null; }
const t = S.ctx.currentTime;
S.g.master.gain.cancelScheduledValues(t);
S.g.master.gain.setTargetAtTime(0, t, 1.5);
@@ -631,12 +1068,12 @@ function stop() {
}
function reroll() { // neue Szene, laufende Stimmen klingen aus
resetScene();
- if (S.running) { drone(); playChord(); }
+ if (S.running && MODES[S.mode].music) { drone(); playChord(); }
}
// ---------- Seed / Link ----------
function writeHash() {
- try { history.replaceState(null, "", "#seed=" + encodeURIComponent(S.seedText) + "&p=" + encodeURIComponent(S.presetName)); } catch (e) {}
+ try { history.replaceState(null, "", "#seed=" + encodeURIComponent(S.seedText) + "&p=" + encodeURIComponent(S.presetName) + "&m=" + encodeURIComponent(S.mode)); } catch (e) {}
}
function applySeed(text) {
S.seedText = text.trim() || String(Math.floor(Math.random() * 900000) + 100000);
@@ -646,7 +1083,7 @@ function applySeed(text) {
function applyPreset(name) {
if (!PRESETS[name]) name = "Frei";
S.presetName = name; S.P = PRESETS[name]; $("preset").value = name; writeHash();
- if (S.running) { wanderTimbre(); changeKey(false); }
+ if (S.running && MODES[S.mode].music) { wanderTimbre(); changeKey(false); }
}
// ---------- UI ----------
@@ -664,6 +1101,14 @@ function arcLabel() {
return p < .15 ? "Ruhe" : p < .4 ? "Aufbau" : p < .6 ? "Höhepunkt" : p < .85 ? "Rückzug" : "Ruhe";
}
function updateInfo() {
+ const M = MODES[S.mode];
+ if (!M.music) {
+ const K = S.modeObj;
+ $("key").textContent = M.name;
+ $("chord").innerHTML = M.desc + (M.info ? ` ${M.info(K && K.ea != null ? K.ea : S.mp.a, K && K.eb != null ? K.eb : S.mp.b)}` : "");
+ return;
+ }
+ if (!S.running) { $("key").textContent = "–"; $("chord").textContent = "Noch still."; return; }
$("key").textContent = `${NOTE[S.root % 12]} ${S.scaleName}`;
$("chord").innerHTML = `Stufe ${ROMAN[S.degree] || S.degree + 1} · ${S.presetName} · ${S.tuning === "just" ? "rein" : "gleichstufig"} · Bogen: ${arcLabel()}`;
}
@@ -674,7 +1119,7 @@ $("vol").oninput = e => { if (S.running) S.g.master.gain.setTargetAtTime(+e.targ
$("density").oninput = e => S.density = +e.target.value;
$("drift").oninput = e => S.drift = +e.target.value;
$("arc").oninput = e => S.arcDepth = +e.target.value;
-$("verb").oninput = e => { S.verb = +e.target.value; if (S.g) S.g.send.gain.setTargetAtTime(S.verb * 1.2, S.ctx.currentTime, .2); };
+$("verb").oninput = e => { S.verb = +e.target.value; if (S.g) { S.g.send.gain.setTargetAtTime(S.verb * 1.2, S.ctx.currentTime, .2); applyRev(); } };
$("tuning").onchange = e => { S.tuning = e.target.value; updateInfo(); };
$("preset").onchange = e => applyPreset(e.target.value);
$("seed").onchange = e => applySeed(e.target.value);
@@ -687,16 +1132,46 @@ $("share").onclick = async () => {
};
S.density = +$("density").value; S.drift = +$("drift").value; S.verb = +$("verb").value; S.arcDepth = +$("arc").value;
-// Startwerte aus dem Link (#seed=…&p=…)
+// Modus-Auswahl
+$("mode").innerHTML = (() => {
+ const groups = {};
+ for (const [k, m] of Object.entries(MODES)) (groups[m.group] = groups[m.group] || []).push(``);
+ return Object.entries(groups).map(([g, o]) => ``).join("");
+})();
+function applyModeUI() {
+ const M = MODES[S.mode], root = document.documentElement;
+ root.classList.toggle("noise", !M.music); root.classList.toggle("rev", !!M.rev);
+ if (!M.music) { $("lla").textContent = M.labels[0]; $("llb").textContent = M.labels[1]; $("la").value = S.mp.a; $("lb").value = S.mp.b; }
+ $("driftL").textContent = M.music ? "Wandel-Tempo" : "Schwankung";
+ applyRev();
+}
+function setMode(name) {
+ if (!MODES[name]) name = "ambient";
+ const was = MODES[S.mode], M = MODES[name];
+ S.mode = name; $("mode").value = name;
+ if (!M.music) S.mp = { a: M.def[0], b: M.def[1] };
+ applyModeUI(); writeHash();
+ if (S.running) {
+ if (was.music) leaveMusic();
+ if (S.modeObj) { S.modeObj.stop(); S.modeObj = null; }
+ enterMode();
+ }
+ updateInfo(); wake();
+}
+$("mode").onchange = e => setMode(e.target.value);
+$("la").oninput = e => { S.mp.a = +e.target.value; if (S.modeObj) S.modeObj.apply(S.mp.a, S.mp.b); };
+$("lb").oninput = e => { S.mp.b = +e.target.value; if (S.modeObj) S.modeObj.apply(S.mp.a, S.mp.b); };
+
+// Startwerte aus dem Link (#seed=…&p=…&m=…)
{
const q = new URLSearchParams(location.hash.slice(1));
applyPreset(q.get("p") || "Frei");
applySeed(q.get("seed") || "");
+ setMode(q.get("m") || "ambient");
}
// ---------- Visualisierung ----------
const cv = $("viz"), cx = cv.getContext("2d");
-let rafId = 0;
S.visUntil = performance.now() + 1500;
function wake() { if (!rafId) rafId = requestAnimationFrame(draw); }
function resize() {
@@ -723,6 +1198,29 @@ function spawnParticles(ev) {
});
}
}
+// Partikel der Klangwelten: Regen, Funken, Windschlieren, Glühwürmchen, sonst treibende Lichter
+function modeVis(W, H, dpr) {
+ const M = MODES[S.mode], K = S.modeObj;
+ if (M.music || !S.running || !K) return;
+ const a = K.a, add = p => { if (parts.length < 450) parts.push(Object.assign({ life: 0, vx: 0, vy: 0, h: M.hue }, p)); };
+ const n = x => Math.random() < x, r = (lo, hi) => lo + Math.random() * (hi - lo);
+ switch (M.vis) {
+ case "rain":
+ for (let i = 0; i < 1 + Math.floor(a * 5); i++) add({ x: Math.random() * W, y: -8 * dpr, vx: -1.2, vy: r(7, 11), r: r(1.6, 3) * dpr, max: r(40, 80) });
+ break;
+ case "rise":
+ if (n(.5 + a * .4)) add({ x: W * r(.35, .65), y: H * .9, vx: r(-.5, .5), vy: -r(.8, 2.4), r: r(3, 9) * dpr, max: r(70, 160), h: M.hue + r(-10, 25) });
+ break;
+ case "wind":
+ if (n(.4 + a * .5)) add({ x: -10 * dpr, y: Math.random() * H, vx: r(4, 9) * (.5 + a), vy: r(-.4, .4), r: r(2, 5) * dpr, max: r(60, 140) });
+ break;
+ case "fly":
+ if (n(.05)) add({ x: Math.random() * W, y: H * r(.3, .95), vx: r(-.3, .3), vy: r(-.3, .3), r: r(5, 10) * dpr, max: r(200, 420) });
+ break;
+ default:
+ if (n(.06 + a * .05)) add({ x: Math.random() * W, y: H * r(.5, .95), vx: r(-.2, .2), vy: -r(.1, .5), r: r(10, 28) * dpr, max: r(300, 600) });
+ }
+}
function draw(ts) {
rafId = 0;
const W = cv.width, H = cv.height, dpr = devicePixelRatio;
@@ -736,7 +1234,8 @@ function draw(ts) {
const bins = new Uint8Array(S.g.analyser.frequencyBinCount);
S.g.analyser.getByteFrequencyData(bins);
let lvl = 0; for (let i = 0; i < 80; i++) lvl += bins[i]; lvl = lvl / 80 / 255;
- const target = (S.root % 12) * 30 + 200 + S.cur.bright * 40;
+ const M0 = MODES[S.mode];
+ const target = M0.music ? (S.root % 12) * 30 + 200 + S.cur.bright * 40 : M0.hue;
hueV += (((target - hueV + 540) % 360) - 180) * .01;
cx.globalCompositeOperation = "lighter";
@@ -748,6 +1247,7 @@ function draw(ts) {
gr.addColorStop(0, `hsla(${hueV + i * 28},70%,50%,${.04 + lvl * .16})`); gr.addColorStop(1, "hsla(0,0%,0%,0)");
cx.fillStyle = gr; cx.fillRect(0, 0, W, H);
}
+ modeVis(W, H, dpr);
// Notenereignisse -> Partikel
const now = S.ctx.currentTime;
S.vis = S.vis.filter(ev => { if (ev.t <= now) { if (now - ev.t < 2) spawnParticles(ev); return false; } return ev.t - now < 120; });
@@ -759,6 +1259,7 @@ function draw(ts) {
gr.addColorStop(0, `hsla(${p.h},80%,65%,${a})`); gr.addColorStop(1, "hsla(0,0%,0%,0)");
cx.fillStyle = gr; cx.beginPath(); cx.arc(p.x, p.y, p.r, 0, 6.3); cx.fill();
}
+ if (S.flash > .02) { cx.fillStyle = `rgba(190,205,255,${S.flash * .28})`; cx.fillRect(0, 0, W, H); S.flash *= .9; }
// Spektrumlinie
cx.lineWidth = 1.6 * dpr; const N = 90;
for (let layer = 0; layer < 2; layer++) {
diff --git a/static/js/sw.js b/static/js/sw.js
index 611a144..b341106 100644
--- a/static/js/sw.js
+++ b/static/js/sw.js
@@ -2,7 +2,7 @@
* diora service worker — caches the app shell for offline use.
*/
-const CACHE = 'diora-v74';
+const CACHE = 'diora-v75';
const SHELL = [
'/static/css/app.css',
'/static/js/app.js',