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();
}