const S = storm ? .8 : st.w === "rain" ? .32 : 0; WX.kind = st.w; WX.gust = storm || st.w === "rain" ? 0 : .3 * m; WX.storm = S; WX.rain = w.rain; WX.cloud = w.cloud * (1 - S); WX.sun = storm ? 0 : Math.max(-.3, Math.min(1, w.sun + SEA.sunAdd * (1 - S))); WX.sunset = Math.min(1, w.sunset + SEA.sunsetAdd * (1 - w.cloud)) * (1 - .85 * S); WX.e = live ? env(t) : .25; WX.wind = st.w === "storm" ? 2.2 : st.w === "rain" ? 1.45 : 1; WX.fallR = st.w === "storm" ? 1 : st.w === "rain" ? .72 : 0; WX.fallA = st.w === "storm" ? .8 : st.w === "rain" ? .45 : 0; WX.calm = st.w === "storm" ? .3 : 1; } const scanX = p => -BW + (W + 2 * BW) * p; function maple(x, y, r, rot) { ctx.save(); ctx.translate(x, y); ctx.rotate(rot); for (let k = 0; k < 5; k++) { const a = k * 1.2566 - 1.57; ctx.beginPath(); ctx.ellipse(Math.cos(a) * r * .55, Math.sin(a) * r * .55, r * .58, r * .26, a, 0, 7); ctx.fill(); } ctx.restore(); } function drawSeasonFx(nowS) { const P = SEA.part, wind = WX.wind, span = W + 40; const drift = (i, per, dx) => { const ph = ((nowS / per + hsh(i * 3 + 2)) % 1 + 1) % 1; return { ph, x: ((hsh(i * 3 + 3) * W + ph * dx * wind + Math.sin(nowS * 1.1 + i) * 20) % span + span) % span - 20, y: -20 + ph * (H + 40) }; }; ctx.save(); if (P === "maple") for (let i = 0; i < 16; i++) { const per = 11 + hsh(i * 3 + 1) * 8, q = drift(i, per, 150), r = 3.4 + hsh(i * 5) * 2.6; ctx.fillStyle = css(popH(SEA.leaf[i % 4], PUNCH.acc), .86); maple(q.x, q.y, r, nowS * (.8 + hsh(i * 7)) + i); ctx.fillStyle = css(hx(C.gold), .5 * WX.sunset); ctx.fillRect(Math.round(q.x) - 1, Math.round(q.y) - 1, 2, 2); } else if (P === "petal") for (let i = 0; i < 20; i++) { const q = drift(i, 13 + hsh(i * 3 + 1) * 9, 120), rot = nowS * (1 + hsh(i * 7)) + i; ctx.fillStyle = css(i % 3 ? popH(SEA.leaf[0], PUNCH.acc) : hx(C.washi), .7); ctx.beginPath(); ctx.ellipse(q.x, q.y, 2.8, 1.5, rot, 0, 7); ctx.fill(); } else if (P === "snow") for (let i = 0; i < 70; i++) { const q = drift(i, 16 + hsh(i * 3 + 1) * 12, 50), sz = i % 7 ? 2 : 3; ctx.fillStyle = css(hx(i % 4 ? FROST : C.washi), .3 + .35 * hsh(i * 11)); ctx.fillRect(Math.round(q.x), Math.round(q.y), sz, sz); } else if (P === "firefly") for (let i = 0; i < 14; i++) { const x = 40 + hsh(i * 5 + 1) * (W - 80) + Math.sin(nowS * .4 + i * 2.1) * 24, y = H * .45 + hsh(i * 5 + 2) * H * .5 + Math.cos(nowS * .5 + i) * 16; const b = Math.max(0, Math.sin(nowS * (1.2 + hsh(i * 5 + 3)) + i * 1.7)) * WX.calm; if (b < .05) continue; wash(x, y, 9, C.gold, .25 * b); ctx.fillStyle = css(hx(i % 2 ? C.mossHi : C.gold), .85 * b); ctx.fillRect(Math.round(x) - 1, Math.round(y) - 1, 2, 2); } ctx.restore(); } function drawEmblem(x, y, kind, a) { ctx.save(); ctx.translate(x, y); ctx.globalAlpha = a; ctx.lineCap = "round"; ctx.fillStyle = "rgba(5,6,10,.45)"; ctx.beginPath(); ctx.arc(1, 2.5, 20, 0, 7); ctx.fill(); ctx.fillStyle = kind === "storm" ? C.seal : "rgba(16,18,26,.86)"; ctx.beginPath(); ctx.arc(0, 0, 18.5, 0, 7); ctx.fill(); ctx.strokeStyle = C.brassHi; ctx.lineWidth = 1.4; ctx.beginPath(); ctx.arc(0, 0, 18.5, 0, 7); ctx.stroke(); ctx.strokeStyle = css(hx(C.brass), .6); ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(0, 0, 15.5, 0, 7); ctx.stroke(); const bar = (bx, by, bw, bh, col) => { ctx.fillStyle = col; roundRect(bx, by, bw, bh, bh / 2); }; switch (kind) { case "sun": { const g = ctx.createRadialGradient(-2, -2, 1, 0, 0, 7.5); g.addColorStop(0, "#f8cd7a"); g.addColorStop(1, "#cf5a2e"); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(0, 0, 7.5, 0, 7); ctx.fill(); for (let k = 0; k < 8; k++) { const an = k * .7854; ctx.fillStyle = k % 2 ? C.gold : C.amber; ctx.fillRect(Math.round(Math.cos(an) * 11.5) - 1, Math.round(Math.sin(an) * 11.5) - 1, 2, 2); } break; } case "clouds": bar(-11, -6, 18, 5, C.washi); bar(-5, 1, 17, 5, "#8a90a4"); bar(-13, 7, 12, 3, css(hx(C.washi), .6)); break; case "rain": bar(-11, -9, 22, 5, "#8a90a4"); bar(-6, -4, 13, 3, css(hx(C.washi), .7)); ctx.strokeStyle = C.washi; ctx.lineWidth = 1.3; for (let i = 0; i < 4; i++) { const tx = -7 + i * 5; ctx.beginPath(); ctx.moveTo(tx, 2 + (i % 2) * 2); ctx.lineTo(tx - 2, 8 + (i % 2) * 2); ctx.stroke(); } ctx.fillStyle = C.brassHi; ctx.fillRect(9, 6, 2, 4); break; case "storm": bar(-12, -10, 24, 6, "#1a1e29"); bar(-8, -5, 15, 4, "#3e4558"); ctx.fillStyle = C.gold; ctx.beginPath(); ctx.moveTo(1, -2); ctx.lineTo(-5, 5); ctx.lineTo(-1, 5); ctx.lineTo(-4, 12); ctx.lineTo(4, 3); ctx.lineTo(0, 3); ctx.lineTo(3, -2); ctx.closePath(); ctx.fill(); ctx.strokeStyle = "rgba(22,26,38,.85)"; ctx.lineWidth = 1; ctx.strokeRect(-13.5, -13.5, 27, 27); break; case "sunset": ctx.save(); ctx.beginPath(); ctx.arc(0, 0, 15, 0, 7); ctx.clip(); ctx.fillStyle = C.rose; ctx.fillRect(-15, -15, 30, 9); ctx.fillStyle = css(hx(C.amber), .8); ctx.fillRect(-15, -6, 30, 8); ctx.fillStyle = "#ec8f3c"; ctx.beginPath(); ctx.arc(0, 3, 7, Math.PI, 0); ctx.fill(); ctx.fillStyle = C.gold; for (let i = 0; i < 3; i++) ctx.fillRect(-12 + i * 2, 5 + i * 3, 24 - i * 4, 1.4); ctx.restore(); break; default: ctx.fillStyle = C.washi; ctx.beginPath(); ctx.arc(-1, 0, 8, .6, 5.7); ctx.arc(3, 0, 6, 5.4, .9, true); ctx.closePath(); ctx.fill(); ctx.fillRect(7, -8, 2, 2); ctx.fillRect(9, 5, 1.5, 1.5); } ctx.restore(); } function drawSeasonBadge(x, y, si, a, fresh) { const sea = SEASONS[si]; ctx.save(); ctx.translate(x, y); ctx.globalAlpha = a; ctx.fillStyle = "rgba(5,6,10,.45)"; roundRect(-14, -12, 30, 30, 4); ctx.fillStyle = "rgba(16,18,26,.86)"; roundRect(-15, -15, 30, 30, 4); ctx.strokeStyle = fresh ? C.brassHi : css(hx(C.brass), .55); ctx.lineWidth = fresh ? 1.4 : 1; ctx.strokeRect(-14.5, -14.5, 29, 29); if (sea.key === "spring") { ctx.fillStyle = sea.leaf[0]; for (let k = 0; k < 5; k++) { const an = k * 1.2566 - 1.57; ctx.beginPath(); ctx.arc(Math.cos(an) * 5, Math.sin(an) * 5, 4, 0, 7); ctx.fill(); } ctx.fillStyle = C.brassHi; ctx.beginPath(); ctx.arc(0, 0, 2, 0, 7); ctx.fill(); } else if (sea.key === "summer") { ctx.fillStyle = C.moss; ctx.beginPath(); ctx.ellipse(0, 0, 10, 5.5, -.6, 0, 7); ctx.fill(); ctx.strokeStyle = C.mossHi; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(-7, 5); ctx.lineTo(7, -5); ctx.stroke(); } else if (sea.key === "autumn") { ctx.fillStyle = C.amber; maple(0, 0, 10, 0); ctx.fillStyle = C.sealHi; maple(0, 0, 5, .6); ctx.strokeStyle = C.bark; ctx.lineWidth = 1.2; ctx.beginPath(); ctx.moveTo(0, 2); ctx.lineTo(2, 10); ctx.stroke(); } else { ctx.strokeStyle = FROST; ctx.lineWidth = 1.3; ctx.lineCap = "round"; for (let k = 0; k < 3; k++) { const an = k * 1.0472 + .5236, c = Math.cos(an) * 10, s2 = Math.sin(an) * 10; ctx.beginPath(); ctx.moveTo(-c, -s2); ctx.lineTo(c, s2); ctx.stroke(); } ctx.fillStyle = FROST; for (let k = 0; k < 6; k++) { const an = k * 1.0472 + .5236; ctx.fillRect(Math.round(Math.cos(an) * 10) - 1, Math.round(Math.sin(an) * 10) - 1, 2, 2); } } ctx.restore(); } function compositeOld(sc) { const sx = scanX(sc.p), x1 = sx + BW / 2, x0 = sx - BW / 2, BX = 8, BY = 4; ctx.save(); ctx.beginPath(); if (x1 < W) ctx.rect(Math.max(0, x1), 0, W - Math.max(0, x1) + 1, H); for (let bx = Math.floor(x0 / BX) * BX; bx < x1; bx += BX) { if (bx + BX <= 0 || bx >= W) continue; const q = (x1 - (bx + BX / 2)) / BW, ci = Math.round(bx / BX); for (let by = 0; by < H; by += BY) if (hsh(ci * 7919 + (by / BY) * 131 + sc.id * 104729) > q) ctx.rect(bx, by, BX, BY); } ctx.clip(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.drawImage(off, 0, 0); ctx.restore(); } function drawWindBand(sc, nowS) { const sx = scanX(sc.p), x0 = sx - BW / 2, x1 = sx + BW / 2, to = sc.st, from = sc.from, sea = SEASONS[to.s]; const fade = sstep(0, .05, sc.p) * (1 - sstep(.95, 1, sc.p)); ctx.save(); const g = ctx.createLinearGradient(x0 - 40, 0, x1 + 6, 0); g.addColorStop(0, css(hx(C.brass), 0)); g.addColorStop(.55, css(hx(sea.mist), .1 * fade)); g.addColorStop(.92, css(hx(C.washi), .13 * fade)); g.addColorStop(1, css(hx(C.washi), 0)); ctx.fillStyle = g; ctx.fillRect(x0 - 40, 0, BW + 46, H); ctx.fillStyle = css(hx(C.inkDeep), .16 * fade); for (let y = 0; y < H; y += 3) ctx.fillRect(x0, y, BW, 1); ctx.fillStyle = css(hx(C.washi), .05 * fade); for (let y = 1; y < H; y += 6) ctx.fillRect(x0 + BW * .3, y, BW * .7, 1); for (let i = 0; i < 28; i++) { const y = 8 + hsh(i * 7 + 3) * (H - 16) + Math.sin(nowS * 2 + i) * 3, len = 30 + hsh(i * 7 + 5) * 120, x = x1 - hsh(i * 7 + 9) * BW * .8; const col = i % 4 === 0 ? C.brassHi : i % 4 === 1 ? sea.mist : i % 4 === 2 ? C.brass : C.washi; ctx.strokeStyle = css(hx(col), (.16 + .3 * hsh(i * 7 + 11)) * fade); ctx.lineWidth = i % 5 ? 1 : 1.6; ctx.beginPath(); ctx.moveTo(x - len, y); ctx.lineTo(x, y); ctx.stroke(); } ctx.strokeStyle = css(hx("#c9aa78"), .5 * fade); ctx.lineWidth = 1.2; for (let i = 0; i < 5; i++) { const y = 110 + i * 92 + Math.sin(nowS * 1.6 + i) * 10, x = x1 - 14 - (i % 3) * 16; ctx.beginPath(); ctx.moveTo(x - 36, y); ctx.lineTo(x - 6, y); ctx.arc(x - 6, y - 6, 6, Math.PI / 2, -Math.PI * .9, true); ctx.stroke(); } ctx.fillStyle = css(hx(C.brassHi), .6 * fade); ctx.fillRect(Math.round(x1), 0, 1, H); for (let y = (nowS * 90) % 12; y < H; y += 12) ctx.fillRect(Math.round(x1) - 3, Math.round(y), 3, 2); ctx.fillStyle = css(hx(C.mossHi), .22 * fade); ctx.fillRect(Math.round(x0), 0, 1, H); const ey = 46 + 5 * Math.sin(nowS * 2.2), by = H - 46 + 4 * Math.sin(nowS * 1.9 + 1); ctx.strokeStyle = css(hx(C.washi), .4 * fade); ctx.lineWidth = 1; for (let k = -1; k <= 1; k++) { ctx.beginPath(); ctx.moveTo(sx - 24, ey + k * 7); ctx.lineTo(sx - 24 - 40 - 14 * (k & 1), ey + k * 7 + Math.sin(nowS * 3 + k) * 2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(sx - 22, by + k * 7); ctx.lineTo(sx - 22 - 30 - 10 * (k & 1), by + k * 7); ctx.stroke(); } drawEmblem(sx, ey, to.w, fade); drawSeasonBadge(sx, by, to.s, fade, from.s !== to.s); ctx.restore(); } function drawWeatherOverlay(nowS, plateIn) { const S = WX.storm, U = WX.sunset; ctx.save(); const v = ctx.createRadialGradient(CX, CY, PLATE_R * .9, CX, CY, W * .74); v.addColorStop(0, "rgba(5,6,10,0)"); v.addColorStop(1, css(hx(C.inkDeep), .45 - .17 * dayLift() + .3 * S)); ctx.fillStyle = v; ctx.fillRect(0, 0, W, H); ctx.globalCompositeOperation = "screen"; const sv = sunVis(), spW = Math.max(0, WX.sun), fall = SEA.key === "autumn"; const sR = PLATE_R + 30 + 12 * U + 8 * spW; wash((CX + SUN0.x) / 2, (CY + SUN0.y) / 2, sR, C.amber, (.12 + .16 * U + (fall ? .12 * spW : 0)) * plateIn * (1 - .4 * S) * sv); if (sv < 1 && WX.kind !== "storm") wash(SUN.x, SUN.y, PLATE_R * .75, C.washi, (WX.kind === "clear" ? .07 : .05) * plateIn); if (!fall && spW > .01) wash(CX, CY, sR + 6, C.washi, .08 * spW * plateIn * sv); if (U > .01) { ctx.globalCompositeOperation = "soft-light"; const g = ctx.createRadialGradient(CX, CY, PLATE_R * .5, CX, CY, INNER_R + 50); const uW = U * (fall ? 1 : .5 + .5 * sstep(.3, .9, U)); g.addColorStop(0, css(hx(C.gold), .6 * uW)); g.addColorStop(.55, css(hx(C.amber), .45 * uW)); g.addColorStop(1, css(hx(C.rose), 0)); ctx.fillStyle = g; ctx.fillRect(0, 0, W, H); } if (S > .01) { ctx.globalCompositeOperation = "multiply"; ctx.fillStyle = css(lerpC([255, 255, 255], hx("#3c4359"), .75 * S)); ctx.fillRect(0, 0, W, H); ctx.globalCompositeOperation = "source-over"; if (WX.kind === "storm") drawStormHalos(nowS * 1000); } if (WX.rain > .01) { const R = WX.rain * WX.fallR, N = Math.floor(90 * R), A = WX.fallA * WX.rain; for (let i = 0; i < N; i++) { const sp = (380 + hsh(i * 3 + 1) * 260) * (SEA.frost ? .3 : 1), len = 12 + hsh(i * 3 + 2) * 16, x0 = hsh(i * 3 + 7) * (W + 80); const y = ((hsh(i * 5 + 11) * (H + 60) + nowS * sp) % (H + 60)) - 30, x = x0 - (y + 30) * .28; if (SEA.frost) { ctx.fillStyle = css(hx(i % 3 ? FROST : C.washi), (.25 + .3 * hsh(i)) * A); ctx.fillRect(Math.round(x), Math.round(y), 2, 2); continue; } if (i % 9 === 0) { ctx.fillStyle = css(hx(C.brassHi), .35 * A); ctx.fillRect(Math.round(x), Math.round(y), 2, 7); continue; } ctx.strokeStyle = css(hx(C.washi), (.12 + .14 * hsh(i)) * A); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x - len * .28, y + len); ctx.stroke(); } } if (S > .01) { ctx.strokeStyle = css(hx(C.brassHi), .7 * S * plateIn); ctx.lineWidth = 1.2; ctx.beginPath(); ctx.arc(CX, CY, PLATE_R + 6, 0, 7); ctx.stroke(); for (const p of plates) if (p.alive > .5) { ctx.strokeStyle = css(hx(p.ring ? C.brassHi : C.gold), .6 * S * p.alive * plateIn); ctx.beginPath(); ctx.arc(p.x, p.y, p.r * p.alive + 2.5, 0, 7); ctx.stroke(); } ctx.globalCompositeOperation = "screen"; const sv2 = sunVis(); wash(SUN.x, SUN.y, SUN.r * 1.6, C.amber, .35 * S * plateIn * sv2); wash(SUN.x, SUN.y, SUN.r * .8, C.sealHi, .3 * S * plateIn * sv2); } ctx.globalCompositeOperation = "source-over"; drawGlassSweeps(nowS * 1000); if (WX.flash > .01) { ctx.globalCompositeOperation = "screen"; ctx.fillStyle = css(hx(C.washi), .16 * WX.flash); ctx.fillRect(0, 0, W, H); } ctx.restore(); } const GLITCH = ["#b08d57", "#dcb46c", "#a9584a", "#c9634e", "#7f9a5c", "#a9bd88", "#d8cfb6", "#e0893a"]; function drawGlitch(nowS) { const song = songPlaying(), clock = song ? songT() : nowS, SL = .2; const slot = Math.floor(clock / SL), u = clock / SL - slot; const base = AN ? (AN.F * 31 + AN.beats * 131) | 0 : 977; const h = i => hsh(slot * 7919 + base * 13 + i * 104729); const p = song ? .008 + .012 * WX.e : .004, uEnd = .3 + .35 * h(1); if (h(0) >= p || u > uEnd) return; const env = Math.sin(Math.PI * Math.min(1, u / uEnd)); const col = k => hx(GLITCH[Math.floor(h(20 + k) * GLITCH.length)]), amp = .5 + .5 * h(3), kind = 3; ctx.save(); if (kind === 0) { const outer = h(4) < .6, r1 = (outer ? OUTER_R - OUTER_P : INNER_R - INNER_P) - 5, r2 = (outer ? OUTER_R + OUTER_P : INNER_R + INNER_P) + 5; const a0 = h(5) * 6.2832, a1 = a0 + .3 + .5 * h(6), d = (h(7) < .5 ? -1 : 1) * (.025 + .04 * amp); ctx.beginPath(); ctx.arc(CX, CY, r2, a0, a1); ctx.arc(CX, CY, r1, a1, a0, true); ctx.closePath(); ctx.clip(); ctx.translate(CX, CY); ctx.rotate(d); ctx.translate(-CX, -CY); ctx.drawImage(cv, 0, 0, W, H); ctx.setTransform(K, 0, 0, K, 0, 0); ctx.globalCompositeOperation = "soft-light"; ctx.fillStyle = css(col(0), .55 * amp); ctx.fillRect(0, 0, W, H); ctx.globalCompositeOperation = "source-over"; for (let j = 0; j <= 10; j++) { const an = a0 + (a1 - a0) * j / 10, rr = r1 + 3 + (j % 3) * (r2 - r1 - 6) / 2; ctx.fillStyle = css(col(1 + (j % 2)), .7); ctx.fillRect(Math.round(CX + Math.cos(an) * rr) - 1.5, Math.round(CY + Math.sin(an) * rr) - 1.5, 3, 3); } ctx.strokeStyle = css(col(3), .5); ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(CX, CY, r1 + .5, a0, a1); ctx.stroke(); ctx.beginPath(); ctx.arc(CX, CY, r2 - .5, a0, a1); ctx.stroke(); } else if (kind === 1) { const top = h(4) < .5, y = top ? 6 + h(5) * 70 : H - 80 + h(5) * 70, hh = 5 + h(6) * 12, dx = (h(7) < .5 ? -1 : 1) * (5 + 14 * amp); ctx.drawImage(cv, 0, y * K, cv.width, hh * K, dx, y, W, hh); ctx.fillStyle = css(col(0), .2 * amp); ctx.fillRect(0, y, W, hh); for (let k = 0; k < 14; k++) { const bx = h(40 + k) * W, bw = 3 * (1 + Math.floor(h(60 + k) * 5)); ctx.fillStyle = css(col(1 + (k % 3)), .6); ctx.fillRect(Math.round(bx), Math.round(y + (k % 2) * (hh - 3)), bw, 3); } } else if (kind === 2) { const y = 80 + h(4) * 440, hh = 2 + h(5) * 5, x0 = h(6) * (W - 120), w = 80 + h(7) * 160, dx = (h(8) < .5 ? -1 : 1) * (4 + 10 * amp); ctx.drawImage(cv, x0 * K, y * K, w * K, hh * K, x0 + dx, y, w, hh); ctx.fillStyle = css(col(0), .55); ctx.fillRect(Math.round(x0 + dx), Math.round(y + hh), Math.round(w * .6), 1); ctx.fillStyle = css(col(1), .35); ctx.fillRect(Math.round(x0), Math.round(y - 1), Math.round(w * .3), 1); } else { const pl = plates[Math.floor(h(4) * plates.length)]; if (!pl || pl.alive < .5) { ctx.restore(); return; } const R = pl.r * pl.alive, cs = 4; ctx.beginPath(); ctx.arc(pl.x, pl.y, Math.max(0, R - 1), 0, 6.2832); ctx.clip(); for (let gx = -R - cs; gx < R; gx += cs) for (let gy = -R - cs; gy < R; gy += cs) { const mx = gx + cs / 2, my = gy + cs / 2; if (mx * mx + my * my > (R + cs) * (R + cs)) continue; const q = hsh(slot * 31 + base + (gx + 40) * 977 + (gy + 40) * 131); if (q < .45) continue; ctx.fillStyle = css(col(Math.floor(q * 3)), (.12 + .18 * amp * q) * env); ctx.fillRect(Math.round(pl.x + gx), Math.round(pl.y + gy), cs, cs); } } ctx.restore(); } const HOP_S = .01, SEQ_BEATS = 32, SEQ_MIN_S = 14, SEQ_MAX_S = 40, GROW_BEATS = 6, BREEZE_PX = 240; let SONG = null, AN = null; function analyze(buf) { const sr = buf.sampleRate, ch = buf.numberOfChannels, Lc = buf.getChannelData(0), Rc = buf.getChannelData(ch - 1), n = Lc.length; const hop = Math.max(1, Math.round(sr * HOP_S)), F = Math.ceil(n / hop); const E = new Float32Array(F), Lo = new Float32Array(F), Hi = new Float32Array(F), Bal = new Float32Array(F); const a1 = 1 - Math.exp(-2 * Math.PI * 160 / sr), a2 = 1 - Math.exp(-2 * Math.PI * 2600 / sr); let l1 = 0, l2 = 0; for (let f = 0; f < F; f++) { let se = 0, sl = 0, sh = 0, sL = 0, sR = 0; const end = Math.min(n, (f + 1) * hop); for (let i = f * hop; i < end; i++) { const x = .5 * (Lc[i] + Rc[i]); l1 += a1 * (x - l1); l2 += a2 * (x - l2); const h = x - l2; se += x * x; sl += l1 * l1; sh += h * h; sL += Lc[i] * Lc[i]; sR += Rc[i] * Rc[i]; } const m = Math.max(1, end - f * hop); E[f] = Math.sqrt(se / m); Lo[f] = Math.sqrt(sl / m); Hi[f] = Math.sqrt(sh / m); { const qL = Math.sqrt(sL), qR = Math.sqrt(sR); Bal[f] = (qR - qL) / (qL + qR + 1e-9); } } const norm = a => { const s = Array.from(a).filter(v => v > 1e-5).sort((x, y) => x - y); const ref = s.length ? s[Math.floor(s.length * .95)] : 1; for (let i = 0; i < a.length; i++) a[i] = Math.min(1, a[i] / (ref || 1)); }; norm(E); norm(Lo); norm(Hi); const smooth = (a, up, down) => { const o = new Float32Array(a.length); let v = 0; for (let i = 0; i < a.length; i++) { v += (a[i] - v) * (a[i] > v ? up : down); o[i] = v; } return o; }; const Es = smooth(E, .5, .05), Ls = smooth(Lo, .5, .06), Hs = smooth(Hi, .6, .1); { let v = 0; for (let i = 0; i < F; i++) { v += (Bal[i] - v) * .03; Bal[i] = v; } } function onsets(a, gap) { const P = new Float64Array(a.length + 1); for (let i = 0; i < a.length; i++) P[i + 1] = P[i] + a[i]; const flux = new Float32Array(a.length); for (let i = 1; i < a.length; i++) { const j = Math.max(0, i - 8); flux[i] = Math.max(0, a[i] - (P[i] - P[j]) / (i - j)); } const Q = new Float64Array(a.length + 1); for (let i = 0; i < a.length; i++) Q[i + 1] = Q[i] + flux[i]; const out = []; let last = -1e9; for (let i = 3; i < a.length - 3; i++) { const lo = Math.max(0, i - 40), hi = Math.min(a.length, i + 40), th = 1.5 * (Q[hi] - Q[lo]) / (hi - lo) + .035; const v = flux[i]; if (v > th && v >= flux[i - 1] && v >= flux[i + 1] && v >= flux[i - 2] && v >= flux[i + 2] && i - last >= gap) { out.push({ t: i * HOP_S, s: Math.min(1, v * 3), f: i }); last = i; } } return out; } const lo = onsets(Lo, 14).map(o => Object.assign(o, { band: "lo" })); const hi = onsets(Hi, 10).map(o => Object.assign(o, { band: "hi" })); const beats = onsets(E, 25); const events = lo.concat(hi).sort((a, b) => a.t - b.t || (a.band < b.band ? 1 : -1)); const dur = buf.duration, switches = [], grows = []; let lastSw = 0, count = 0; for (const b of beats) { while (b.t - lastSw > SEQ_MAX_S && lastSw + SEQ_MAX_S < dur - 4) { lastSw += SEQ_MAX_S; switches.push(lastSw); count = 0; } count++; if (count % GROW_BEATS === 0) grows.push(b.t); if (count >= SEQ_BEATS && b.t - lastSw >= SEQ_MIN_S && b.t < dur - 4) { switches.push(b.t); lastSw = b.t; count = 0; } } while (dur - lastSw > SEQ_MAX_S + 4) { lastSw += SEQ_MAX_S; switches.push(lastSw); } const Wc = new Float32Array(F); let w = 0; for (let f = 0; f < F; f++) { w += (.3 + .95 * Es[f]) * HOP_S; Wc[f] = w; } const Ss = smooth(E, .004, .003), Sl = smooth(Lo, .004, .003), St = new Float32Array(F), mx = new Float32Array(F); for (let f = 0; f < F; f++) mx[f] = .7 * Ss[f] + .3 * Sl[f]; const so = Array.from(mx).sort((a, b) => a - b), q1 = so[Math.floor(F * .72)] || 0, q2 = Math.max(q1 + .04, so[Math.min(F - 1, Math.floor(F * .93))] || 1); for (let f = 0; f < F; f++) St[f] = sstep(q1, q2, mx[f]) * sstep(.18, .38, mx[f]); return { F, Es, Ls, Hs, St, Bal, events, beats: beats.length, switches, grows, Wc, dur }; } const at = (a, t) => { if (!AN) return 0; const i = Math.max(0, Math.min(AN.F - 1, Math.floor(t / HOP_S))); return a[i]; }; const env = t => at(AN.Es, t), lowE = t => at(AN.Ls, t); const countLE = (arr, t) => { let lo = 0, hi = arr.length; while (lo < hi) { const m = (lo + hi) >> 1; if (arr[m] <= t) lo = m + 1; else hi = m; } return lo; }; function breezeAt(t) { if (!AN) return -60; const f = Math.max(0, Math.min(AN.F - 1, t / HOP_S)), i = Math.floor(f), j = Math.min(AN.F - 1, i + 1); const w = AN.Wc[i] + (AN.Wc[j] - AN.Wc[i]) * (f - i); return (w * BREEZE_PX) % (W + 120) - 60; } function songT() { return SONG && SONG.aud ? SONG.aud.currentTime : 0; } function songPlaying() { return !!(SONG && SONG.aud && !SONG.aud.paused); } let lastSongT = -1, evI = 0, growI = 0, loN = 0, hiN = 0, sweepN = 0, breezeX = -60, modelT0 = 0; function resync(t) { evI = AN ? countLE(AN.events.map(e => e.t), t) : 0; const mi = AN ? countLE(AN.switches, t) % MODELS.length : 0; modelIdx = mi; plantModel(); updateModelLabel(); modelT0 = AN && countLE(AN.switches, t) ? AN.switches[countLE(AN.switches, t) - 1] : 0; growI = AN ? countLE(AN.grows, modelT0) : 0; while (AN && growI < AN.grows.length && AN.grows[growI] <= t) { growStep(true); growI++; } computeThick(); breezeX = breezeAt(t); lastSongT = t; flashT = -99; } function onBeat(o) { if (o.band === "lo") EDGE.kick = Math.max(EDGE.kick, o.s); else EDGE.hk = Math.max(EDGE.hk, o.s); if (o.band === "lo") { const p = plates.filter(q => q.ring === 0)[loN++ % 5]; if (p.alive > .9) { p.glow = Math.max(p.glow, .45 + .55 * o.s); p.pulse = Math.max(p.pulse, .5 + .5 * o.s); p.rip = 0; if (o.s > .35) ripples.push({ x: p.x, y: p.y, r: p.r, a: .35 + .3 * o.s }); } glassKick(p, o.s); { const G = WX.storm + WX.gust; if (G > .15) for (const n of leaves()) kick(n, 14 * G * o.s, -4 * o.s, .25 * G); } } else { const p = plates.filter(q => q.ring === 1)[(hiN++ * 5) % 12]; if (p.alive > .9) { p.glow = Math.max(p.glow, .35 + .6 * o.s); p.pulse = Math.max(p.pulse, .5 + .5 * o.s); p.rip = 0; } glassKick(p, o.s); const L = leaves(); if (L.length) { const n = L[(hiN * 7) % L.length]; n.glow = Math.max(n.glow, .5 + .5 * o.s); kick(n, 0, -18 * o.s, .5 + .6 * o.s); for (const m of L) if (m !== n && Math.hypot(m.x - n.x, m.y - n.y) < 30) kick(m, 0, -6 * o.s, .3 * o.s); } } } function followSong(dt) { if (!SONG || !AN) return; const t = songT(); if (t < lastSongT - .05 || lastSongT < 0) resync(t); while (evI < AN.events.length && AN.events[evI].t <= t) { onBeat(AN.events[evI]); evI++; } const sw = countLE(AN.switches, t), mi = sw % MODELS.length; if (mi !== modelIdx) { modelIdx = mi; modelT0 = sw ? AN.switches[sw - 1] : 0; plantModel(); updateModelLabel(); plates.forEach(p => { if (p.alive < .5) p.regrow = true; }); ripples.push({ x: CX, y: CY, r: PLATE_R, a: .35 }); growI = countLE(AN.grows, t); if (songPlaying()) setStat(`New model: ${model().name} in a ${pot().name}.`); } while (growI < AN.grows.length && AN.grows[growI] <= t) { growStep(false); computeThick(); growI++; } const prev = breezeX, bx = breezeAt(t), e = env(t); const spans = bx >= prev ? [[prev, bx]] : [[prev, W + 60], [-60, bx]]; if (bx < prev) { sweepN++; const dead = plates.find(p => p.alive < .5 && !p.regrow); if (dead) dead.regrow = true; } const hit = x => spans.some(([a, b]) => x > a && x <= b); if (t > lastSongT) { for (const p of plates) if (p.alive > .9 && hit(p.x)) p.glow = Math.max(p.glow, .2 + .55 * e); for (const n of leaves()) if (hit(n.x)) { n.glow = Math.max(n.glow, .25 + .6 * e); kick(n, 18 + 42 * e, -5, .2 + .4 * e); } } breezeX = bx; lastSongT = t; } function modelAlpha(t) { if (!AN) return 1; const k = countLE(AN.switches, t), next = AN.switches[k], prevS = k ? AN.switches[k - 1] : null; let a = 1; if (next != null && next - t < .5) a = Math.min(a, Math.max(0, (next - t) / .5)); if (prevS != null && t - prevS < .8) a = Math.min(a, ease((t - prevS) / .8)); return a; } const MIST_OFF = { d: 0, rise: 0 }; function mistAt(t) { if (!AN) return MIST_OFF; const k = countLE(AN.switches, t), next = AN.switches[k], prevS = k ? AN.switches[k - 1] : null; let d = 0, rise = 0; if (next != null && next - t < MIST.lead) { const x = clamp01(1 - (next - t) / MIST.lead); d = Math.pow(x, MIST.pow); rise = sstep(0, 1, Math.min(1, x * 1.15)); } if (prevS != null && t - prevS < MIST.clear) { const x = clamp01((t - prevS) / MIST.clear), di = 1 - sstep(0, 1, x); if (di > d) { d = di; rise = 1 - .35 * sstep(.2, 1, x); } } return { d, rise }; } const EDGE = { lo: 0, hi: 0, e: 0, bal: 0, kick: 0, hk: 0, pk: 0, on: 0 }; const EDGE_COL = { veil: hx("#5d78ad"), core: hx("#8fa6d6"), tick: hx("#b9c8e6") }; const EDGE_N = 20, EDGE_STEP = 10; const EDGE_SOFT = { veil: .68, tick: .78 }; function edgeStep(dt, on, t) { const k = Math.min(1, dt / 110), kp = Math.pow(.9, dt / 16); EDGE.on += ((on ? 1 : 0) - EDGE.on) * Math.min(1, dt / 400); EDGE.lo += ((on ? lowE(t) : 0) - EDGE.lo) * k; EDGE.hi += ((on ? at(AN.Hs, t) : 0) - EDGE.hi) * Math.min(1, dt / 60); EDGE.e += ((on ? env(t) : 0) - EDGE.e) * k; EDGE.bal += ((on && AN.Bal ? at(AN.Bal, t) : 0) - EDGE.bal) * k; EDGE.kick *= kp; EDGE.hk *= Math.pow(.86, dt / 16); EDGE.pk = Math.max(EDGE.hi, EDGE.pk - dt / 1000 * .45); } function drawEdgeLight() { if (EDGE.on < .01) return; ctx.save(); ctx.setTransform(K, 0, 0, K, 0, 0); for (const s of [-1, 1]) { const x0 = s < 0 ? 0 : W, lean = Math.max(.35, Math.min(1.35, 1 + .9 * s * EDGE.bal)) * EDGE.on; const a = (.07 + .26 * EDGE.lo + .18 * EDGE.kick) * lean * EDGE_SOFT.veil, r = 58 + 30 * EDGE.e + 16 * EDGE.kick; ctx.save(); ctx.translate(x0, CY); ctx.scale(1, 3.3 + .6 * EDGE.e); const g = ctx.createRadialGradient(0, 0, 0, 0, 0, r); g.addColorStop(0, css(EDGE_COL.core, a)); g.addColorStop(.35, css(EDGE_COL.veil, a * .55)); g.addColorStop(1, css(EDGE_COL.veil, 0)); ctx.fillStyle = g; ctx.fillRect(-r, -r, 2 * r, 2 * r); ctx.restore(); const xt = s < 0 ? 4 : W - 8, n = EDGE.hi * EDGE_N, pk = Math.min(EDGE_N - 1, Math.round(EDGE.pk * EDGE_N)); for (let i = 0; i < EDGE_N; i++) { const f = clamp01(n - i); if (f <= 0 && i !== pk) continue; const ta = (i === pk && f <= 0 ? .32 : (.22 + .3 * f) * (1 - .55 * i / EDGE_N)) * (.75 + .35 * EDGE.hk) * lean * EDGE_SOFT.tick; ctx.fillStyle = css(EDGE_COL.tick, ta); const w = i % 5 === 4 ? 4 : 3, xx = s < 0 ? xt : xt + 4 - w; ctx.fillRect(xx, Math.round(CY - 5 - i * EDGE_STEP), w, 2); ctx.fillRect(xx, Math.round(CY + 3 + i * EDGE_STEP), w, 2); } } ctx.restore(); } const FILM = { on: true, k: 1.0, tint: .30, grain: .60, dust: .50, scr: .40, flick: .50, weave: .15, vig: .55, mask: "circle", msize: .57, feather: 18, invert: true, centerWhisper: .15 }; const fh = n => { const x = Math.sin(n * 12.9898 + 78.233) * 43758.5453; return x - Math.floor(x); }; const FSC = {}; function filmScratch(key, w, h) { let c = FSC[key]; if (!c) c = FSC[key] = document.createElement("canvas"); if (c.width !== w || c.height !== h) { c.width = w; c.height = h; } const x = c.getContext("2d"); x.setTransform(1, 0, 0, 1, 0, 0); x.globalAlpha = 1; x.globalCompositeOperation = "source-over"; x.filter = "none"; x.clearRect(0, 0, w, h); return c; } let FGT = null; function filmGrainTiles() { if (FGT) return FGT; let sd = 0x2545f491; const rn = () => { sd |= 0; sd = sd + 0x6D2B79F5 | 0; let t = Math.imul(sd ^ sd >>> 15, 1 | sd); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; const gau = () => (rn() + rn() + rn() + rn() - 2) * .866; FGT = []; for (let i = 0; i < 8; i++) { const c = document.createElement("canvas"); c.width = c.height = 256; const x = c.getContext("2d"); const im = x.createImageData(256, 256), d = im.data; for (let q = 0; q < d.length; q += 4) { const v = 128 + gau() * 60; d[q] = d[q + 1] = d[q + 2] = v; d[q + 3] = 255; } x.putImageData(im, 0, 0); FGT.push(c); } return FGT; } function filmGrain(g, w, h, tt, amt) { if (amt <= 0) return; const T = filmGrainTiles(), fr = Math.floor(tt * 24), tile = T[((fr % 8) + 8) % 8]; const sc = Math.max(.5, h / 1080), tw = 256 * sc, ox = -fh(fr * 7.13) * tw, oy = -fh(fr * 3.71) * tw; g.save(); g.globalCompositeOperation = "overlay"; g.globalAlpha = Math.min(1, .5 * amt); g.imageSmoothingEnabled = sc > 1.3; for (let y = oy; y < h; y += tw) for (let x = ox; x < w; x += tw) g.drawImage(tile, x, y, tw, tw); g.restore(); } function filmVignette(g, w, h, size, soft, dark) { if (dark <= 0) return; const r1 = Math.hypot(w, h) / 2, mid = r1 * (.25 + .55 * size), rI = Math.max(1, mid * (1 - .7 * soft)), rO = Math.max(rI + 1, mid * (1 + .6 * soft) + r1 * .08); const gr = g.createRadialGradient(w / 2, h / 2, rI, w / 2, h / 2, rO); gr.addColorStop(0, "rgba(0,0,0,0)"); gr.addColorStop(1, "rgba(0,0,0," + Math.min(.95, dark).toFixed(3) + ")"); g.save(); g.fillStyle = gr; g.fillRect(0, 0, w, h); g.restore(); } function filmCoat(tt) { if (!FILM.on || FILM.k <= 0) return; const w = cv.width, h = cv.height, P = key => Math.min(2, FILM[key] * FILM.k), fr = Math.floor(tt * 24); const A = filmScratch("A", w, h); A.getContext("2d").drawImage(cv, 0, 0); const B = filmScratch("B", w, h), g = B.getContext("2d"); const wv = P("weave"), fl = P("flick"), ti = Math.min(1, P("tint")); const dx = (Math.sin(tt * 2.1) * .6 + Math.sin(tt * 5.3 + 1) * .3 + (fh(fr) - .5) * .9) * w * .004 * wv; const dy = (Math.sin(tt * 1.7 + 2) * .5 + (fh(fr * 1.37) - .5) * 1.3) * h * .006 * wv; const br = 1 + ((fh(fr * 2.91) - .5) * .22 + Math.sin(tt * 9.1) * .04) * fl, z = 1 + .014 * wv; g.fillStyle = "#000"; g.fillRect(0, 0, w, h); g.filter = "sepia(" + ti.toFixed(3) + ") saturate(" + (1 - .45 * ti).toFixed(3) + ") contrast(" + (1 + .14 * ti).toFixed(3) + ") brightness(" + br.toFixed(3) + ")"; g.drawImage(A, (w - w * z) / 2 + dx, (h - h * z) / 2 + dy, w * z, h * z); g.filter = "none"; filmGrain(g, w, h, tt, P("grain") * 1.25); const u = h / 1080; g.save(); const nd = Math.round(P("dust") * 16); for (let j = 0; j < nd; j++) { const hx_ = fh(fr * 31.7 + j * 3.3), hy_ = fh(fr * 17.1 + j * 5.9), hr = fh(fr * 9.9 + j * 1.7); if (hr < .35) continue; g.fillStyle = fh(j * 7.7 + fr) < .72 ? "rgba(250,246,232," + (.45 + .5 * hr).toFixed(2) + ")" : "rgba(10,8,6," + (.4 + .4 * hr).toFixed(2) + ")"; g.beginPath(); g.ellipse(hx_ * w, hy_ * h, (1 + hr * 3.2) * u * 1.6, (1 + fh(j + fr) * 2.4) * u * 1.6, hr * 3, 0, 7); g.fill(); } const hc = Math.floor(fr / 60); if (fr % 60 < 4 && fh(hc * 4.1) < .75 * Math.min(1, P("dust"))) { const x0 = fh(hc * 1.9) * w, y0 = fh(hc * 2.7) * h, L = h * (.08 + .12 * fh(hc * 3.3)); g.strokeStyle = "rgba(12,10,8,.75)"; g.lineWidth = Math.max(1, 1.3 * u); g.beginPath(); g.moveTo(x0, y0); g.bezierCurveTo(x0 + L * .4, y0 + L * .1, x0 - L * .2, y0 + L * .6, x0 + L * .3, y0 + L); g.stroke(); } const ns = Math.round(P("scr") * 3); for (let j = 0; j < ns; j++) { const seg = Math.floor(tt / 2.3 + j * .37); if (fh(seg * 5.1 + j) > .82) continue; const x = (.08 + .84 * fh(seg * 9.3 + j * 2.1)) * w + Math.sin(tt * .9 + j * 2) * w * .012, a = .18 + .32 * fh(fr * 1.1 + j * 4.4); g.fillStyle = fh(seg + j * 11) < .8 ? "rgba(255,252,238," + a.toFixed(2) + ")" : "rgba(0,0,0," + (a * .9).toFixed(2) + ")"; const y0 = fh(seg * 2.2 + j) < .4 ? fh(seg * 6.6 + j) * h * .5 : 0; g.fillRect(x, y0, Math.max(1, 1.4 * u), h - y0); } g.restore(); filmVignette(g, w, h, .55, .7, P("vig") * .85); const kk = w / W, mcx = CX * kk, mcy = CY * kk, R = Math.min(W, H) / 2 * FILM.msize * kk; const fpx = FILM.feather / 100 * R * .9, r0 = Math.max(1, R - fpx * .5); const rIn = Math.max(0, r0 - 2 * fpx), rOut = r0 + 2 * fpx, wsp = FILM.centerWhisper; const mg = g.createRadialGradient(mcx, mcy, rIn, mcx, mcy, rOut); for (let i = 0; i <= 12; i++) { const q = i / 12, e = q * q * (3 - 2 * q), f = FILM.invert ? e : 1 - e; mg.addColorStop(q, "rgba(0,0,0," + (wsp + (1 - wsp) * f).toFixed(4) + ")"); } g.save(); g.globalCompositeOperation = "destination-in"; g.fillStyle = mg; g.fillRect(0, 0, w, h); g.restore(); ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.globalAlpha = 1; ctx.globalCompositeOperation = "source-over"; ctx.filter = "none"; ctx.drawImage(B, 0, 0); ctx.restore(); } let lastT = 0, flashT = -99; function frame(now) { requestAnimationFrame(frame); const dt = Math.min(50, now - (lastT || now)); lastT = now; boardNow = now; const nowS = now / 1000, live = !!(SONG && AN); const sc = sceneNow(live ? songT() : nowS); applyState(sc.st, songT()); { const key = sc.st.s * 10 + Object.keys(WEATHERS).indexOf(sc.st.w); if (key !== skyKey) { skyKey = key; skyNow = sc.st; updateModelLabel(); } } followSong(dt); const t = songT(); WX.flash = 0; if (WX.kind === "storm" && (!live || songPlaying())) { const fc = live ? t : nowS, sl = Math.floor(fc / 2.2), t0 = sl * 2.2 + hsh(sl * 7 + 3) * 1.2; if (hsh(sl * 13 + 1) < .5 && fc >= t0) WX.flash = .8 * WX.storm * Math.exp(-(fc - t0) * 7); } WX.sway = live && t > 0 ? .55 + 2.2 * WX.e + 2.4 * (WX.storm + WX.gust) : .8 + .3 * Math.sin(nowS * .31); const st = dt / 1000; for (const n of nodes) { if (n.grow < 1) n.grow = Math.min(1, n.grow + dt / 900); n.glow *= Math.pow(.94, dt / 16); if (n.kids.length || n.depth < 2) { n.dx = n.dy = n.rot = 0; continue; } leafInit(n); n.svx += (-70 * n.sx - 7 * n.svx) * st; n.svy += (-70 * n.sy - 7 * n.svy) * st; n.sx = Math.max(-10, Math.min(10, n.sx + n.svx * st)); n.sy = Math.max(-10, Math.min(10, n.sy + n.svy * st)); n.fl *= Math.pow(.955, dt / 16); const A = WX.sway * (.7 + .3 * Math.sin(n.ph * 2.3)); n.dx = A * Math.sin(nowS * 1.25 + n.ph + n.y * .02) + A * .35 * Math.sin(nowS * 2.7 + n.ph * 1.7) + n.sx + n.fl * 2.4 * Math.sin(nowS * 21 + n.ph * 3.1); n.dy = A * .35 * Math.sin(nowS * 1.8 + n.ph * .7) + n.sy + n.fl * 1.4 * Math.cos(nowS * 17 + n.ph * 2.3); n.rot = .05 * A * Math.sin(nowS * 1.25 + n.ph) + n.fl * .45 * Math.sin(nowS * 19 + n.ph) + n.sx * .03; } for (const p of plates) { p.glow *= Math.pow(.93, dt / 16); p.pulse *= Math.pow(.9, dt / 16); p.rip = Math.min(1, p.rip + dt / 650); const tgt = live && t > 0 ? (p.ring ? at(AN.Hs, t) : lowE(t)) : .12 + .08 * Math.sin(nowS * .7 + p.i); p.lv += (tgt - p.lv) * Math.min(1, dt / 90); p.ph += (dt / 1000) * (.25 + 1.6 * p.lv) * (p.ring ? 1 : -1); if (p.regrow) { p.alive = Math.min(1, p.alive + dt / 1200); if (p.alive >= 1) p.regrow = false; } } layout(); const plateIn = ease((now - arriveT0) / 700), mA = modelAlpha(t); mistNow = live ? mistAt(t) : MIST_OFF; mistT = live ? t : nowS; switching = mA < 1; drawWorld(now, nowS, plateIn, mA); if (sc.from) { if (off.width !== cv.width || off.height !== cv.height) { off.width = cv.width; off.height = cv.height; } const keepWX = Object.assign({}, WX), keepSEA = SEA, main = ctx; applyState(sc.from, t); ctx = octx; try { drawWorld(now, nowS, plateIn, mA); } finally { ctx = main; Object.assign(WX, keepWX); SEA = keepSEA; } ctx.setTransform(K, 0, 0, K, 0, 0); compositeOld(sc); drawWindBand(sc, nowS); windKick(sc); } else kickId = null; edgeStep(dt, live && songPlaying(), t); drawEdgeLight(); ctx.save(); ctx.imageSmoothingEnabled = false; ctx.globalAlpha = .55; ctx.drawImage(grain, 0, 0, W, H); ctx.restore(); drawGlitch(nowS); drawFx(dt); filmCoat(songPlaying() ? songT() : nowS); } function drawWorld(now, nowS, plateIn, mA) { ctx.setTransform(K, 0, 0, K, 0, 0); drawBackground(now); drawCenterPlate(plateIn, plateIn * mA, mA); drawPlates(now); drawBreeze(now); drawSeasonFx(nowS); drawWeatherOverlay(nowS, plateIn); } let kickId = null, kickX = 0, skyKey = -1, skyNow = null; function windKick(sc) { const x = scanX(sc.p) + BW / 2; if (kickId === sc.id && x > kickX) { for (const n of leaves()) if (n.x > kickX && n.x <= x) { kick(n, 34 + 30 * WX.e, -7, .75); n.glow = Math.max(n.glow, .35); } for (const p of plates) if (p.alive > .9 && p.x > kickX && p.x <= x) p.glow = Math.max(p.glow, .4); } kickId = sc.id; kickX = x; } const stat = document.getElementById("stat"); function setStat(s) { stat.textContent = s; } function updateModelLabel() { document.getElementById("model").textContent = `model ${modelIdx + 1}/${MODELS.length} · ${model().name} · ${pot().name}` + (skyNow ? ` · ${SEASONS[skyNow.s].key} · ${WEATHERS[skyNow.w].label}` : ""); } const playBtn = document.getElementById("play"), pauseBtn = document.getElementById("pause"); function pauseAll(msg) { if (SONG && SONG.aud && SONG.aud.pause) SONG.aud.pause(); if (msg) setStat(msg); } playBtn.onclick = () => { if (!SONG || !SONG.aud) { setStat("LOAD a song first"); return; } audio(); const a = SONG.aud; if (a.ended) a.currentTime = 0; const p = a.play(); if (p && p.catch) p.catch(err => setStat("could not play: " + (err && err.message || err))); setStat(`playing ${SONG.name} · ${AN.beats} beats heard · a new pot + bonzai every ${SEQ_BEATS} beats`); }; pauseBtn.onclick = () => { if (!SONG) { setStat("LOAD a song first"); return; } pauseAll("paused — ▶ Play to go on, or ⏺ RENDER for a clean video"); }; fit(); buildPlates(); plantModel(); updateModelLabel(); arriveT0 = performance.now(); requestAnimationFrame(frame); const RS = CleanRec.settings; const smallDev = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent || "") || (navigator.maxTouchPoints > 1 && /Mac/i.test(navigator.platform || "")); let FPS = smallDev ? RS.fps.phone : RS.fps.computer; const fpsBtn = document.getElementById("fps"); const mbps = () => RS.video_mbps[(smallDev ? "phone" : "computer") + "_" + FPS] || (FPS > 30 ? 16 : 10); const showFps = () => { if (fpsBtn) fpsBtn.textContent = FPS + " FPS"; }; showFps(); const RHEAD = .5, RTAIL = .5, RSIM = 60, renderBtn = document.getElementById("render"); const rbreathe = () => new Promise(r => { const c = new MessageChannel(); c.port1.onmessage = () => { c.port1.close(); r(); }; c.port2.postMessage(0); }); const rmmss = s => { s = Math.max(0, Math.round(s)); return Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0"); }; const recName = () => "tokonoma-render-" + new Date().toISOString().replace(/[:.]/g, "-"); if (typeof RenderChild !== "undefined" && RenderChild) { const C = RenderChild; let stop = false; arriveT0 = -1e9; try { document.getElementById("bar").style.display = "none"; document.body.style.background = "#000"; document.getElementById("wrap").style.padding = "0"; } catch (e) {} fit(); const say = m => { try { window.parent.postMessage(Object.assign({ rnd: 1 }, m), "*"); } catch (e) {} }; window.addEventListener("error", e => say({ kind: "error", msg: String(e.message || e) })); window.addEventListener("message", async e => { const m = e.data || {}; if (e.source !== window.parent || !m.rnd) return; if (m.kind === "stop") { stop = true; return; } if (m.kind !== "go") return; try { const oc = new OfflineAudioContext(2, 1, 48000), buf = await CleanRec.decode(oc, m.bytes); const sr = buf.sampleRate, ch = Math.min(2, buf.numberOfChannels), pcm = [...Array(ch)].map((_, c) => buf.getChannelData(c)), dur = buf.duration; const aud = { get currentTime() { return Math.max(0, Math.min(dur, C.t - RHEAD)); }, set currentTime(v) {}, get ended() { return C.t - RHEAD >= dur; }, get paused() { return C.t < RHEAD || C.t - RHEAD >= dur; }, pause() {}, play() { return Promise.resolve(); } }; AN = analyze(buf); SONG = { name: m.name || "song", dur, url: "", aud }; lastSongT = -1; const fps = m.fps, per = Math.max(1, Math.round(RSIM / fps)), size = m.size || RS.size; const steps = Math.ceil((RHEAD + dur + RTAIL) * RSIM); const k0 = m.whole || !(m.from > 0) ? 0 : Math.round((RHEAD + m.from) * RSIM / per) * per; const kEnd = m.whole ? steps : Math.min(steps, k0 + Math.round((m.len + (k0 ? 0 : RHEAD)) * RSIM)); const enc = document.createElement("canvas"); enc.width = enc.height = size; const ecx = enc.getContext("2d"); const R = await CleanRec.render({ canvas: enc, w: size, h: size, fps, mbps: m.mbps, head: RHEAD - k0 / RSIM, audio: { pcm, sr, ch } }); if (!R) { say({ kind: "error", msg: "this browser cannot render " + size + "² at " + fps + " fps (it needs H.264 + AAC encoders: Chrome or Edge on a computer)" }); return; } ecx.imageSmoothingEnabled = true; ecx.imageSmoothingQuality = "high"; const frames = Math.ceil((kEnd - k0) / per), w0 = Date.now(); let f = 0; for (let k = 0; k < kEnd; k++) { C.t = k / RSIM; for (const fn of [...C.ivs.values()]) fn(); const cbs = C.raf; C.raf = []; for (const cb of cbs) cb(C.t * 1000); if (k >= k0 && k % per === 0) { ecx.drawImage(cv, 0, 0, size, size); if (!await R.frame(f++)) { say({ kind: "error", msg: "the encoder stopped at frame " + f }); return; } } if (k % 30 === 0) { say({ kind: "progress", done: f / frames, t: C.t, total: kEnd / RSIM, el: (Date.now() - w0) / 1000, runup: k < k0, at: k0 / RSIM }); await rbreathe(); if (stop) { R.cancel(); say({ kind: "stopped" }); return; } } } const res = await R.finish(f / fps); if (!res || !res.blob) { say({ kind: "error", msg: "the MP4 could not be closed" + (res && res.err ? ": " + res.err : "") }); return; } say({ kind: "done", blob: res.blob, frames: res.frames, fps: res.fps, aerr: res.aerr, secs: (Date.now() - w0) / 1000 }); } catch (err) { say({ kind: "error", msg: String(err && err.message || err) }); } }); say({ kind: "ready" }); } else { let RJ = null; if (fpsBtn) fpsBtn.onclick = () => { if (RJ) { setStat("the frame rate changes between renders, not during one"); return; } FPS = FPS === 60 ? 30 : 60; showFps(); setStat(FPS === 60 ? "renders at 60 frames a second: as smooth as the screen, about twice the size" : "renders at 30 frames a second: lighter, for a phone or a tablet"); }; const guard = (b, why) => { if (!b) return; const f0 = b.onclick; b.onclick = e => { if (RJ) { setStat(why); return; } if (f0) return f0.call(b, e); }; }; guard(playBtn, "a render is running: Play waits until it is done"); const place = () => { if (!RJ) return; const b = cv.getBoundingClientRect(); Object.assign(RJ.fr.style, { left: b.left + "px", top: b.top + "px", transform: "scale(" + (b.width / 1080) + ")" }); }; window.addEventListener("resize", place); const end = msg => { if (!RJ) return; try { RJ.sleep(); } catch (e) {} window.removeEventListener("message", RJ.on); clearTimeout(RJ.wait); try { RJ.fr.remove(); } catch (e) {} RJ = null; renderBtn.classList.remove("on"); renderBtn.innerHTML = "⏺ RENDER"; if (msg) setStat(msg); }; renderBtn.onclick = async () => { if (RJ) { try { RJ.fr.contentWindow.postMessage({ rnd: 1, kind: "stop" }, "*"); } catch (e) {} end("render stopped · nothing was saved"); return; } if (!SONG || !SONG.url) { setStat("LOAD a song first"); return; } if (typeof CleanRec === "undefined" || !CleanRec.render || !CleanRec.ok()) { setStat("this browser cannot render (it needs WebCodecs: Chrome or Edge on a computer)"); return; } const P = CleanRec.plan ? await CleanRec.plan({ dur: SONG.dur, pos: (SONG.aud && SONG.aud.currentTime) || 0, fps: FPS, head: RHEAD, tail: RTAIL, say: t => setStat(t) }) : { light: false, size: RS.size, mbps: mbps(), from: 0, len: SONG.dur, whole: true, slow: "" }; if (!P) return; const slow = P.slow || ""; pauseAll(""); let bytes = null; try { bytes = await (await fetch(SONG.url)).arrayBuffer(); } catch (e) { setStat("could not read the song again: " + (e && e.message || e)); return; } const fr = document.createElement("iframe"), fps = FPS, name = SONG.name; fr.style.cssText = "position:fixed;left:0;top:0;width:1080px;height:1080px;border:0;transform-origin:0 0;z-index:50;background:#000;pointer-events:none"; fr.src = location.href.replace(/#.*$/, "") + "#render"; RJ = { fr, on: null, wait: 0, sleep: () => {} }; try { if (CleanRec.awake) RJ.sleep = await CleanRec.awake(); } catch (e) {} renderBtn.classList.add("on"); renderBtn.innerHTML = "■ STOP RENDER"; setStat("⏺ RENDER — opening a fresh copy of the board…"); RJ.on = e => { if (!RJ || e.source !== fr.contentWindow) return; const m = e.data || {}; if (!m.rnd) return; if (m.kind === "ready") { clearTimeout(RJ.wait); fr.contentWindow.postMessage({ rnd: 1, kind: "go", bytes, fps, mbps: P.mbps, size: P.size, from: P.from, len: P.len, whole: P.whole, name }, "*", [bytes]); setStat("⏺ RENDER — the copy is listening to the song…"); } else if (m.kind === "progress") setStat(m.runup ? "⏺ RENDER" + (P.light ? " LIGHT" : "") + " — getting to " + rmmss(m.at) + " (nothing filmed yet) · " + rmmss(m.t) + slow : "⏺ RENDER" + (P.light ? " LIGHT" : "") + " · " + Math.round(m.done * 100) + "% · " + rmmss(m.t) + " of " + rmmss(m.total) + " · " + fps + " fps" + (m.done > .02 ? " · about " + rmmss(m.el / m.done * (1 - m.done)) + " left" : "") + slow); else if (m.kind === "done") { const url = URL.createObjectURL(m.blob), a = document.createElement("a"); a.href = url; a.download = recName() + (P.light ? "-light-" + P.size : "") + ".mp4"; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 4000); end("rendered · MP4 (H.264 + AAC) · " + m.fps + " fps · " + m.frames + " frames · " + rmmss(m.frames / m.fps) + " · in " + rmmss(m.secs) + " · nothing skipped" + (m.aerr ? " · ⚠ the sound encoder complained" : "") + " · nothing uploaded"); }