1162 lines
27 KiB
JavaScript
1162 lines
27 KiB
JavaScript
/*
|
|
TODO:
|
|
*/
|
|
|
|
const audioCtx = new (AudioContext || webkitAudioContext)();
|
|
|
|
function mod(n, m) {
|
|
return ((n % m) + m) % m;
|
|
}
|
|
|
|
const WHITE = 0;
|
|
const BLACK = 1;
|
|
|
|
let pianoLayout = [
|
|
WHITE,
|
|
BLACK,
|
|
WHITE,
|
|
BLACK,
|
|
WHITE,
|
|
WHITE,
|
|
BLACK,
|
|
WHITE,
|
|
BLACK,
|
|
WHITE,
|
|
BLACK,
|
|
WHITE
|
|
];
|
|
|
|
let blackKeyOffset = [1, 3, 0, 6, 8, 10, 0];
|
|
let whiteKeyOffset = [0, 2, 4, 5, 7, 9, 11];
|
|
|
|
let keybindValues = [0, 1, 2, 3, 4, -1, 5, 6, 7, 8, 9, 10, 11, -1];
|
|
|
|
let keyMap = {
|
|
"IntlBackslash": -16,
|
|
"KeyA": -15,
|
|
"KeyZ": -14,
|
|
"KeyS": -13,
|
|
"KeyX": -12,
|
|
"KeyD": -11,
|
|
"KeyC": -10,
|
|
"KeyF": -9,
|
|
"KeyV": -8,
|
|
"KeyG": -7,
|
|
"KeyB": -6,
|
|
"KeyH": -5,
|
|
"KeyN": -4,
|
|
"KeyJ": -3,
|
|
"KeyM": -2,
|
|
"KeyK": -1,
|
|
"Comma": 0,
|
|
"KeyL": 1,
|
|
"Period": 2,
|
|
"Semicolon": 3,
|
|
"Slash": 4,
|
|
"Quote": 5,
|
|
"Digit1": -1,
|
|
"KeyQ": 0,
|
|
"Digit2": 1,
|
|
"KeyW": 2,
|
|
"Digit3": 3,
|
|
"KeyE": 4,
|
|
"Digit4": 5,
|
|
"KeyR": 6,
|
|
"Digit5": 7,
|
|
"KeyT": 8,
|
|
"Digit6": 9,
|
|
"KeyY": 10,
|
|
"Digit7": 11,
|
|
"KeyU": 12,
|
|
"Digit8": 13,
|
|
"KeyI": 14,
|
|
"Digit9": 15,
|
|
"KeyO": 16,
|
|
"Digit0": 17,
|
|
"KeyP": 18,
|
|
"Minus": 19,
|
|
"BracketLeft": 20,
|
|
"Equal": 21,
|
|
"BracketRight": 22
|
|
};
|
|
|
|
let keysDown = new Set();
|
|
|
|
let piano;
|
|
let audioEngine;
|
|
let net;
|
|
let chat;
|
|
|
|
let focus = true;
|
|
let userMenuUser;
|
|
|
|
let soundBank = [];
|
|
|
|
let loading = 0;
|
|
|
|
function loadMppBank(url, format) {
|
|
for (let i=21; i<109; i++) {
|
|
piano.keys[i - 21].loaded = false;
|
|
let keyName = ["c", "cs", "d", "ds", "e", "f", "fs", "g", "gs", "a", "as", "b"][i % 12] + (Math.floor(i / 12) - 2);
|
|
let xhttp = new XMLHttpRequest();
|
|
xhttp.responseType = "arraybuffer";
|
|
xhttp.addEventListener("load", (function(index) {
|
|
return function() {
|
|
audioCtx.decodeAudioData(xhttp.response, function(buffer) {
|
|
soundBank[index] = buffer;
|
|
loading++;
|
|
piano.keys[i - 21].loaded = true;
|
|
});
|
|
}
|
|
})(i));
|
|
xhttp.open("GET", url + keyName + "." + format);
|
|
xhttp.send();
|
|
}
|
|
}
|
|
|
|
class Key {
|
|
constructor(index) {
|
|
this.index = index;
|
|
this.value = index + 21;
|
|
this.type = pianoLayout[this.value % 12];
|
|
this.pressed = false;
|
|
this.presser = null;
|
|
this.loaded = false;
|
|
|
|
this.y = 1;
|
|
this.color = null;
|
|
}
|
|
|
|
animate() {
|
|
if (this.pressed) {
|
|
this.y = 0;
|
|
if (!(this.presser in net.users)) {
|
|
this.pressed = false;
|
|
return;
|
|
}
|
|
|
|
if (Date.now() - this.pressed > 5000) {
|
|
this.pressed = false;
|
|
return;
|
|
}
|
|
|
|
this.color = net.users[this.presser].color;
|
|
} else {
|
|
this.y = 1;
|
|
this.color = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
class Piano {
|
|
constructor(canvas) {
|
|
this.canvas = canvas;
|
|
this.ctx = canvas.getContext("2d");
|
|
|
|
this.keys = [];
|
|
for (let i=0; i<88; i++) {
|
|
this.keys.push(new Key(i));
|
|
}
|
|
|
|
this.sustain = false;
|
|
this.performanceMode = false;
|
|
|
|
this.margin = 8;
|
|
|
|
this.scale = 1;
|
|
this.setScale(3);
|
|
}
|
|
|
|
// Get key at position
|
|
hit(x, y) {
|
|
let whiteWidth = (this.canvas.width - 155 - this.margin * 2) / 52 + 2;
|
|
let blackWidth = Math.ceil(whiteWidth * 0.5 / 2) * 2 + 1;
|
|
let blackHeight = Math.floor((this.canvas.height - this.margin * 2) / 2);
|
|
|
|
// Offset to octave 0
|
|
x += (whiteWidth + 1) * 5 - this.margin;
|
|
y -= this.margin;
|
|
|
|
if (y <= blackHeight) {
|
|
let blackX = x - (blackWidth - 1) / 2;
|
|
let blackKey = Math.floor(blackX / (whiteWidth + 1));
|
|
|
|
if (!(blackKey % 7 === 2 || blackKey % 7 === 6 || blackKey == 4 || blackKey == 56)) {
|
|
blackX = blackX % (whiteWidth + 1);
|
|
|
|
if (blackX >= whiteWidth - (blackWidth - 1)) {
|
|
let index = Math.floor(blackKey / 7) * 12 + blackKeyOffset[blackKey % 7];
|
|
return this.keys[index - 9];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (x % (whiteWidth + 1) == whiteWidth) {
|
|
return null;
|
|
}
|
|
|
|
let whiteKey = Math.floor(x / (whiteWidth + 1));
|
|
let index = Math.floor(whiteKey / 7) * 12 + whiteKeyOffset[whiteKey % 7];
|
|
return this.keys[index - 9];
|
|
}
|
|
|
|
setScale(scale) {
|
|
this.scale = scale;
|
|
this.canvas.style.transform = "scale(" + scale + ")";
|
|
this.resize();
|
|
}
|
|
|
|
resize() {
|
|
// border + margin pixels: 155
|
|
let size = Math.floor((window.innerWidth / this.scale - 155) / 52);
|
|
|
|
this.canvas.width = size * 52 + 155 + this.margin * 2;
|
|
this.canvas.height = Math.floor(this.canvas.width * 0.18);
|
|
}
|
|
|
|
animateKeys() {
|
|
this.keys.forEach(function(key) {
|
|
key.animate();
|
|
});
|
|
}
|
|
|
|
render() {
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
// Render casing
|
|
this.ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
// Render keys
|
|
|
|
let keyRegionWidth = this.canvas.width - this.margin * 2;
|
|
let keyRegionHeight = this.canvas.height - this.margin * 2;
|
|
|
|
let whiteWidth = (keyRegionWidth - 155) / 52 + 2;
|
|
let blackWidth = Math.ceil(whiteWidth * 0.5 / 2) * 2 + 1;
|
|
let blackHeight = Math.floor(keyRegionHeight / 2);
|
|
|
|
let keyDepth = Math.ceil(keyRegionHeight / 20);
|
|
|
|
this.ctx.save();
|
|
this.ctx.translate(this.margin, this.margin);
|
|
|
|
// Draw white keys
|
|
this.ctx.strokeStyle = "#ccc";
|
|
for (let i=0, x=0; i<this.keys.length; i++) {
|
|
let key = this.keys[i];
|
|
|
|
if (key.type === BLACK) continue;
|
|
|
|
if (!key.loaded) {
|
|
x += whiteWidth + 1;
|
|
continue;
|
|
}
|
|
|
|
let y = Math.floor(keyDepth * (1 - key.y));
|
|
|
|
let color = [0xff, 0xff, 0xff];
|
|
if (key.color) {
|
|
color = key.color;
|
|
}
|
|
|
|
this.ctx.fillStyle = `rgb(${color[0] * 0.8}, ${color[1] * 0.8}, ${color[2] * 0.8})`;
|
|
this.ctx.fillRect(x, keyDepth + y, whiteWidth, keyRegionHeight - y - keyDepth);
|
|
this.ctx.fillStyle = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
|
|
this.ctx.fillRect(x + 1, keyDepth + y + 1, whiteWidth - 2, keyRegionHeight - keyDepth * 2 - 2);
|
|
|
|
x += whiteWidth + 1;
|
|
}
|
|
|
|
this.ctx.strokeStyle = "#222";
|
|
for (let i=0, x=-((blackWidth + 1) / 2); i<this.keys.length; i++) {
|
|
let key = this.keys[i];
|
|
|
|
if (key.type === WHITE) {
|
|
x += whiteWidth + 1;
|
|
continue;
|
|
}
|
|
|
|
if (!key.loaded) continue;
|
|
|
|
let y = Math.floor(keyDepth * (1 - key.y));
|
|
|
|
let color = [0x44, 0x44, 0x44];
|
|
if (key.color) {
|
|
color = key.color;
|
|
}
|
|
|
|
this.ctx.fillStyle = `rgb(${color[0] * 0.6}, ${color[1] * 0.6}, ${color[2] * 0.6})`;
|
|
this.ctx.fillRect(x, y, blackWidth, blackHeight - y);
|
|
this.ctx.fillStyle = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
|
|
this.ctx.fillRect(x + 1, y + 1, blackWidth - 2, blackHeight - (keyDepth + 2));
|
|
}
|
|
|
|
this.ctx.restore();
|
|
}
|
|
|
|
localKeyDown(key, velocity) {
|
|
net.keyDown(key, velocity);
|
|
this.keyDown(key, velocity, net.id);
|
|
}
|
|
|
|
localKeyUp(key) {
|
|
net.keyUp(key, this.sustain);
|
|
this.keyUp(key, this.sustain, net.id);
|
|
}
|
|
|
|
localReleaseSustain() {
|
|
let should = false;
|
|
for (let i=0; i<audioEngine.playing.length; i++) {
|
|
if (audioEngine.playing[i].user == net.id) {
|
|
should = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!should) return;
|
|
|
|
net.releaseSustain();
|
|
this.releaseSustain(net.id);
|
|
}
|
|
|
|
keyDown(key, velocity, user) {
|
|
if (!((key - 21) in this.keys)) return;
|
|
|
|
if (net.users[user].muteNotes) return;
|
|
|
|
this.keys[key - 21].pressed = Date.now();
|
|
this.keys[key - 21].presser = user;
|
|
audioEngine.noteOn(key, velocity, user);
|
|
}
|
|
|
|
keyUp(key, sustain, user) {
|
|
if (!((key - 21) in this.keys)) return;
|
|
|
|
if (net.users[user].muteNotes) return;
|
|
|
|
this.keys[key - 21].pressed = false;
|
|
if (!sustain) {
|
|
audioEngine.noteOff(key, user);
|
|
}
|
|
}
|
|
|
|
releaseSustain(user) {
|
|
audioEngine.releaseSustain(user);
|
|
}
|
|
}
|
|
|
|
class NoteSound {
|
|
constructor(key, velocity, user) {
|
|
this.key = key;
|
|
let sound = soundBank[key];
|
|
|
|
this.source = audioCtx.createBufferSource();
|
|
this.source.buffer = sound;
|
|
this.ended = () => {
|
|
this.remove();
|
|
};
|
|
this.source.addEventListener("ended", this.ended);
|
|
this.gain = audioCtx.createGain();
|
|
this.gain.gain.value = velocity;
|
|
this.source.connect(this.gain);
|
|
this.gain.connect(audioEngine.destination);
|
|
this.source.start();
|
|
|
|
this.user = user;
|
|
|
|
audioEngine.playing.push(this);
|
|
}
|
|
|
|
stop() {
|
|
let fade = 0.15;
|
|
this.gain.gain.setTargetAtTime(0, audioCtx.currentTime, fade / 3);
|
|
//this.gain.gain.linearRampToValueAtTime(this.gain.gain.value * 0.1, time + 0.2);
|
|
//this.gain.gain.linearRampToValueAtTime(0, time + 0.5);
|
|
this.source.removeEventListener("ended", this.ended);
|
|
this.source.stop(audioCtx.currentTime + fade);
|
|
|
|
this.remove();
|
|
}
|
|
|
|
purge() {
|
|
this.source.removeEventListener("ended", this.ended);
|
|
this.source.stop();
|
|
this.gain.disconnect(audioEngine.destination);
|
|
|
|
this.remove();
|
|
}
|
|
|
|
remove() {
|
|
let index = audioEngine.playing.indexOf(this);
|
|
if (index === -1) {
|
|
// This should never happen, but shit will fuck up if it happens.
|
|
return;
|
|
}
|
|
|
|
audioEngine.playing.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
class AudioEngine {
|
|
constructor() {
|
|
this.active = false; // autoplay policy
|
|
|
|
this.playing = [];
|
|
|
|
this.masterGain = audioCtx.createGain();
|
|
this.masterGain.gain.value = 0.5;
|
|
this.masterGain.connect(audioCtx.destination);
|
|
|
|
this.compressor = audioCtx.createDynamicsCompressor();
|
|
// These should probably be fine tuned
|
|
this.compressor.threshold.value = -10;
|
|
this.compressor.knee.value = 0;
|
|
this.compressor.ratio.value = 20;
|
|
this.compressor.attack.value = 0;
|
|
this.compressor.release.value = 0.1;
|
|
this.compressor.connect(this.masterGain);
|
|
|
|
this.destination = this.compressor;
|
|
}
|
|
|
|
get volume() {
|
|
return this.masterGain.gain.value;
|
|
}
|
|
|
|
set volume(value) {
|
|
this.masterGain.gain.value = value;
|
|
}
|
|
|
|
noteOn(key, velocity, user) {
|
|
if (!this.active) return;
|
|
|
|
/*if (this.playing.length >= 500) {
|
|
// Prevent lag
|
|
return;
|
|
}*/
|
|
|
|
for (let i=0; i<this.playing.length; i++) {
|
|
if (this.playing[i].key == key && this.playing[i].user == user) {
|
|
if (piano.performanceMode) {
|
|
this.playing[i].purge();
|
|
} else {
|
|
this.playing[i].stop();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
new NoteSound(key, velocity, user);
|
|
}
|
|
|
|
noteOff(key, user) {
|
|
for (let i=this.playing.length - 1; i>=0; i--) {
|
|
if (this.playing[i].key == key && this.playing[i].user == user) {
|
|
this.playing[i].stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
releaseSustain(user) {
|
|
for (let i=this.playing.length - 1; i>=0; i--) {
|
|
if (this.playing[i].user == user) {
|
|
this.playing[i].stop();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function render() {
|
|
piano.animateKeys();
|
|
piano.render();
|
|
|
|
window.requestAnimationFrame(render);
|
|
}
|
|
|
|
function setVolume(value, modifySlider) {
|
|
audioEngine.volume = value;
|
|
|
|
if (value === 0) {
|
|
document.getElementById("volume-indicator").className = "mute";
|
|
} else if (value <= 0.5) {
|
|
document.getElementById("volume-indicator").className = "low";
|
|
} else {
|
|
document.getElementById("volume-indicator").className = "";
|
|
}
|
|
|
|
if (modifySlider) {
|
|
document.getElementById("volume").value = value;
|
|
}
|
|
}
|
|
|
|
function openModal(element, dimmer, callback) {
|
|
if (dimmer) {
|
|
document.getElementById("dimmer").classList.add("active");
|
|
}
|
|
element.classList.add("active");
|
|
|
|
function clickListener(event) {
|
|
if (!element.contains(event.target)) {
|
|
element.classList.remove("active");
|
|
document.getElementById("dimmer").classList.remove("active");
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
document.removeEventListener("click", clickListener, true);
|
|
}
|
|
}
|
|
|
|
document.addEventListener("click", clickListener, true);
|
|
}
|
|
|
|
class User {
|
|
constructor(id, uid, nick, color) {
|
|
this.id = id;
|
|
this.uid = uid;
|
|
this.nick = nick;
|
|
this.color = color;
|
|
|
|
this.local = this.id === net.id;
|
|
|
|
this.muteNotes = false;
|
|
|
|
this.element = document.createElement("li");
|
|
if (this.local) {
|
|
this.element.classList.add("local");
|
|
}
|
|
this.element.addEventListener("click", () => {
|
|
this.updateUserMenu();
|
|
openModal(document.getElementById("user-menu"), false, () => {
|
|
if (!this.local) return;
|
|
|
|
let nick = document.getElementById("user-nick").value;
|
|
if (nick != this.nick) {
|
|
net.setNick(nick);
|
|
}
|
|
});
|
|
event.stopImmediatePropagation();
|
|
});
|
|
this.element.innerText = this.nick ? this.nick : this.id;
|
|
this.element.style.backgroundColor = this.getColor();
|
|
document.getElementById("users").appendChild(this.element);
|
|
}
|
|
|
|
getColor() {
|
|
if (this.muteNotes) {
|
|
return "rgb(140, 140, 140)";
|
|
} else {
|
|
return `rgb(${this.color[0]}, ${this.color[1]}, ${this.color[2]})`;
|
|
}
|
|
}
|
|
|
|
updateUserMenu() {
|
|
userMenuUser = this.id;
|
|
let userMenu = document.getElementById("user-menu");
|
|
if (this.id == net.id) {
|
|
userMenu.classList.add("local");
|
|
} else {
|
|
userMenu.classList.remove("local");
|
|
}
|
|
userMenu.style.backgroundColor = this.getColor();
|
|
document.getElementById("user-uid").textContent = this.uid;
|
|
document.getElementById("user-nick").value = this.nick;
|
|
document.getElementById("user-mute-notes").checked = this.muteNotes;
|
|
|
|
let box = this.element.getBoundingClientRect();
|
|
userMenu.style.top = (box.bottom + 8) + "px";
|
|
userMenu.style.left = Math.max(box.left, 8) + "px";
|
|
}
|
|
|
|
update() {
|
|
this.element.style.backgroundColor = this.getColor();
|
|
}
|
|
|
|
setMuteNotes(value) {
|
|
this.muteNotes = value;
|
|
|
|
if (this.muteNotes) {
|
|
piano.releaseSustain(this.id);
|
|
}
|
|
}
|
|
|
|
changeNick(nick) {
|
|
this.nick = nick;
|
|
this.element.innerText = this.nick;
|
|
}
|
|
|
|
leave() {
|
|
document.getElementById("users").removeChild(this.element);
|
|
delete net.users[this.id];
|
|
}
|
|
}
|
|
|
|
class Networker {
|
|
constructor(url = location.origin.replace("http", "ws")) {
|
|
if (localStorage.nick) url += "?nick=" + encodeURIComponent(localStorage.nick);
|
|
|
|
this.ws = new WebSocket(url);
|
|
this.ws.binaryType = "arraybuffer";
|
|
this.ws.addEventListener("open", function() {
|
|
console.log("open");
|
|
});
|
|
this.ws.addEventListener("message", (message) => {
|
|
this.message(message.data);
|
|
});
|
|
this.ws.addEventListener("close", function() {
|
|
console.log("close");
|
|
chat.receive({
|
|
type: "disconnected"
|
|
});
|
|
});
|
|
|
|
this.users = {};
|
|
}
|
|
|
|
message(message) {
|
|
if (typeof message == "string") {
|
|
message = JSON.parse(message);
|
|
|
|
console.log(message);
|
|
|
|
switch(message.type) {
|
|
case "load":
|
|
|
|
this.id = message.id;
|
|
|
|
this.users = {};
|
|
for (let i=0; i<message.users.length; i++) {
|
|
let user = message.users[i];
|
|
this.users[user.id] = new User(user.id, user.uid, user.nick, user.color);
|
|
}
|
|
|
|
chat.chatLog(message.chatlog);
|
|
|
|
if (localStorage.adminlogin) {
|
|
net.chat("/adminlogin " + localStorage.adminlogin);
|
|
}
|
|
break;
|
|
case "join":
|
|
this.users[message.id] = new User(message.id, message.uid, message.nick, message.color);
|
|
break;
|
|
case "leave":
|
|
this.users[message.id].leave();
|
|
break;
|
|
case "chat":
|
|
chat.receive(message.message);
|
|
break;
|
|
case "nick":
|
|
this.users[message.id].changeNick(message.nick);
|
|
if (message.id == this.id) {
|
|
localStorage.nick = message.nick;
|
|
}
|
|
break;
|
|
case "rank":
|
|
let rank = message.rank;
|
|
|
|
chat.receive({
|
|
type: "rank",
|
|
rank: rank
|
|
});
|
|
|
|
if (rank === 0) {
|
|
document.body.className = "";
|
|
} else if (rank === 1) {
|
|
document.body.className = "owner";
|
|
} else if (rank === 2) {
|
|
document.body.className = "admin";
|
|
}
|
|
break;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
let dv = new DataView(message);
|
|
switch(dv.getUint8(0)) {
|
|
case 0: { // Key down
|
|
let key = dv.getUint8(1);
|
|
let velocity = dv.getUint8(2) / 255;
|
|
let id = dv.getUint8(3);
|
|
piano.keyDown(key, velocity, id);
|
|
break;
|
|
}
|
|
case 1: { // Key up
|
|
let key = dv.getUint8(1);
|
|
let sustain = dv.getUint8(2);
|
|
let id = dv.getUint8(3);
|
|
piano.keyUp(key, sustain, id);
|
|
break;
|
|
}
|
|
case 2: { // Sustain release
|
|
let id = dv.getUint8(1);
|
|
piano.releaseSustain(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
keyDown(key, velocity) {
|
|
let buffer = new ArrayBuffer(3);
|
|
let dv = new DataView(buffer);
|
|
dv.setUint8(0, 0);
|
|
dv.setUint8(1, key);
|
|
dv.setUint8(2, Math.floor(velocity * 255));
|
|
this.ws.send(buffer);
|
|
}
|
|
|
|
keyUp(key, sustain) {
|
|
let buffer = new ArrayBuffer(3);
|
|
let dv = new DataView(buffer);
|
|
dv.setUint8(0, 1);
|
|
dv.setUint8(1, key);
|
|
dv.setUint8(2, sustain);
|
|
this.ws.send(buffer);
|
|
}
|
|
|
|
releaseSustain() {
|
|
let buffer = new ArrayBuffer(1);
|
|
let dv = new DataView(buffer);
|
|
dv.setUint8(0, 2);
|
|
this.ws.send(buffer);
|
|
}
|
|
|
|
chat(message) {
|
|
this.ws.send(JSON.stringify({
|
|
type: "chat",
|
|
message: message
|
|
}));
|
|
}
|
|
|
|
setNick(nick) {
|
|
this.ws.send(JSON.stringify({
|
|
type: "nick",
|
|
nick: nick
|
|
}));
|
|
}
|
|
}
|
|
|
|
class Chat {
|
|
constructor(chatElement, messagesElement, inputElement) {
|
|
this.chatElement = chatElement;
|
|
this.messagesElement = messagesElement;
|
|
this.inputElement = inputElement;
|
|
|
|
this.prevNotif = null;
|
|
this.notifTimeout = null;
|
|
|
|
this.addListeners();
|
|
}
|
|
|
|
addListeners() {
|
|
window.addEventListener("keydown", event => {
|
|
if (event.code == "Enter") {
|
|
if (document.activeElement == this.inputElement) {
|
|
let message = this.inputElement.value;
|
|
message = message.trim();
|
|
if (message) {
|
|
this.send(message);
|
|
}
|
|
this.inputElement.value = "";
|
|
this.inputElement.blur();
|
|
} else {
|
|
this.inputElement.focus();
|
|
}
|
|
}
|
|
});
|
|
this.inputElement.addEventListener("focus", () => {
|
|
this.chatElement.className = "focus";
|
|
});
|
|
this.inputElement.addEventListener("blur", () => {
|
|
this.chatElement.className = "";
|
|
});
|
|
}
|
|
|
|
chatLog(chatLog) {
|
|
for (let i=0; i<chatLog.length; i++) {
|
|
this.receive(chatLog[i], true);
|
|
}
|
|
}
|
|
|
|
notification(nick, message) {
|
|
if (focus) return;
|
|
|
|
if (!("Notification" in window)) return false;
|
|
|
|
if (Notification.permission == "denied") return false;
|
|
|
|
if (Notification.permission == "granted") {
|
|
if (this.prevNotif) {
|
|
console.log("close notif");
|
|
this.prevNotif.close();
|
|
}
|
|
|
|
let notif = new Notification(nick, {
|
|
body: message
|
|
});
|
|
|
|
this.prevNotif = notif;
|
|
|
|
notif.addEventListener("click", function() {
|
|
window.focus();
|
|
});
|
|
|
|
// Close notification after 2 seconds
|
|
clearInterval(this.notifTimeout);
|
|
this.notifTimeout = setTimeout(() => {
|
|
if (this.prevNotif) {
|
|
this.prevNotif.close();
|
|
}
|
|
}, 2000);
|
|
|
|
return true;
|
|
}
|
|
|
|
Notification.requestPermission().then(function(permission) {
|
|
if (permission === "granted") {
|
|
new Notification("Notifications enabled!");
|
|
}
|
|
});
|
|
}
|
|
|
|
receive(message, chatLog) {
|
|
let element = document.createElement("li");
|
|
switch(message.type) {
|
|
case "message":
|
|
element.className = "message";
|
|
let user = message.user;
|
|
let nickSpan = document.createElement("span");
|
|
nickSpan.className = "nick";
|
|
nickSpan.style.color = `rgb(${user.color[0]}, ${user.color[1]}, ${user.color[2]})`;
|
|
nickSpan.textContent = user.nick;
|
|
element.appendChild(nickSpan);
|
|
let text = document.createElement("span");
|
|
text.className = "content";
|
|
text.textContent = message.content;
|
|
element.appendChild(text);
|
|
|
|
if (!chatLog) {
|
|
this.notification(user.nick, message.content);
|
|
}
|
|
break;
|
|
case "join":
|
|
element.className = "join";
|
|
element.textContent = message.nick + " joined";
|
|
break;
|
|
case "leave":
|
|
element.className = "leave";
|
|
element.textContent = message.nick + " left";
|
|
break;
|
|
case "nick":
|
|
element.className = "nick";
|
|
element.textContent = message.id + " changed nick to " + message.nick;
|
|
break;
|
|
case "disconnected":
|
|
element.className = "disconnected";
|
|
element.textContent = "You were disconnected from the server!";
|
|
break;
|
|
}
|
|
|
|
let shouldScroll = false;
|
|
if (this.messagesElement.scrollTop >= this.messagesElement.scrollHeight - this.messagesElement.clientHeight) {
|
|
shouldScroll = true;
|
|
}
|
|
|
|
this.messagesElement.appendChild(element);
|
|
|
|
if (shouldScroll) {
|
|
this.messagesElement.scrollTop = this.messagesElement.scrollHeight - this.messagesElement.clientHeight;
|
|
}
|
|
}
|
|
|
|
send(message) {
|
|
net.chat(message);
|
|
}
|
|
}
|
|
|
|
|
|
window.addEventListener("load", function() {
|
|
piano = new Piano(document.getElementById("piano"));
|
|
audioEngine = new AudioEngine();
|
|
//loadMppBank("https://ledlamp.github.io/piano-sounds/Emotional/", "mp3");
|
|
loadMppBank("https://ledlamp.github.io/piano-sounds/Untitled/", "mp3");
|
|
|
|
window.addEventListener("resize", function() {
|
|
piano.resize();
|
|
});
|
|
|
|
function activate() {
|
|
audioEngine.active = true;
|
|
document.getElementById("autoplay-notice").classList.add("hidden");
|
|
|
|
window.removeEventListener("click", activate);
|
|
}
|
|
|
|
window.addEventListener("click", activate);
|
|
|
|
chat = new Chat(
|
|
document.getElementById("chat"),
|
|
document.getElementById("chat-messages"),
|
|
document.getElementById("chat-input")
|
|
);
|
|
|
|
// User menu
|
|
document.getElementById("user-mute-notes").addEventListener("input", function() {
|
|
net.users[userMenuUser].setMuteNotes(this.checked);
|
|
net.users[userMenuUser].updateUserMenu();
|
|
net.users[userMenuUser].update();
|
|
});
|
|
|
|
// Focus
|
|
window.addEventListener("focus", function() {
|
|
focus = true;
|
|
});
|
|
window.addEventListener("blur", function() {
|
|
focus = false;
|
|
});
|
|
|
|
// Volume slider
|
|
document.getElementById("volume").addEventListener("input", function(event) {
|
|
let value = parseFloat(event.target.value);
|
|
setVolume(value);
|
|
});
|
|
document.getElementById("volume").addEventListener("focus", function(event) {
|
|
this.blur();
|
|
});
|
|
|
|
// Footer menus
|
|
document.getElementById("midi-player-button").addEventListener("click", function(event) {
|
|
openModal(document.getElementById("midi-player-menu"), false);
|
|
event.stopImmediatePropagation();
|
|
});
|
|
document.getElementById("settings-button").addEventListener("click", function(event) {
|
|
openModal(document.getElementById("settings-menu"), false);
|
|
event.stopImmediatePropagation();
|
|
});
|
|
|
|
// Midi player
|
|
document.getElementById("midi-upload").addEventListener("change", function() {
|
|
let reader = new FileReader();
|
|
reader.addEventListener("load", function() {
|
|
let midiFile = new MidiFile(reader.result);
|
|
midiPlayer.loadMidi(midiFile);
|
|
console.log(midiFile);
|
|
});
|
|
reader.readAsArrayBuffer(this.files[0]);
|
|
});
|
|
document.getElementById("midi-play").addEventListener("click", function() {
|
|
midiPlayer.play();
|
|
});
|
|
document.getElementById("midi-pause").addEventListener("click", function() {
|
|
midiPlayer.pause();
|
|
});
|
|
|
|
|
|
// Settings menu
|
|
let tabs = ["general", "midi", "input", "audio"];
|
|
tabs.forEach(function(tab) {
|
|
document.getElementById("tab-" + tab + "-button").addEventListener("click", function() {
|
|
for (let i=0; i<tabs.length; i++) {
|
|
document.getElementById("tab-" + tabs[i]).classList.remove("active");
|
|
document.getElementById("tab-" + tabs[i] + "-button").classList.remove("active");
|
|
}
|
|
|
|
document.getElementById("tab-" + tab).classList.add("active");
|
|
document.getElementById("tab-" + tab + "-button").classList.add("active");
|
|
});
|
|
});
|
|
|
|
// Scale option
|
|
for (let i=1; i<=6; i++) {
|
|
let option = document.createElement("option");
|
|
option.textContent = i + "x";
|
|
option.value = i;
|
|
document.getElementById("setting-scale").appendChild(option);
|
|
}
|
|
document.getElementById("setting-scale").value = piano.scale;
|
|
document.getElementById("setting-scale").addEventListener("input", function() {
|
|
let scale = parseInt(this.value);
|
|
piano.setScale(scale);
|
|
});
|
|
|
|
// Performance mode
|
|
document.getElementById("setting-performance").addEventListener("input", function() {
|
|
piano.performanceMode = this.checked;
|
|
});
|
|
|
|
// Sounds
|
|
let sounds = [
|
|
["Emotional", "https://ledlamp.github.io/piano-sounds/Emotional/", "mp3"],
|
|
["Emotional_2.0", "https://ledlamp.github.io/piano-sounds/Emotional_2.0/", "mp3"],
|
|
["GreatAndSoftPiano", "https://ledlamp.github.io/piano-sounds/GreatAndSoftPiano/", "mp3"],
|
|
["HardAndToughPiano", "https://ledlamp.github.io/piano-sounds/HardAndToughPiano/", "mp3"],
|
|
["HardPiano", "https://ledlamp.github.io/piano-sounds/HardPiano/", "mp3"],
|
|
["Harp", "https://ledlamp.github.io/piano-sounds/Harp/", "mp3"],
|
|
["Harpsicord", "https://ledlamp.github.io/piano-sounds/Harpsicord/", "mp3"],
|
|
["LoudAndProudPiano", "https://ledlamp.github.io/piano-sounds/LoudAndProudPiano/", "mp3"],
|
|
["MLG", "https://ledlamp.github.io/piano-sounds/MLG/", "wav"],
|
|
["Music_Box", "https://ledlamp.github.io/piano-sounds/Music_Box/", "mp3"],
|
|
["NewPiano", "https://ledlamp.github.io/piano-sounds/NewPiano/", "mp3"],
|
|
["Orchestra", "https://ledlamp.github.io/piano-sounds/Orchestra/", "wav"],
|
|
["Piano2", "https://ledlamp.github.io/piano-sounds/Piano2/", "mp3"],
|
|
["PianoSounds", "https://ledlamp.github.io/piano-sounds/PianoSounds/", "mp3"],
|
|
["Rhodes_MK1", "https://ledlamp.github.io/piano-sounds/Rhodes_MK1/", "mp3"],
|
|
["SoftPiano", "https://ledlamp.github.io/piano-sounds/SoftPiano/", "mp3"],
|
|
["Steinway_Grand", "https://ledlamp.github.io/piano-sounds/Steinway_Grand/", "mp3"],
|
|
["Untitled", "https://ledlamp.github.io/piano-sounds/Untitled/", "mp3"],
|
|
["Vintage_Upright", "https://ledlamp.github.io/piano-sounds/Vintage_Upright/", "mp3"],
|
|
["Vintage_Upright_Soft", "https://ledlamp.github.io/piano-sounds/Vintage_Upright_Soft/", "mp3"]
|
|
];
|
|
|
|
sounds.forEach(function(sound, index) {
|
|
function getOption() {
|
|
let element = document.createElement("option");
|
|
//element.value = index;
|
|
element.textContent = sound[0];
|
|
return element;
|
|
}
|
|
|
|
document.getElementById("setting-sounds").appendChild(getOption());
|
|
});
|
|
document.getElementById("setting-sounds").addEventListener("input", function() {
|
|
let sound = this.value;
|
|
let index = sounds.findIndex(function(s) {
|
|
return s[0] == sound;
|
|
});
|
|
loadMppBank(sounds[index][1], sounds[index][2]);
|
|
});
|
|
|
|
|
|
// Mouse Input
|
|
|
|
let keyDown = null;
|
|
piano.canvas.addEventListener("mousedown", (event) => {
|
|
if (keyDown) {
|
|
piano.localKeyUp(keyDown);
|
|
keyDown = null;
|
|
}
|
|
|
|
let key = piano.hit(event.offsetX, event.offsetY);
|
|
if (key) {
|
|
piano.localKeyDown(key.value, 1);
|
|
keyDown = key.value;
|
|
}
|
|
});
|
|
window.addEventListener("mouseup", (event) => {
|
|
if (keyDown) {
|
|
piano.localKeyUp(keyDown);
|
|
keyDown = null;
|
|
}
|
|
});
|
|
|
|
// Keyboard Input
|
|
|
|
window.addEventListener("keydown", (event) => {
|
|
if (document.activeElement != document.body) return;
|
|
|
|
let key = event.code;
|
|
|
|
if (key == "Backspace") {
|
|
piano.sustain = !piano.sustain;
|
|
if (!piano.sustain) {
|
|
piano.localReleaseSustain();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (keysDown.has(key)) return;
|
|
|
|
keysDown.add(key);
|
|
|
|
if (!(key in keyMap)) return;
|
|
|
|
let rawValue = keyMap[key];
|
|
|
|
if (keybindValues[mod(rawValue, 14)] === -1) return;
|
|
|
|
let value = Math.floor(rawValue / 14) * 12 + keybindValues[mod(rawValue, 14)] + 60;
|
|
|
|
piano.localKeyDown(value, 1);
|
|
});
|
|
window.addEventListener("keyup", (event) => {
|
|
if (document.activeElement != document.body) return;
|
|
|
|
let key = event.code;
|
|
|
|
keysDown.delete(key);
|
|
|
|
let rawValue = keyMap[key];
|
|
|
|
if (!(key in keyMap)) return;
|
|
|
|
if (keybindValues[mod(rawValue, 14)] === -1) return;
|
|
|
|
let value = Math.floor(rawValue / 14) * 12 + keybindValues[mod(rawValue, 14)] + 60;
|
|
|
|
piano.localKeyUp(value);
|
|
});
|
|
|
|
// MIDI Input
|
|
|
|
if (navigator.requestMIDIAccess) {
|
|
navigator.requestMIDIAccess().then(function(midi) {
|
|
console.log(midi);
|
|
|
|
function messageHandler(event) {
|
|
let cmd = event.data[0] >> 4;
|
|
let channel = event.data[0] & 0xF;
|
|
let note = event.data[1];
|
|
let vel = event.data[2];
|
|
|
|
if (cmd == 8 || (cmd == 9 && vel === 0)) {
|
|
// Note off
|
|
piano.localKeyUp(note);
|
|
} else if (cmd == 9) {
|
|
// Note on
|
|
piano.localKeyDown(note, vel / 127);
|
|
} else if (cmd == 11) {
|
|
// Control change
|
|
if (note == 64) {
|
|
if (vel >= 64) {
|
|
// Sustain
|
|
piano.sustain = true;
|
|
} else {
|
|
// Sustain off
|
|
piano.sustain = false;
|
|
piano.localReleaseSustain();
|
|
}
|
|
} else if (note == 7) {
|
|
// Volume
|
|
let volume = vel / 127;
|
|
setVolume(volume, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
function port() {
|
|
midi.inputs.forEach(function(input) {
|
|
input.addEventListener("midimessage", messageHandler);
|
|
});
|
|
}
|
|
|
|
port();
|
|
|
|
// Get lists of available MIDI controllers
|
|
const inputs = midi.inputs.values();
|
|
const outputs = midi.outputs.values();
|
|
|
|
midi.addEventListener("statechange", function(e) {
|
|
// Print information about the (dis)connected MIDI controller
|
|
console.log(e.port.name, e.port.manufacturer, e.port.state);
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
|
|
// Net
|
|
|
|
net = new Networker();
|
|
|
|
window.requestAnimationFrame(render);
|
|
});
|