import gcd from "compute-gcd"; import keys from "./lib/keys"; import color from "./lib/color"; import kalimba from "./lib/kalimba"; import life from "./lib/life"; import organ from "./lib/organ"; import { browser, requestAudioContext, choice } from "./lib/util"; let instrument = kalimba; const root = 440; const s = 50; const w = window.innerWidth; const h = window.innerHeight; const ws = Math.ceil(w / s), hs = Math.ceil(h / s); const add_on = 0; const mul_on = 1.0; const add_off = 0.1; const mul_off = 0.9; let dragging = false; let erasing = false; let lastFreq = 0; let notes = []; requestAudioContext(() => { for (var i = 0; i < ws; i++) { notes[i] = []; for (var j = 0; j < hs; j++) { notes[i][j] = add(i, j); } } life.init(notes, assign); }); function play(freq) { if (freq.playing) return; freq.playing = true; instrument.play(freq.frequency); if (instrument === organ || hash || life.isRunning()) { freq.div.classList.add("playing"); } life.assign_item(freq, true); } function pause(freq) { if (!freq.playing) return; freq.playing = false; instrument.pause(freq.frequency); freq.div.classList.remove("playing"); life.assign_item(freq, false); } function assign(freq, state) { if (state) { play(freq); } else { pause(freq); } } function toggle(freq) { assign(freq, !freq.playing); } const gliderShape = [ [0, 0, 0, 0, 0], [0, 1, 1, 1, 0], [0, 0, 0, 1, 0], [0, 0, 1, 0, 0], [0, 0, 0, 0, 0], ]; const gliderShapeFlip = gliderShape.map((a) => a.slice(0).reverse()); const gliderShapes = [ gliderShape, gliderShapeFlip, gliderShape.slice(0).reverse(), gliderShapeFlip.slice(0).reverse(), ]; function glider() { const x = Math.floor(Math.random() * ws); const y = Math.floor(Math.random() * hs); const shape = choice(gliderShapes); weave(x, y, shape); } function weave(x, y, shape) { const xmag = shape.length; const ymag = shape[0].length; let i, j, px, py; for (i = 0; i < xmag; i++) { for (j = 0; j < ymag; j++) { px = (x + i) % ws; py = (y + j) % hs; assign(notes[px][py], shape[i][j]); } } } function forEach(f) { let i, j, note, s; for (i = 0; i < ws; i++) { for (j = 0; j < hs; j++) { note = notes[i][j]; s = f(i, j, note.playing); assign(note, s); } } } function clone() { let i, j; let a = []; for (i = 0; i < ws; i++) { a[i] = []; for (j = 0; j < hs; j++) { a[i][j] = notes[i][j].playing; } } return a; } function move(dx, dy) { let a = clone(); forEach((x, y, state) => { x = (x + dx + ws) % ws; y = (y + dy + hs) % hs; return a[x][y]; }); } function clear() { forEach(() => { return false; }); } function stripex(odd) { odd = !!odd; forEach((x) => { return x % 2 ? odd : !odd; }); } function stripey(odd) { odd = !!odd; forEach((x, y) => { return y % 2 ? odd : !odd; }); } function checker(odd, n) { odd = !!odd; n = n || 1; forEach((x, y) => { return Math.floor(x / n) % 2 ^ Math.floor(y / n) % 2 ? odd : !odd; }); } function noise(n) { n = n || 0.5; n = n * n; forEach(() => { return Math.random() < n; }); } function add(i, j) { const a = i + 1; const b = j + 1; const div = document.createElement("div"); const frequency = (root * a) / b; let add = 0; let frac; div.style.left = i * s + "px"; div.style.top = j * s + "px"; const freq = { frequency, div, i, j, playing: false, recolor: (numerator, denominator) => { let aa = a / numerator; let bb = b / denominator; if (aa < bb) { add = -Math.log(bb / aa) / 3.5; } else { add = Math.log(aa / bb) / 6; } let a_inv = a * denominator; let b_inv = b * numerator; let ba_gcd = gcd(a_inv, b_inv); let a_disp = a_inv / ba_gcd; let b_disp = b_inv / ba_gcd; frac = Math.log2(aa / bb) % 1; let frac_orig = Math.log2(a / b) % 1; if (frac < 0) { frac += 1; } if (frac_orig < 0) { frac += 1; } if (frac_orig === 0) { div.style.fontWeight = "900"; } else { div.style.fontWeight = "500"; } div.innerHTML = `
${a_disp}
/
${b_disp}
`; if (freq.playing) { div.style.backgroundColor = color(frac, add + add_on, mul_on); } else { div.style.backgroundColor = color(frac, add + add_off, mul_off); } }, }; freq.recolor(1, 1); if (browser.isDesktop) { div.addEventListener("mousedown", function (event) { if (event.button === 2) { // rightclick event.preventDefault(); notes.forEach((row) => row.forEach((note) => note.recolor(a, b))); return; } div.style.backgroundColor = color(frac, add + add_on, mul_on); toggle(freq); erasing = !freq.playing; }); div.addEventListener("mouseenter", function () { div.style.backgroundColor = color(frac, add + add_on, mul_on); if (dragging) { if (erasing) { pause(freq); } else { toggle(freq); } } }); div.addEventListener("mouseleave", function () { div.style.backgroundColor = color(frac, add + add_off, mul_off); }); div.addEventListener("contextmenu", function (event) { if (!event.ctrlKey || !event.metaKey || !event.altKey) { event.preventDefault(); } }); } else { div.addEventListener("touchstart", function (e) { e.preventDefault(); toggle(freq); erasing = !freq.playing; lastFreq = freq; }); } document.body.appendChild(div); return freq; } if (browser.isDesktop) { document.addEventListener("mousedown", (event) => { if (event.button !== 2) { dragging = true; } }); document.addEventListener("mouseup", () => { dragging = false; }); } else { document.addEventListener("touchstart", (e) => { e.preventDefault(); dragging = true; }); document.addEventListener("touchmove", (e) => { e.preventDefault(); const x = Math.floor(e.touches[0].pageX / s); const y = Math.floor(e.touches[0].pageY / s); if (!(x in notes) || !(y in notes[x])) return; const freq = notes[x][y]; if (freq !== lastFreq) { if (dragging) { if (erasing) { pause(freq); } else { toggle(freq); } } lastFreq = freq; } }); document.addEventListener("touchend", () => { dragging = false; }); } function swap_instrument() { instrument = instrument === kalimba ? organ : kalimba; } let life_bpm = 50; window.addEventListener("keydown", keydown, true); function keydown(e) { // console.log(e.keyCode) if (e.altKey || e.ctrlKey || e.metaKey) return; switch (e.keyCode) { case 32: // space life.toggle(); break; case 188: // comma life_bpm += e.shiftKey ? 1 : 5; life.setTempo(life_bpm); break; case 190: // period life_bpm -= e.shiftKey ? 1 : 5; life_bpm = Math.max(1, life_bpm); life.setTempo(life_bpm); break; case 37: // left move(1, 0); break; case 38: // up move(0, 1); break; case 39: // right move(-1, 0); break; case 40: // down move(0, -1); break; case 71: // g glider(); break; case 83: // s swap_instrument(); break; case 67: // c clear(); break; case 87: // w clear(); break; case 78: // n noise(0.5); break; case 69: // e stripex(Math.random() < 0.5); break; case 82: // r stripey(Math.random() < 0.5); break; case 84: // t checker(Math.random() < 0.5, 1); break; case 89: // y checker(Math.random() < 0.5, 2); break; case 85: // u checker(Math.random() < 0.5, 3); break; case 73: // i checker(Math.random() < 0.5, 4); break; case 79: // o checker(Math.random() < 0.5, 5); break; case 80: // p checker(Math.random() < 0.5, 6); break; case 219: // [ checker(Math.random() < 0.5, 7); break; case 221: // ] checker(Math.random() < 0.5, 11); break; case 49: // 1 noise(0.1); break; case 50: // 2 noise(0.2); break; case 51: // 3 noise(0.3); break; case 52: // 4 noise(0.4); break; case 53: // 5 noise(0.5); break; case 54: // 6 noise(0.6); break; case 55: // 7 noise(0.7); break; case 56: // 8 noise(0.8); break; case 57: // 9 noise(0.9); break; case 48: // 0 noise(1); break; } } keys.listen(function (index) { // const freq = scales.current().index(index) // document.body.style.backgroundColor = color( index / scales.current().scale.length ) // instrument.toggle(freq) }); let hash = window.location.hash || window.location.search; if (hash.match("sin") || hash.match("organ")) { instrument = organ; } if (hash.match("glider")) { instrument = organ; clear(); glider(); life.setTempo((life_bpm = 120 * 8)); life.toggle(); }