diff --git a/brain.js b/brain.js index 4ae74ef..e0f7df1 100644 --- a/brain.js +++ b/brain.js @@ -165,12 +165,13 @@ const guessText = (spec) => spec && spec.guess != null ? ` · ${Math.round(spec. })(); /* ---------- TASTE: the brain and the map ---------- */ +const cssv = (n) => getComputedStyle(document.documentElement).getPropertyValue(n).trim(); function drawBrain(canvas, net, spec, pulse = 0) { const ctx = canvas.getContext("2d"), W = canvas.width, H = canvas.height; - ctx.clearRect(0, 0, W, H); ctx.fillStyle = "#121216"; ctx.fillRect(0, 0, W, H); + ctx.clearRect(0, 0, W, H); ctx.fillStyle = cssv("--card"); ctx.fillRect(0, 0, W, H); const cols = [W * 0.16, W * 0.5, W * 0.86], ins = ["tempo", "energy", "key·x", "key·y", "minor", "words"]; const yI = (i) => H * (0.12 + 0.76 * i / 5), yH = (i) => H * (0.07 + 0.86 * i / (NH - 1)), yO = H / 2; - if (!net) { ctx.fillStyle = "#8a8a96"; ctx.font = `${W * 0.035}px -apple-system, system-ui, sans-serif`; ctx.textAlign = "center"; ctx.fillText("react to three tracks and the brain wakes up", W / 2, H / 2); return; } + if (!net) { ctx.fillStyle = cssv("--dim"); ctx.font = `${W * 0.035}px -apple-system, system-ui, sans-serif`; ctx.textAlign = "center"; ctx.fillText("react to three tracks and the brain wakes up", W / 2, H / 2); return; } const x = spec ? feats(spec) : new Array(NIN).fill(0), { h, y } = net.forward(x); // the words block is shown as one input: its activation is how much of the song's wording the net recognises let wordsAct = 0; for (let j = NUM; j < NIN; j++) wordsAct += Math.abs(x[j]); wordsAct = Math.min(1, wordsAct / 3); @@ -185,20 +186,20 @@ function drawBrain(canvas, net, spec, pulse = 0) { const glow = Math.abs(a) * (1 + pulse); ctx.beginPath(); ctx.arc(cx, cy, r + 10 * glow, 0, Math.PI * 2); ctx.fillStyle = `hsl(${a >= 0 ? 44 : 220} 80% 65% / ${0.18 * glow})`; ctx.fill(); ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.fillStyle = `hsl(${a >= 0 ? 44 : 220} ${40 + 50 * Math.abs(a)}% ${35 + 35 * Math.abs(a)}%)`; ctx.fill(); - if (label) { ctx.fillStyle = "#8a8a96"; ctx.font = `600 ${W * 0.026}px ui-monospace, Menlo, monospace`; ctx.textAlign = "right"; ctx.fillText(label, cx - r - 8, cy + 4); } + if (label) { ctx.fillStyle = cssv("--dim"); ctx.font = `600 ${W * 0.026}px ui-monospace, Menlo, monospace`; ctx.textAlign = "right"; ctx.fillText(label, cx - r - 8, cy + 4); } }; shown.forEach((v, j) => node(cols[0], yI(j), W * 0.018, v, ins[j])); h.forEach((v, k) => node(cols[1], yH(k), W * 0.017, v)); node(cols[2], yO, W * 0.034, y * 2 - 1); - ctx.fillStyle = "#ededf0"; ctx.font = `600 ${W * 0.03}px ui-monospace, Menlo, monospace`; ctx.textAlign = "left"; ctx.fillText(`${Math.round(y * 100)}%`, cols[2] + W * 0.05, yO + 5); - ctx.fillStyle = "#8a8a96"; ctx.font = `${W * 0.024}px ui-monospace, Menlo, monospace`; ctx.fillText(spec ? "would like this" : "", cols[2] + W * 0.05, yO + 5 + W * 0.03); + ctx.fillStyle = cssv("--ink"); ctx.font = `600 ${W * 0.03}px ui-monospace, Menlo, monospace`; ctx.textAlign = "left"; ctx.fillText(`${Math.round(y * 100)}%`, cols[2] + W * 0.05, yO + 5); + ctx.fillStyle = cssv("--dim"); ctx.font = `${W * 0.024}px ui-monospace, Menlo, monospace`; ctx.fillText(spec ? "would like this" : "", cols[2] + W * 0.05, yO + 5 + W * 0.03); } function drawTaste(rows, title, net) { const c = $("#tasteMap"), ctx = c.getContext("2d"), W = c.width, H = c.height, pad = 70; const X = (bpm) => pad + (Math.min(180, Math.max(60, bpm)) - 60) / 120 * (W - 2 * pad); const Y = (en) => H - pad - Math.min(1, Math.max(0, en)) * (H - 2 * pad); const pts = rows.map((t) => ({ x: X(+t.spec.bpm || 100), y: Y(+t.spec.energy || 0.5), w: reactionWeight(t.spec, t), hue: keyHue(t.spec.key), minor: /m(in|inor)?\b/i.test(t.spec.key || "") && !/maj/i.test(t.spec.key || ""), t })); - ctx.fillStyle = "#121216"; ctx.fillRect(0, 0, W, H); + ctx.fillStyle = cssv("--card"); ctx.fillRect(0, 0, W, H); // the field: the net's prediction, in the station's usual key; lit gold where it says yes, blue where no const keys = {}; for (const t of rows) keys[t.spec.key] = (keys[t.spec.key] || 0) + 1; const key = Object.entries(keys).sort((a, b) => b[1] - a[1])[0]?.[0] || "A minor"; @@ -212,8 +213,8 @@ function drawTaste(rows, title, net) { ctx.fillStyle = v > 0 ? `hsl(44 60% 60% / ${0.5 * Math.abs(v)})` : `hsl(220 30% 45% / ${0.45 * Math.abs(v)})`; ctx.fillRect(gx, gy, cell, cell); } - ctx.strokeStyle = "#232329"; ctx.lineWidth = 2; ctx.strokeRect(pad, pad, W - 2 * pad, H - 2 * pad); - ctx.fillStyle = "#8a8a96"; ctx.font = `600 ${W * 0.018}px ui-monospace, Menlo, monospace`; ctx.textAlign = "center"; + ctx.strokeStyle = cssv("--line"); ctx.lineWidth = 2; ctx.strokeRect(pad, pad, W - 2 * pad, H - 2 * pad); + ctx.fillStyle = cssv("--dim"); ctx.font = `600 ${W * 0.018}px ui-monospace, Menlo, monospace`; ctx.textAlign = "center"; for (const b of [60, 90, 120, 150, 180]) ctx.fillText(b + "", X(b), H - pad + 34); ctx.textAlign = "right"; for (const e of [0, 0.5, 1]) ctx.fillText(e === 0 ? "calm" : e === 1 ? "wild" : "·", pad - 14, Y(e) + 6); // the points, with the listener's emoji over them @@ -227,12 +228,12 @@ function drawTaste(rows, title, net) { const em = reactionEmoji(p.t.spec.reaction); const tg = tagSignals(p.t.spec), tagMark = tg.length ? ` ${tg.filter((s) => s.like).length}▲${tg.filter((s) => !s.like).length}▼` : ""; const tag = [...em].slice(0, 4).join("") + ((p.t.plays || 0) > 1 ? `×${p.t.plays}` : "") + tagMark; - if (tag) { ctx.font = `${W * 0.024}px -apple-system, system-ui, sans-serif`; ctx.fillStyle = "#ededf0"; ctx.fillText(tag, p.x, p.y - r - 8); } + if (tag) { ctx.font = `${W * 0.024}px -apple-system, system-ui, sans-serif`; ctx.fillStyle = cssv("--ink"); ctx.fillText(tag, p.x, p.y - r - 8); } } // the track in the oven, as a hollow mark const nx = state.nextJob && state.nextJob.spec; if (nx && rows.some((t) => t.spec.stationId === nx.stationId || title === "ALL")) { - ctx.beginPath(); ctx.arc(X(+nx.bpm || 100), Y(+nx.energy || 0.5), W * 0.014, 0, Math.PI * 2); ctx.setLineDash([4, 4]); ctx.lineWidth = 2; ctx.strokeStyle = "#ededf0"; ctx.stroke(); ctx.setLineDash([]); + ctx.beginPath(); ctx.arc(X(+nx.bpm || 100), Y(+nx.energy || 0.5), W * 0.014, 0, Math.PI * 2); ctx.setLineDash([4, 4]); ctx.lineWidth = 2; ctx.strokeStyle = cssv("--ink"); ctx.stroke(); ctx.setLineDash([]); } const warm = pts.filter((p) => p.w >= 0.25).length, cold = pts.filter((p) => p.w <= -0.25).length; $("#tasteNote").textContent = !pts.length ? "Nothing heard here yet." :