Compare commits
17 Commits
templating
...
dev2
Author | SHA1 | Date |
---|---|---|
Hri7566 | 87352df5b2 | |
Hri7566 | ef70175403 | |
Hri7566 | fb17f616a7 | |
Hri7566 | 253ef651c6 | |
Hri7566 | d76069109b | |
Hri7566 | b65bf920be | |
Hri7566 | bb4454345d | |
Hri7566 | ccc02f653a | |
Hri7566 | 2c50e7ae5d | |
Hri7566 | cb72f5ac7b | |
Hri7566 | 911b907f85 | |
Hri7566 | c7344e1bd9 | |
Hri7566 | c15dee967b | |
Hri7566 | af660e9834 | |
Hri7566 | f5b708aceb | |
Hri7566 | 9181370a82 | |
Hri7566 | 9b28ae985c |
|
@ -0,0 +1,4 @@
|
||||||
|
[submodule "sounds"]
|
||||||
|
path = sounds
|
||||||
|
url = git@github.com:Hri7566/piano-sounds
|
||||||
|
branch = master
|
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 4.6 KiB |
282
Client.js
|
@ -1,24 +1,15 @@
|
||||||
|
WebSocket.prototype.send = new Proxy(WebSocket.prototype.send, {
|
||||||
if(typeof module !== "undefined") {
|
apply: (target, thisArg, args) => {
|
||||||
module.exports = Client;
|
if (localStorage.token && !args[0].startsWith(`[{"m":"hi"`))
|
||||||
WebSocket = require("ws");
|
args[0] = args[0].replace(localStorage.token, "[REDACTED]");
|
||||||
EventEmitter = require("events").EventEmitter;
|
return target.apply(thisArg, args);
|
||||||
} else {
|
|
||||||
this.Client = Client;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class Client extends EventEmitter {
|
||||||
|
constructor(uri) {
|
||||||
|
super();
|
||||||
|
|
||||||
function mixin(obj1, obj2) {
|
|
||||||
for(var i in obj2) {
|
|
||||||
if(obj2.hasOwnProperty(i)) {
|
|
||||||
obj1[i] = obj2[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function Client(uri) {
|
|
||||||
EventEmitter.call(this);
|
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.ws = undefined;
|
this.ws = undefined;
|
||||||
this.serverTimeOffset = 0;
|
this.serverTimeOffset = 0;
|
||||||
|
@ -35,41 +26,54 @@ function Client(uri) {
|
||||||
this.noteBuffer = [];
|
this.noteBuffer = [];
|
||||||
this.noteBufferTime = 0;
|
this.noteBufferTime = 0;
|
||||||
this.noteFlushInterval = undefined;
|
this.noteFlushInterval = undefined;
|
||||||
this['🐈'] = 0;
|
this.permissions = {};
|
||||||
|
this["🐈"] = 0;
|
||||||
|
this.loginInfo = undefined;
|
||||||
|
|
||||||
this.bindEventListeners();
|
this.bindEventListeners();
|
||||||
|
|
||||||
this.emit("status", "(Offline mode)");
|
this.emit("status", "(Offline mode)");
|
||||||
};
|
}
|
||||||
|
|
||||||
mixin(Client.prototype, EventEmitter.prototype);
|
isSupported() {
|
||||||
|
|
||||||
Client.prototype.constructor = Client;
|
|
||||||
|
|
||||||
Client.prototype.isSupported = function() {
|
|
||||||
return typeof WebSocket === "function";
|
return typeof WebSocket === "function";
|
||||||
};
|
}
|
||||||
|
|
||||||
Client.prototype.isConnected = function() {
|
isConnected() {
|
||||||
return this.isSupported() && this.ws && this.ws.readyState === WebSocket.OPEN;
|
return (
|
||||||
};
|
this.isSupported() &&
|
||||||
|
this.ws &&
|
||||||
|
this.ws.readyState === WebSocket.OPEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Client.prototype.isConnecting = function() {
|
isConnecting() {
|
||||||
return this.isSupported() && this.ws && this.ws.readyState === WebSocket.CONNECTING;
|
return (
|
||||||
};
|
this.isSupported() &&
|
||||||
|
this.ws &&
|
||||||
|
this.ws.readyState === WebSocket.CONNECTING
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Client.prototype.start = function() {
|
start(enableTokens = true, enableChallenge = true) {
|
||||||
this.canConnect = true;
|
this.canConnect = true;
|
||||||
this.connect();
|
if (!this.connectionTime) {
|
||||||
};
|
this.connect(enableTokens, enableChallenge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Client.prototype.stop = function() {
|
stop() {
|
||||||
this.canConnect = false;
|
this.canConnect = false;
|
||||||
this.ws.close();
|
this.ws.close();
|
||||||
};
|
}
|
||||||
|
|
||||||
Client.prototype.connect = function() {
|
connect(enableTokens, enableChallenge) {
|
||||||
if(!this.canConnect || !this.isSupported() || this.isConnected() || this.isConnecting())
|
if (
|
||||||
|
!this.canConnect ||
|
||||||
|
!this.isSupported() ||
|
||||||
|
this.isConnected() ||
|
||||||
|
this.isConnecting()
|
||||||
|
)
|
||||||
return;
|
return;
|
||||||
this.emit("status", "Connecting...");
|
this.emit("status", "Connecting...");
|
||||||
if (typeof module !== "undefined") {
|
if (typeof module !== "undefined") {
|
||||||
|
@ -100,7 +104,7 @@ Client.prototype.connect = function() {
|
||||||
} else {
|
} else {
|
||||||
++self.connectionAttempts;
|
++self.connectionAttempts;
|
||||||
}
|
}
|
||||||
var ms_lut = [50, 2950, 7000, 10000];
|
var ms_lut = [50, 2500, 10000];
|
||||||
var idx = self.connectionAttempts;
|
var idx = self.connectionAttempts;
|
||||||
if (idx >= ms_lut.length) idx = ms_lut.length - 1;
|
if (idx >= ms_lut.length) idx = ms_lut.length - 1;
|
||||||
var ms = ms_lut[idx];
|
var ms = ms_lut[idx];
|
||||||
|
@ -111,17 +115,20 @@ Client.prototype.connect = function() {
|
||||||
self.ws.close(); // self.ws.emit("close");
|
self.ws.close(); // self.ws.emit("close");
|
||||||
});
|
});
|
||||||
this.ws.addEventListener("open", function (evt) {
|
this.ws.addEventListener("open", function (evt) {
|
||||||
self.connectionTime = Date.now();
|
|
||||||
self.sendArray([{"m": "hi", "🐈": self['🐈']++ || undefined }]);
|
|
||||||
self.pingInterval = setInterval(function () {
|
self.pingInterval = setInterval(function () {
|
||||||
self.sendArray([{m: "t", e: Date.now()}]);
|
self.sendPing();
|
||||||
}, 20000);
|
}, 20000);
|
||||||
//self.sendArray([{m: "t", e: Date.now()}]);
|
|
||||||
self.noteBuffer = [];
|
self.noteBuffer = [];
|
||||||
self.noteBufferTime = 0;
|
self.noteBufferTime = 0;
|
||||||
self.noteFlushInterval = setInterval(function () {
|
self.noteFlushInterval = setInterval(function () {
|
||||||
if (self.noteBufferTime && self.noteBuffer.length > 0) {
|
if (self.noteBufferTime && self.noteBuffer.length > 0) {
|
||||||
self.sendArray([{m: "n", t: self.noteBufferTime + self.serverTimeOffset, n: self.noteBuffer}]);
|
self.sendArray([
|
||||||
|
{
|
||||||
|
m: "n",
|
||||||
|
t: self.noteBufferTime + self.serverTimeOffset,
|
||||||
|
n: self.noteBuffer
|
||||||
|
}
|
||||||
|
]);
|
||||||
self.noteBufferTime = 0;
|
self.noteBufferTime = 0;
|
||||||
self.noteBuffer = [];
|
self.noteBuffer = [];
|
||||||
}
|
}
|
||||||
|
@ -129,24 +136,42 @@ Client.prototype.connect = function() {
|
||||||
|
|
||||||
self.emit("connect");
|
self.emit("connect");
|
||||||
self.emit("status", "Joining channel...");
|
self.emit("status", "Joining channel...");
|
||||||
|
|
||||||
|
if (!enableChallenge) {
|
||||||
|
const token = localStorage.token;
|
||||||
|
var hiMsg = { m: "hi", token };
|
||||||
|
self.sendArray([hiMsg]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.ws.addEventListener("message", function(evt) {
|
this.ws.addEventListener("message", async function (evt) {
|
||||||
var transmission = JSON.parse(evt.data);
|
var transmission = JSON.parse(evt.data);
|
||||||
for (var i = 0; i < transmission.length; i++) {
|
for (var i = 0; i < transmission.length; i++) {
|
||||||
var msg = transmission[i];
|
var msg = transmission[i];
|
||||||
self.emit(msg.m, msg);
|
self.emit(msg.m, msg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
Client.prototype.bindEventListeners = function() {
|
bindEventListeners() {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.on("hi", function (msg) {
|
this.on("hi", function (msg) {
|
||||||
|
self.connectionTime = Date.now();
|
||||||
self.user = msg.u;
|
self.user = msg.u;
|
||||||
self.receiveServerTime(msg.t, msg.e || undefined);
|
self.receiveServerTime(msg.t, msg.e || undefined);
|
||||||
if (self.desiredChannelId) {
|
if (self.desiredChannelId) {
|
||||||
self.setChannel();
|
self.setChannel();
|
||||||
}
|
}
|
||||||
|
if (msg.token) localStorage.token = msg.token;
|
||||||
|
if (msg.permissions) {
|
||||||
|
self.permissions = msg.permissions;
|
||||||
|
} else {
|
||||||
|
self.permissions = {};
|
||||||
|
}
|
||||||
|
if (msg.accountInfo) {
|
||||||
|
self.accountInfo = msg.accountInfo;
|
||||||
|
} else {
|
||||||
|
self.accountInfo = undefined;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.on("t", function (msg) {
|
this.on("t", function (msg) {
|
||||||
self.receiveServerTime(msg.t, msg.e || undefined);
|
self.receiveServerTime(msg.t, msg.e || undefined);
|
||||||
|
@ -164,40 +189,69 @@ Client.prototype.bindEventListeners = function() {
|
||||||
});
|
});
|
||||||
this.on("m", function (msg) {
|
this.on("m", function (msg) {
|
||||||
if (self.ppl.hasOwnProperty(msg.id)) {
|
if (self.ppl.hasOwnProperty(msg.id)) {
|
||||||
self.participantUpdate(msg);
|
self.participantMoveMouse(msg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.on("bye", function (msg) {
|
this.on("bye", function (msg) {
|
||||||
self.removeParticipant(msg.p);
|
self.removeParticipant(msg.p);
|
||||||
});
|
});
|
||||||
};
|
this.on("b", function (msg) {
|
||||||
|
var hiMsg = { m: "hi" };
|
||||||
|
hiMsg["🐈"] = self["🐈"]++ || undefined;
|
||||||
|
if (this.loginInfo) hiMsg.login = this.loginInfo;
|
||||||
|
this.loginInfo = undefined;
|
||||||
|
console.log(msg);
|
||||||
|
if (msg.code) {
|
||||||
|
try {
|
||||||
|
if (msg.code.startsWith("~")) {
|
||||||
|
hiMsg.code = Function(msg.code.substring(1))();
|
||||||
|
} else {
|
||||||
|
hiMsg.code = Function(msg.code)();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
hiMsg.code = "broken";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (localStorage.token) {
|
||||||
|
hiMsg.token = localStorage.token;
|
||||||
|
}
|
||||||
|
self.sendArray([hiMsg]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Client.prototype.send = function(raw) {
|
send(raw) {
|
||||||
if (this.isConnected()) this.ws.send(raw);
|
if (this.isConnected()) this.ws.send(raw);
|
||||||
};
|
}
|
||||||
|
|
||||||
Client.prototype.sendArray = function(arr) {
|
sendArray(arr) {
|
||||||
this.send(JSON.stringify(arr));
|
this.send(JSON.stringify(arr));
|
||||||
};
|
}
|
||||||
|
|
||||||
Client.prototype.setChannel = function(id, set) {
|
setChannel(id, set) {
|
||||||
this.desiredChannelId = id || this.desiredChannelId || "lobby";
|
this.desiredChannelId = id || this.desiredChannelId || "lobby";
|
||||||
this.desiredChannelSettings = set || this.desiredChannelSettings || undefined;
|
this.desiredChannelSettings =
|
||||||
this.sendArray([{m: "ch", _id: this.desiredChannelId, set: this.desiredChannelSettings}]);
|
set || this.desiredChannelSettings || undefined;
|
||||||
};
|
this.sendArray([
|
||||||
|
{
|
||||||
|
m: "ch",
|
||||||
|
_id: this.desiredChannelId,
|
||||||
|
set: this.desiredChannelSettings
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
Client.prototype.offlineChannelSettings = {
|
offlineChannelSettings = {
|
||||||
color: "#ecfaed"
|
color: "#ecfaed"
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.getChannelSetting = function(key) {
|
getChannelSetting(key) {
|
||||||
if (!this.isConnected() || !this.channel || !this.channel.settings) {
|
if (!this.isConnected() || !this.channel || !this.channel.settings) {
|
||||||
return this.offlineChannelSettings[key];
|
return this.offlineChannelSettings[key];
|
||||||
}
|
}
|
||||||
return this.channel.settings[key];
|
return this.channel.settings[key];
|
||||||
};
|
}
|
||||||
|
|
||||||
Client.prototype.setChannelSettings = function(settings) {
|
setChannelSettings(settings) {
|
||||||
if (!this.isConnected() || !this.channel || !this.channel.settings) {
|
if (!this.isConnected() || !this.channel || !this.channel.settings) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -207,19 +261,19 @@ Client.prototype.setChannelSettings = function(settings) {
|
||||||
}
|
}
|
||||||
this.sendArray([{ m: "chset", set: this.desiredChannelSettings }]);
|
this.sendArray([{ m: "chset", set: this.desiredChannelSettings }]);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
Client.prototype.offlineParticipant = {
|
offlineParticipant = {
|
||||||
_id: "",
|
_id: "",
|
||||||
name: "",
|
name: "",
|
||||||
color: "#777"
|
color: "#777"
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.getOwnParticipant = function() {
|
getOwnParticipant() {
|
||||||
return this.findParticipantById(this.participantId);
|
return this.findParticipantById(this.participantId);
|
||||||
};
|
}
|
||||||
|
|
||||||
Client.prototype.setParticipants = function(ppl) {
|
setParticipants(ppl) {
|
||||||
// remove participants who left
|
// remove participants who left
|
||||||
for (var id in this.ppl) {
|
for (var id in this.ppl) {
|
||||||
if (!this.ppl.hasOwnProperty(id)) continue;
|
if (!this.ppl.hasOwnProperty(id)) continue;
|
||||||
|
@ -238,17 +292,17 @@ Client.prototype.setParticipants = function(ppl) {
|
||||||
for (var i = 0; i < ppl.length; i++) {
|
for (var i = 0; i < ppl.length; i++) {
|
||||||
this.participantUpdate(ppl[i]);
|
this.participantUpdate(ppl[i]);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
Client.prototype.countParticipants = function() {
|
countParticipants() {
|
||||||
var count = 0;
|
var count = 0;
|
||||||
for (var i in this.ppl) {
|
for (var i in this.ppl) {
|
||||||
if (this.ppl.hasOwnProperty(i)) ++count;
|
if (this.ppl.hasOwnProperty(i)) ++count;
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
};
|
}
|
||||||
|
|
||||||
Client.prototype.participantUpdate = function(update) {
|
participantUpdate(update) {
|
||||||
var part = this.ppl[update.id] || null;
|
var part = this.ppl[update.id] || null;
|
||||||
if (part === null) {
|
if (part === null) {
|
||||||
part = update;
|
part = update;
|
||||||
|
@ -256,35 +310,53 @@ Client.prototype.participantUpdate = function(update) {
|
||||||
this.emit("participant added", part);
|
this.emit("participant added", part);
|
||||||
this.emit("count", this.countParticipants());
|
this.emit("count", this.countParticipants());
|
||||||
} else {
|
} else {
|
||||||
if(update.x) part.x = update.x;
|
Object.keys(update).forEach(key => {
|
||||||
if(update.y) part.y = update.y;
|
part[key] = update[key];
|
||||||
if(update.color) part.color = update.color;
|
});
|
||||||
if(update.name) part.name = update.name;
|
if (!update.tag) delete part.tag;
|
||||||
|
if (!update.vanished) delete part.vanished;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
Client.prototype.removeParticipant = function(id) {
|
participantMoveMouse(update) {
|
||||||
|
var part = this.ppl[update.id] || null;
|
||||||
|
if (part !== null) {
|
||||||
|
part.x = update.x;
|
||||||
|
part.y = update.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeParticipant(id) {
|
||||||
if (this.ppl.hasOwnProperty(id)) {
|
if (this.ppl.hasOwnProperty(id)) {
|
||||||
var part = this.ppl[id];
|
var part = this.ppl[id];
|
||||||
delete this.ppl[id];
|
delete this.ppl[id];
|
||||||
this.emit("participant removed", part);
|
this.emit("participant removed", part);
|
||||||
this.emit("count", this.countParticipants());
|
this.emit("count", this.countParticipants());
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
Client.prototype.findParticipantById = function(id) {
|
findParticipantById(id) {
|
||||||
return this.ppl[id] || this.offlineParticipant;
|
return this.ppl[id] || this.offlineParticipant;
|
||||||
};
|
}
|
||||||
|
|
||||||
Client.prototype.isOwner = function() {
|
isOwner() {
|
||||||
return this.channel && this.channel.crown && this.channel.crown.participantId === this.participantId;
|
return (
|
||||||
};
|
this.channel &&
|
||||||
|
this.channel.crown &&
|
||||||
|
this.channel.crown.participantId === this.participantId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Client.prototype.preventsPlaying = function() {
|
preventsPlaying() {
|
||||||
return this.isConnected() && !this.isOwner() && this.getChannelSetting("crownsolo") === true;
|
return (
|
||||||
};
|
this.isConnected() &&
|
||||||
|
!this.isOwner() &&
|
||||||
|
this.getChannelSetting("crownsolo") === true &&
|
||||||
|
!this.permissions.playNotesAnywhere
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Client.prototype.receiveServerTime = function(time, echo) {
|
receiveServerTime(time, echo) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var now = Date.now();
|
var now = Date.now();
|
||||||
var target = time - now;
|
var target = time - now;
|
||||||
|
@ -309,27 +381,49 @@ Client.prototype.receiveServerTime = function(time, echo) {
|
||||||
// this.serverTimeOffset = time - now; // mostly time zone offset ... also the lags so todo smoothen this
|
// this.serverTimeOffset = time - now; // mostly time zone offset ... also the lags so todo smoothen this
|
||||||
// not smooth:
|
// not smooth:
|
||||||
// if(echo) this.serverTimeOffset += echo - now; // mostly round trip time offset
|
// if(echo) this.serverTimeOffset += echo - now; // mostly round trip time offset
|
||||||
};
|
}
|
||||||
|
|
||||||
Client.prototype.startNote = function(note, vel) {
|
startNote(note, vel) {
|
||||||
|
if (typeof note !== "string") return;
|
||||||
if (this.isConnected()) {
|
if (this.isConnected()) {
|
||||||
var vel = typeof vel === "undefined" ? undefined : +vel.toFixed(3);
|
var vel = typeof vel === "undefined" ? undefined : +vel.toFixed(3);
|
||||||
if (!this.noteBufferTime) {
|
if (!this.noteBufferTime) {
|
||||||
this.noteBufferTime = Date.now();
|
this.noteBufferTime = Date.now();
|
||||||
this.noteBuffer.push({ n: note, v: vel });
|
this.noteBuffer.push({ n: note, v: vel });
|
||||||
} else {
|
} else {
|
||||||
this.noteBuffer.push({d: Date.now() - this.noteBufferTime, n: note, v: vel});
|
this.noteBuffer.push({
|
||||||
|
d: Date.now() - this.noteBufferTime,
|
||||||
|
n: note,
|
||||||
|
v: vel
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
Client.prototype.stopNote = function(note) {
|
stopNote(note) {
|
||||||
|
if (typeof note !== "string") return;
|
||||||
if (this.isConnected()) {
|
if (this.isConnected()) {
|
||||||
if (!this.noteBufferTime) {
|
if (!this.noteBufferTime) {
|
||||||
this.noteBufferTime = Date.now();
|
this.noteBufferTime = Date.now();
|
||||||
this.noteBuffer.push({ n: note, s: 1 });
|
this.noteBuffer.push({ n: note, s: 1 });
|
||||||
} else {
|
} else {
|
||||||
this.noteBuffer.push({d: Date.now() - this.noteBufferTime, n: note, s: 1});
|
this.noteBuffer.push({
|
||||||
|
d: Date.now() - this.noteBufferTime,
|
||||||
|
n: note,
|
||||||
|
s: 1
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
sendPing() {
|
||||||
|
var msg = { m: "t", e: Date.now() };
|
||||||
|
this.sendArray([msg]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoginInfo(loginInfo) {
|
||||||
|
this.loginInfo = loginInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Client = Client;
|
||||||
|
|
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 220 B |
After Width: | Height: | Size: 149 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 6.4 KiB |
236
index.html
|
@ -3,18 +3,89 @@
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
||||||
<title>Multiplayer Piano</title>
|
<title>Multiplayer Piano</title>
|
||||||
<meta name="description" content="An online piano you can play alone or with others in real-time. MIDI support, 88 keys, velocity sensitive. You can show off your skill or chat while listening to others play."/>
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="An online piano you can play alone or with others in real-time. MIDI support, 88 keys, velocity sensitive. You can show off your skill or chat while listening to others play."
|
||||||
|
/>
|
||||||
<link rel="stylesheet" href="/screen.css" />
|
<link rel="stylesheet" href="/screen.css" />
|
||||||
|
{% if usersConfig.enableTags %}
|
||||||
|
<link rel="stylesheet" href="/tags.css" />
|
||||||
|
{% endif %} {% if config.topButtons == "mppnet" %}
|
||||||
|
<link rel="stylesheet" href="/top-button.css" />
|
||||||
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{% if config.topButtons == "original" %}
|
||||||
<div id="social">
|
<div id="social">
|
||||||
<div id="more-button"></div>
|
<div id="more-button"></div>
|
||||||
</div>
|
</div>
|
||||||
|
{% elif config.topButtons == "mppnet" %}
|
||||||
|
<a
|
||||||
|
href="https://mpp.community/"
|
||||||
|
title="MPPNet Community Forum"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="mppcommunity-button icon-button top-button"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<img src="/mppcommunity.ico" style="vertical-align: middle" />
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://github.com/mppnet/frontend"
|
||||||
|
title="MPPNet Frontend Repo"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="github-button icon-button top-button"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<img src="/github.ico" style="vertical-align: middle" />
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.reddit.com/r/multiplayerpianonet/"
|
||||||
|
title="MPPNet Reddit"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="reddit-button icon-button top-button"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<img src="/reddit.ico" style="vertical-align: middle" />
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://discord.gg/338D2xMufC"
|
||||||
|
title="MPPNet Discord"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="discord-button icon-button top-button"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<img src="/discord.ico" style="vertical-align: middle" />
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://docs.google.com/document/d/1wQvGwQdaI8PuEjSWxKDDThVIoAlCYIxQOyfyi4o6HcM/edit?usp=sharing"
|
||||||
|
title="Multiplayer Piano Rules"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<button class="mpp-rules-button top-button" aria-hidden="true">
|
||||||
|
Rules
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div id="chat">
|
<div id="chat">
|
||||||
<ul></ul>
|
<ul></ul>
|
||||||
<input placeholder="You can chat with this thing." class="translate" maxlength="512"/>
|
<input
|
||||||
|
placeholder="You can chat with this thing."
|
||||||
|
class="translate"
|
||||||
|
maxlength="512"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="room-notice"></div>
|
<div id="room-notice"></div>
|
||||||
|
@ -28,13 +99,23 @@
|
||||||
<noscript>
|
<noscript>
|
||||||
<center>
|
<center>
|
||||||
<p>
|
<p>
|
||||||
Multiplayer Piano is an online, full 88-key piano you can play alone or with others in real-time. Plug up your MIDI keyboard, MIDI in and out are supported. You should be able to hear some seriously talented piano players performing here! Join in or just chat and listen.
|
Multiplayer Piano is an online, full 88-key piano you can
|
||||||
|
play alone or with others in real-time. Plug up your MIDI
|
||||||
|
keyboard, MIDI in and out are supported. You should be able
|
||||||
|
to hear some seriously talented piano players performing
|
||||||
|
here! Join in or just chat and listen.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
For good performance, Chrome is highly recommended. Firefox also supports the requisite Web Audio API, but performance may not be as good. Chrome has Web MIDI.
|
For good performance, Chrome is highly recommended. Firefox
|
||||||
|
also supports the requisite Web Audio API, but performance
|
||||||
|
may not be as good. Chrome has Web MIDI.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Of course, first you need to <a href="http://www.enable-javascript.com/" class="link">Enable Javascript</a> or it won't do anything...!
|
Of course, first you need to
|
||||||
|
<a href="http://www.enable-javascript.com/" class="link"
|
||||||
|
>Enable Javascript</a
|
||||||
|
>
|
||||||
|
or it won't do anything...!
|
||||||
</p>
|
</p>
|
||||||
</center>
|
</center>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
@ -48,16 +129,32 @@
|
||||||
<div class="new translate">New Room...</div>
|
<div class="new translate">New Room...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="new-room-btn" class="ugly-button translate">New Room...</div>
|
<div id="new-room-btn" class="ugly-button translate">
|
||||||
|
New Room...
|
||||||
|
</div>
|
||||||
<div id="play-alone-btn" class="ugly-button">Play Alone</div>
|
<div id="play-alone-btn" class="ugly-button">Play Alone</div>
|
||||||
<div id="room-settings-btn" class="ugly-button">Room Settings</div>
|
<div id="room-settings-btn" class="ugly-button">
|
||||||
<div id="midi-btn" class="ugly-button translate">MIDI In/Out</div>
|
Room Settings
|
||||||
<div id="record-btn" class="ugly-button translate">Record MP3</div>
|
</div>
|
||||||
|
<div id="midi-btn" class="ugly-button translate">
|
||||||
|
MIDI In/Out
|
||||||
|
</div>
|
||||||
|
<div id="record-btn" class="ugly-button translate">
|
||||||
|
Record MP3
|
||||||
|
</div>
|
||||||
<div id="synth-btn" class="ugly-button translate">Synth</div>
|
<div id="synth-btn" class="ugly-button translate">Synth</div>
|
||||||
<div id="sound-btn" class="ugly-button sound-btn">Sound Select</div>
|
<div id="sound-btn" class="ugly-button sound-btn">
|
||||||
|
Sound Select
|
||||||
|
</div>
|
||||||
<div id="status"></div>
|
<div id="status"></div>
|
||||||
<div id="volume">
|
<div id="volume">
|
||||||
<input type="range" id="volume-slider" min="0.0" max="1.0" step="0.01">
|
<input
|
||||||
|
type="range"
|
||||||
|
id="volume-slider"
|
||||||
|
min="0.0"
|
||||||
|
max="1.0"
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div id="volume-label">volume</div>
|
<div id="volume-label">volume</div>
|
||||||
<div id="quota">
|
<div id="quota">
|
||||||
|
@ -70,30 +167,121 @@
|
||||||
<div class="bg"></div>
|
<div class="bg"></div>
|
||||||
<div id="modals">
|
<div id="modals">
|
||||||
<div id="sound-warning" class="dialog">
|
<div id="sound-warning" class="dialog">
|
||||||
<p>This site makes a lot of sound! You may want to adjust the volume before continuing.</p>
|
<p>
|
||||||
|
This site makes a lot of sound! You may want to adjust
|
||||||
|
the volume before continuing.
|
||||||
|
</p>
|
||||||
<button class="submit">PLAY</button>
|
<button class="submit">PLAY</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="new-room" class="dialog">
|
<div id="new-room" class="dialog">
|
||||||
<p><input type="text" name="name" placeholder="room name" class="text translate" maxlength="512"/></p>
|
<p>
|
||||||
<p><label><input type="checkbox" name="visible" class="checkbox translate" checked>Visible (open to everyone)</label></p>
|
<input
|
||||||
</label></p>
|
type="text"
|
||||||
|
name="name"
|
||||||
|
placeholder="room name"
|
||||||
|
class="text translate"
|
||||||
|
maxlength="512"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
name="visible"
|
||||||
|
class="checkbox translate"
|
||||||
|
checked
|
||||||
|
/>Visible (open to everyone)</label
|
||||||
|
>
|
||||||
|
</p>
|
||||||
<button class="submit">go</button>
|
<button class="submit">go</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="room-settings" class="dialog">
|
<div id="room-settings" class="dialog">
|
||||||
<p><div class="ugly-button drop-crown">Drop crown</div></p>
|
<div class="ugly-button drop-crown">Drop crown</div>
|
||||||
<p><label><input type="checkbox" name="visible" class="checkbox translate" checked>Visible (open to everyone)</label></p>
|
<p>
|
||||||
</label></p>
|
<label
|
||||||
<p><label><input type="checkbox" name="chat" class="checkbox translate" checked>Enable Chat</label></p>
|
><input
|
||||||
<p><label><input type="checkbox" name="crownsolo" class="checkbox">Only Owner can Play<button class="submit">APPLY</button>
|
type="checkbox"
|
||||||
<p><label>Background color: <input type="color" name="color" placeholder="" maxlength="7" class="color"></label></p>
|
name="visible"
|
||||||
|
class="checkbox translate"
|
||||||
|
checked
|
||||||
|
/>Visible (open to everyone)</label
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
name="chat"
|
||||||
|
class="checkbox translate"
|
||||||
|
checked
|
||||||
|
/>Enable Chat</label
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
name="crownsolo"
|
||||||
|
class="checkbox"
|
||||||
|
/>Only Owner can Play</label
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<button class="submit">APPLY</button>
|
||||||
|
<p>
|
||||||
|
<label
|
||||||
|
>Background color: <input
|
||||||
|
type="color"
|
||||||
|
name="color"
|
||||||
|
placeholder=""
|
||||||
|
maxlength="7"
|
||||||
|
class="color"
|
||||||
|
/></label>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="rename" class="dialog">
|
<div id="rename" class="dialog">
|
||||||
<p><input type="text" name="name" placeholder="My Fancy New Name" maxlength="40" class="text"/></p>
|
<p>
|
||||||
<!--<p><input type="color" name="color" placeholder="" maxlength="7" class="color"/></p>-->
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
placeholder="My Fancy New Name"
|
||||||
|
maxlength="40"
|
||||||
|
class="text"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
{% if usersConfig.enableColorChanging %}
|
||||||
|
<p>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
name="color"
|
||||||
|
placeholder=""
|
||||||
|
maxlength="7"
|
||||||
|
class="color"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
<button class="submit">USER SET</button>
|
<button class="submit">USER SET</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="more">
|
||||||
|
<div class="header"></div>
|
||||||
|
<div class="items"></div>
|
||||||
|
<div class="footer">
|
||||||
|
Keep looking here for the eventual appearance of a blog,
|
||||||
|
help guide, etc. Also there are unofficial
|
||||||
|
(community-run)
|
||||||
|
<a href="http://www.reddit.com/r/multiplayerpiano"
|
||||||
|
>subreddit</a
|
||||||
|
>
|
||||||
|
and
|
||||||
|
<a href="http://www.youtube.com/MultiplayerPiano"
|
||||||
|
>youtube</a
|
||||||
|
>
|
||||||
|
pages. Thanks!
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="config" style="display: none">{{base64config}}</div>
|
||||||
|
|
||||||
<script src="/jquery.min.js"></script>
|
<script src="/jquery.min.js"></script>
|
||||||
<script src="/util.js"></script>
|
<script src="/util.js"></script>
|
||||||
|
|
After Width: | Height: | Size: 11 KiB |
|
@ -0,0 +1,150 @@
|
||||||
|
<div class="items">
|
||||||
|
<div class="item">
|
||||||
|
<div class="content">
|
||||||
|
<p>Multiplayer Piano is available in the Chrome Web Store.</p>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://chrome.google.com/webstore/detail/multiplayer-piano/cbadoggeokhliehfonkefnfcbgocojid"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<img src="ChromeWebStore_BadgeWBorder_v2_206x58.png" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
A
|
||||||
|
<a
|
||||||
|
href="https://chrome.google.com/webstore/detail/multiplayer-piano/cbadoggeokhliehfonkefnfcbgocojid/reviews"
|
||||||
|
target="_blank"
|
||||||
|
>review</a
|
||||||
|
>
|
||||||
|
is certainly a helpful thing; we need more of those.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<div class="content">
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://www.facebook.com/MultiplayerPiano"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<img src="FB_FindUsOnFacebook-144.png" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
There is a
|
||||||
|
<a
|
||||||
|
href="https://www.facebook.com/MultiplayerPiano"
|
||||||
|
target="_blank"
|
||||||
|
>Facebook page</a
|
||||||
|
>
|
||||||
|
for the site. Click 'Like' to show your support, learn about
|
||||||
|
updates, and discuss ideas.
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="fb-like"
|
||||||
|
data-href="http://www.facebook.com/MultiplayerPiano"
|
||||||
|
data-send="false"
|
||||||
|
data-layout="button_count"
|
||||||
|
data-width="300"
|
||||||
|
data-show-faces="false"
|
||||||
|
></div>
|
||||||
|
<p>
|
||||||
|
You could also
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onclick="window.open('https://www.facebook.com/sharer/sharer.php?u='+encodeURIComponent(location.href),'facebook-share-dialog','width=626,height=436');return false;"
|
||||||
|
>share your current room</a
|
||||||
|
>
|
||||||
|
with your friends.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<div class="content">
|
||||||
|
<p>You can share using Twitter, too!</p>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://twitter.com/share"
|
||||||
|
class="twitter-share-button"
|
||||||
|
data-url="http://www.multiplayerpiano.com"
|
||||||
|
data-text="Multiplayer Piano"
|
||||||
|
data-via="MultiplayrPiano"
|
||||||
|
target="_blank"
|
||||||
|
>Tweet</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Follow
|
||||||
|
<a href="http://twitter.com/MultiplayrPiano" target="_blank"
|
||||||
|
>@MultiplayrPiano</a
|
||||||
|
>
|
||||||
|
for updates.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<div class="content">
|
||||||
|
<p>Hey this rectangle is completely empty!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<div class="content">
|
||||||
|
<p>
|
||||||
|
Want to donate money to help support the site? That's amazing!
|
||||||
|
You could donate using PayPal and it would help immensely!
|
||||||
|
</p>
|
||||||
|
<form
|
||||||
|
class="text-small"
|
||||||
|
action="https://www.paypal.com/cgi-bin/webscr"
|
||||||
|
method="post"
|
||||||
|
target="_top"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="cmd" value="_s-xclick" />
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="hosted_button_id"
|
||||||
|
value="6YDYHR6UX6QSY"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="image"
|
||||||
|
src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif"
|
||||||
|
border="0"
|
||||||
|
name="submit"
|
||||||
|
alt="PayPal - The safer, easier way to pay online!"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<p>
|
||||||
|
<a href="bitcoin:15g16JDpYRvWjNKCkYaVnu2FAHCmmaLvGu"
|
||||||
|
>Bitcoins</a
|
||||||
|
>
|
||||||
|
can be money, too!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<div class="content">
|
||||||
|
<p>
|
||||||
|
My name is Brandon. You can send an email to ask a question or
|
||||||
|
say hello!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
id="email"
|
||||||
|
href="#"
|
||||||
|
obscured="zhygvcynlrecvnab.pbz@tznvy.pbz"
|
||||||
|
>
|
||||||
|
<img src="email.png" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you encrypt your message using
|
||||||
|
<a
|
||||||
|
href="http://pgp.mit.edu:11371/pks/lookup?op=get&search=0x38202C5F4E0C0715"
|
||||||
|
target="_blank"
|
||||||
|
>my public key</a
|
||||||
|
>, I will think you're awesome automatically!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 2.4 KiB |
1320
screen.css
After Width: | Height: | Size: 164 B |
2
sounds
|
@ -1 +1 @@
|
||||||
Subproject commit 81033e1069557150bf10a1dd130d8ca02965b47b
|
Subproject commit 26ec744c86f0c77f20d883d7aeb2bde9397a0d2a
|
|
@ -0,0 +1,31 @@
|
||||||
|
#names .nametag {
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
padding: 4px;
|
||||||
|
margin-right: 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
-webkit-border-radius: 2px;
|
||||||
|
-moz-border-radius: 2px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#names .nametag {
|
||||||
|
z-index: 301;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor .name .curtag {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 7px;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-left: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
padding: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
.top-button {
|
||||||
|
height: 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #111;
|
||||||
|
border: 1px solid #444;
|
||||||
|
padding: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 12px;
|
||||||
|
border-radius: 2px;
|
||||||
|
-webkit-border-radius: 2px;
|
||||||
|
-moz-border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-button:hover {
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-button.stuck {
|
||||||
|
background: rgba(204, 187, 170, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button img {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
position: fixed;
|
||||||
|
top: 6px;
|
||||||
|
z-index: 200;
|
||||||
|
width: 26px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mppcommunity-button {
|
||||||
|
right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-button {
|
||||||
|
right: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-button {
|
||||||
|
right: 66px;
|
||||||
|
background-color: white;
|
||||||
|
filter: invert(100%);
|
||||||
|
border-color: #c6c6c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-button:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reddit-button {
|
||||||
|
right: 96px;
|
||||||
|
z-index: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mpp-rules-button {
|
||||||
|
position: fixed;
|
||||||
|
right: 6px;
|
||||||
|
top: 32px;
|
||||||
|
z-index: 200;
|
||||||
|
}
|