Working setup

main v1.2.13
Benjamin Kraft 2 years ago
commit c4c299f701
  1. 1
      .gitignore
  2. 6
      project.json
  3. 55
      public/changelog.txt
  4. BIN
      public/data/images/favicon.ico
  5. 53
      public/data/lib/benjocraeft/collision.js
  6. 337
      public/data/lib/benjocraeft/colorPicker.js
  7. 29
      public/data/lib/benjocraeft/cookie.js
  8. 30
      public/data/lib/benjocraeft/loader.js
  9. 30
      public/data/lib/benjocraeft/prototypes.js
  10. 48
      public/data/lib/benjocraeft/technical.js
  11. 395
      public/data/scripts/drawer.js
  12. 81
      public/data/scripts/events.js
  13. 125
      public/data/scripts/init.js
  14. 67
      public/data/scripts/minimap.js
  15. 82
      public/data/scripts/online.js
  16. 3
      public/data/settings/get_port.php
  17. 8
      public/data/settings/libraries.json
  18. 22
      public/data/settings/settings.json
  19. 76
      public/data/styles/color_picker.css
  20. BIN
      public/data/styles/font.ttf
  21. 86
      public/data/styles/range_input.css
  22. 109
      public/index.html
  23. 226
      public/styles.css
  24. BIN
      public/thumbnail.png
  25. 5
      server/.gitignore
  26. 424
      server/package-lock.json
  27. 17
      server/package.json
  28. 169
      server/src/client.ts
  29. 20
      server/src/definitions/serialized.d.ts
  30. 29
      server/src/definitions/settings.d.ts
  31. 38
      server/src/game_standard.ts
  32. 149
      server/src/global_draw.ts
  33. 7
      server/src/index.ts
  34. 105
      server/src/logger.ts
  35. 149
      server/src/manager.ts
  36. 136
      server/src/room.ts
  37. 39
      server/src/start.ts
  38. 11
      server/tsconfig.json

1
.gitignore vendored

@ -0,0 +1 @@
.idea

@ -0,0 +1,6 @@
{
"display_name": "Global draw sheet",
"info_text": "A draw sheet for everyone.",
"visible": true,
"tags": ["Tool", "Multiplayer"]
}

@ -0,0 +1,55 @@
1.0.0 > - Started versioning
- Contents:
- ColorPicker
- Color pick from screen
- Thickness range input
- Thickness preview
- Minimap
- Two Modes:
- Draw Freely (lines)
- Pixel Art
- Disabled browser scrolling, added own scrolling via mousewheel
1.0.1 > - Made changelog.txt public
1.1.0 > - Changed Graphics resolution from 8000/8000 to 5000/5000
- Changed pixel count for pixel art mode from 250 to 500
- Added Skip Button for first drawing
- Inverted progress bar :D
- Added Button to request server save of drawings
1.1.1 > - Enabled button usability feedback
1.2.0 > - Disabled grid for pixel art
- Added hotkey for new alpha "grid"
1.2.1 > - Changed pixel count for pixel art mode from 500 to 1000
- Activated anti aliasing for free mode, deactivated for pixel mode
- Color copy ignores alpha grid
1.2.2 > - Seperated settings for free mode and pixel mode
- Added hotkey set for pixel grid (defaults to 'G')
- Hotkey for pixel grid loads from session data
1.2.3 > - ColorPicker displays a list of recently used colors
- Recently used colors are also loaded from site session data
1.2.4 > - Recently used color defaults to white instead of black
1.2.5 > - Made hotkey feature more understandable -_-
1.2.6 > - 3-digit hex values for ColorPicker instantly convert to 6-digit values
1.2.7 > - Enabled moving also with w,a,s,d
1.2.8 > - Updated downloadable zip with newest contents
1.2.9 > - Better Loading screen and bug fixes
1.2.10 > - Nicer selection rectangle borders on minimap
1.2.11 > - Zooming via mousewheel works much better now and is relative to mouse position
1.2.12 > - Enabled user feedback :)
1.2.13 > - Loading works much faster now!!!

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

@ -0,0 +1,53 @@
'use strict';
/*
2D Collision detection and performing
*/
class Collision{
static ellipseToEllipse(e1, e2){
//Colliding angle of ball 1 to ball 2 using arc tan of both x and y differences
let collisionAngle = Math.atan2((e2.pos.y - e1.pos.y), (e2.pos.x - e1.pos.x));
//Converting directions of velocity vector of balls into angles
let d1 = Math.atan2(e1.vel.y, e1.vel.x);
let d2 = Math.atan2(e2.vel.y, e2.vel.x);
//Ignoring mass effects new velocites are simply magnitude multiplied with value of angle differences
let newXspeed1 = e1.vel.mag() * Math.cos(d1 - collisionAngle);
let newYspeed1 = e1.vel.mag() * Math.sin(d1 - collisionAngle);
let newXspeed2 = e2.vel.mag() * Math.cos(d2 - collisionAngle);
let newYspeed2 = e2.vel.mag() * Math.sin(d2 - collisionAngle);
//According to the principle of linear momentum, kinetic energy stays the same after collision, so velocities are now related to masses
let finalXspeed1 = ((e1.radius - e2.radius) * newXspeed1 + e2.radius * 2 * newXspeed2) / (e1.radius + e2.radius);
let finalYspeed1 = newYspeed1;
let finalXspeed2 = (e1.radius * 2 * newXspeed1 + (e2.radius - e1.radius) * newXspeed2) / (e1.radius + e2.radius);
let finalYspeed2 = newYspeed2;
//Values of collisionAngle
let cosAngle = Math.cos(collisionAngle);
let sinAngle = Math.sin(collisionAngle);
//To also keep velocites relative to pure collisionAngle, subtract sin*x from cos*x and add sin*y to cos*y because coordSystem has y = 0 on the top
let u1x = cosAngle * finalXspeed1 - sinAngle * finalYspeed1;
let u1y = sinAngle * finalXspeed1 + cosAngle * finalYspeed1;
let u2x = cosAngle * finalXspeed2 - sinAngle * finalYspeed2;
let u2y = sinAngle * finalXspeed2 + cosAngle * finalYspeed2;
//Set new velocities to both balls
e1.vel.x = u1x;
e1.vel.y = u1y;
e2.vel.x = u2x;
e2.vel.y = u2y;
//Move balls one vx/vy forward to avoid double inverting collision detection
e1.pos.x += e1.vel.x;
e1.pos.y += e1.vel.y;
e2.pos.x += e2.vel.x;
e2.pos.y += e2.vel.y;
}
}

@ -0,0 +1,337 @@
class ColorPicker {
constructor(){
this.movingObject = "";
}
updateFromGraphical(){
this.h = (1 - $("#hue_picker").position().top / $("#hue").height()) * 360;
this.s = ($("#sb_picker").position().left + 8) / $("#saturation").width() * 100;
this.v = (1 - ($("#sb_picker").position().top + 8) / $("#value").height()) * 100;
this.r = HSVtoRGB(this.h, this.s, this.v).r;
this.g = HSVtoRGB(this.h, this.s, this.v).g;
this.b = HSVtoRGB(this.h, this.s, this.v).b;
this.hex = RGBtoHEX(this.r, this.g, this.b);
this.updateInterface();
}
updateFromHSV(){
this.h = $($("#color_picker_hsv input")[0]).val();
this.s = $($("#color_picker_hsv input")[1]).val();
this.v = $($("#color_picker_hsv input")[2]).val();
this.r = HSVtoRGB(this.h, this.s, this.v).r;
this.g = HSVtoRGB(this.h, this.s, this.v).g;
this.b = HSVtoRGB(this.h, this.s, this.v).b;
this.hex = RGBtoHEX(this.r, this.g, this.b);
this.updateFromHEX(null, true);
this.updateInterface();
}
updateFromRGB(rgba){
if (rgba){
this.r = rgba[0];
this.g = rgba[1];
this.b = rgba[2];
} else {
this.r = $($("#color_picker_rgb input")[0]).val();
this.g = $($("#color_picker_rgb input")[1]).val();
this.b = $($("#color_picker_rgb input")[2]).val();
}
this.h = RGBtoHSV(this.r, this.g, this.b).h;
this.s = RGBtoHSV(this.r, this.g, this.b).s;
this.v = RGBtoHSV(this.r, this.g, this.b).v;
this.hex = RGBtoHEX(this.r, this.g, this.b);
this.updateFromHEX(null, true);
this.updateInterface();
}
updateFromHEX(input, otf){
if (!otf){ //Not on the fly
if ($(input).val().isValidHEX()) this.hex = $(input).val();
else {
alert("Error!");
return;
}
}
this.r = HEXtoRGB(this.hex).r;
this.g = HEXtoRGB(this.hex).g;
this.b = HEXtoRGB(this.hex).b;
this.h = RGBtoHSV(this.r, this.g, this.b).h;
this.s = RGBtoHSV(this.r, this.g, this.b).s;
this.v = RGBtoHSV(this.r, this.g, this.b).v;
this.hex = RGBtoHEX(this.r, this.g, this.b);
this.updateInterface();
}
setFromUsed(used){
let usedColorsData = p.getItem('usedColors');
let index = $('.used_color').toArray().reverse().indexOf(used);
let color = usedColorsData[index];
this.hex = color;
this.updateFromHEX(null, true);
}
setUsed(color){
this.lastSetUsedColor = color;
let usedColors = $('.used_color').toArray().reverse();
let usedColorsData = p.getItem('usedColors');
if (usedColorsData.includes(color)){
let index = usedColorsData.indexOf(color);
let swap = usedColorsData[index - 1];
if (swap){
usedColorsData[index - 1] = color;
usedColorsData[index] = swap;
}
} else {
set(color, 0);
function set(c, i){
let oldColor = usedColorsData[i];
usedColorsData[i] = c;
if (usedColorsData.length > i + 1)
set(oldColor, i + 1);
}
}
for (let c of usedColorsData){
let dom = usedColors[usedColorsData.indexOf(c)];
$(dom).css('background-color', c);
}
//p.storeItem('usedColors', usedColorsData);
}
updateInterface(){
let r = $($("#color_picker_rgb input")[0]),
g = $($("#color_picker_rgb input")[1]),
b = $($("#color_picker_rgb input")[2]),
h = $($("#color_picker_hsv input")[0]),
s = $($("#color_picker_hsv input")[1]),
v = $($("#color_picker_hsv input")[2]),
hex = $("#color_picker_hex"),
bgColor;
r.val(round(this.r));
g.val(round(this.g));
b.val(round(this.b));
h.val(round(this.h));
s.val(round(this.s));
v.val(round(this.v));
bgColor = color(this.r, 0, 0);
r.css({
"background-color": bgColor.toString(),
"color": fontColor(bgColor)
});
bgColor = color(0, this.g, 0);
g.css({
"background-color": bgColor.toString(),
"color": fontColor(bgColor)
});
bgColor = color(0, 0, this.b);
b.css({
"background-color": bgColor.toString(),
"color": fontColor(bgColor)
});
colorMode(HSL);
bgColor = color(this.h, 100, 50);
h.css({
"background-color": bgColor.toString(),
"color": fontColor(bgColor)
});
bgColor = color(this.h, this.s, 100 - this.s / 2);
s.css({
"background-color": bgColor.toString(),
"color": fontColor(bgColor)
});
bgColor = color(this.h, 100, this.v / 2);
v.css({
"background-color": bgColor.toString(),
"color": fontColor(bgColor)
});
colorMode(RGB);
hex.val(this.hex);
hex.css({
"background-color": this.hex,
"color": fontColor(color(this.hex))
});
let sRGB = HSVtoRGB(this.h, 100, 100);
let saturationBackground = "linear-gradient(to right, #FFF 0%, rgb("
+ sRGB.r + ","
+ sRGB.g + ","
+ sRGB.b + ") 100%)";
$("#hue_picker").css("top", (1 - this.h / 360) * $("#hue").height());
$("#sb_picker").css({
"left": this.s / 100 * $("#saturation").width() - 8,
"top": (1 - this.v / 100) * $("#value").height() - 8
});
$("#saturation").css("background", saturationBackground);
$("#thickness_preview").css("background-color", this.hex);
}
mousePressed(){
let x = p.winMouseX - $("#saturation").offset().left;
let y = p.winMouseY - $("#value").offset().top;
if (x > 0 && x < $("#saturation").width() && y > 0 && y < $("#value").height()){
this.movingObject = "sb";
this.mouseDragged();
}
if (x > $("#saturation").width() + 6 && x < $("#saturation").width() + 6 + $("#hue").width() && y > 0 && y < $("#hue").height()){
this.movingObject = "hue";
this.mouseDragged();
}
}
mouseDragged(){
if (this.movingObject == "hue"){
let objH = $("#hue");
let picker = $("#hue_picker");
let h = p.winMouseY - objH.offset().top;
if (h > 0 && h < objH.height()){
picker.css("top", h - 1);
} else if (h > objH.height()){
picker.css("top", objH.height() - 1);
} else if (h < 0){
picker.css("top", -1);
}
this.updateFromGraphical();
}
if (this.movingObject == "sb"){
let objS = $("#saturation");
let objV = $("#value");
let picker = $("#sb_picker");
let s = p.winMouseX - objS.offset().left;
let v = p.winMouseY - objV.offset().top;
if (s > 0 && s < objS.width()){
picker.css("left", s - 8);
} else if (s < 0){
picker.css("left", -8);
} else if (s < objS.width()){
picker.css("left", objS.width() - 8);
}
if (v > 0 && v < objV.height()){
picker.css("top", v - 8);
} else if (v < 0){
picker.css("top", -8);
} else if (v > objV.height()){
picker.css("top", objV.height() - 8);
}
this.updateFromGraphical();
}
}
mouseReleased(){
this.movingObject = "";
}
getColor(){
return this.hex;
}
}
function fontColor(bg){
//http://www.nbdtech.com/Blog/archive/2008/04/27/Calculating-the-Perceived-Brightness-of-a-Color.aspx
let o = (red(bg) * 299 + green(bg) * 587 + blue(bg) * 114) / 1000;
return (o > 125) ? "#000" : "#CCC";
}
//www.stackoverflow.com -->
function RGBtoHEX(r, g, b) {
let rgb = b | (g << 8) | (r << 16);
return '#' + (0x1000000 + rgb).toString(16).slice(1);
}
function HEXtoRGB(hex) {
let shorthandRegex = /^#?([a-fA-F\d])([a-fA-F\d])([a-fA-F\d])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
return r + r + g + g + b + b;
});
let result = /^#?([a-fA-F\d]{2})([a-fA-F\d]{2})([a-fA-F\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function HSVtoRGB(h, s, v) {
let r, g, b, i, f, p, q, t;
if (arguments.length === 1) {
s = h.s, v = h.v, h = h.h;
}
h /= 360;
s /= 100;
v /= 100;
i = Math.floor(h * 6);
f = h * 6 - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
return {
r: r * 255,
g: g * 255,
b: b * 255
};
}
function RGBtoHSV(r, g, b) {
if (arguments.length === 1) {
g = r.g, b = r.b, r = r.r;
}
let max = Math.max(r, g, b), min = Math.min(r, g, b),
d = max - min,
h,
s = (max === 0 ? 0 : d / max),
v = max / 255;
switch (max) {
case min: h = 0; break;
case r: h = (g - b) + d * (g < b ? 6: 0); h /= 6 * d; break;
case g: h = (b - r) + d * 2; h /= 6 * d; break;
case b: h = (r - g) + d * 4; h /= 6 * d; break;
}
return {
h: h * 360,
s: s * 100,
v: v * 100
};
}
let round = (val) => p.round(val);
let color = (val) => p.color(val);
let red = (val) => p.red(val);
let green = (val) => p.green(val);
let blue = (val) => p.blue(val);
let colorMode = (val) => p.colorMode(val);
let HSL = p.HSL;
let RGB = p.RGB;

@ -0,0 +1,29 @@
function setCookie(name, value, years){
let expires = "";
if (years){
let date = new Date();
date.setTime(date.getTime() + (years * 365 * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + value + expires + "; path=/";
}
function getCookie(name){
let nameEQ = name + "=";
let ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++){
let c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
function deleteCookies(){
for (let i = 0; i < arguments.length; i++) setCookie(arguments[i], "", -1);
}
function deleteAllCookies(){
let cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) deleteCookies(cookies[i].split("=")[0]);
}

@ -0,0 +1,30 @@
class Loader{
constructor(dom){
this.dim = createVector($(dom).width(), $(dom).height());
this.c = createGraphics(this.dim.x, this.dim.y);
this.c.parent(dom);
this.radius = min(this.dim.x, this.dim.y) * 0.4;
this.center = createVector(this.dim.x / 2, this.dim.y / 2);
$(dom).find('canvas').show();
this.angle = 0;
}
update(){
this.angle += PI / 10;
}
display(){
let c = this.c;
c.clear();
c.noFill();
c.stroke(0);
c.strokeWeight(5);
c.arc(this.center.x, this.center.y, this.radius * 2, this.radius * 2, this.angle, this.angle + PI + HALF_PI);
}
destroy(){
this.c.remove();
}
}

@ -0,0 +1,30 @@
Array.prototype.shuffle = function(){
let currentIndex = this.length, temporaryValue, randomIndex;
while (0 != currentIndex){
randomIndex = floor(random() * currentIndex);
currentIndex -= 1;
temporaryValue = this[currentIndex];
this[currentIndex] = this[randomIndex];
this[randomIndex] = temporaryValue;
}
}
Array.prototype.copy = function(){
return this.slice(0);
};
//Divides big Array into big multidimensional Array
Array.prototype.partitiate = function(dimensions){
if (!dimensions) return this;
let parts = [];
while(this.length) parts.push(this.splice(0, round(pow(this.length, 1 / (1 + 1 / dimensions)))).partitiate(dimensions - 1));
return parts;
}
String.prototype.isValidHEX = function(){
return /(^#[0-9A-Fa-f]{6}$)|(^#[0-9A-Fa-f]{3}$)/i.test(this);
}
String.prototype.capitalize = function(){
return this.charAt(0).toUpperCase() + this.slice(1);
}

@ -0,0 +1,48 @@
function toTimeString(time, hoursWanted){
time = floor(time / 10);
let hs = String(floor(time % 100));
let fs = String(floor((time / 100) % 60));
if (hoursWanted){
let min = String(floor(((time / 100) / 60) % 60));
let hr = String(floor(((time / 100) / 60) / 60));
if (hs.length < 2) hs = "0" + hs;
if (fs.length < 2) fs = "0" + fs;
if (min.length < 2) min = "0" + min;
if (hr.length < 2) hr = "0" + hr;
return hr + ":" + min + ":" + fs + ":" + hs;
} else {
let min = String(floor(((time / 100) / 60) % 60));
if (hs.length < 2) hs = "0" + hs;
if (fs.length < 2) fs = "0" + fs;
if (min.length < 2) min = "0" + min;
return min + ":" + fs + ":" + hs;
}
}
function debugInformation(x, y){
push();
textSize(12);
textStyle(NORMAL);
stroke(255);
strokeWeight(1);
fill(255);
text("FPS : " + round(frameRate()), 10 + x, 10 + textAscent("FPS : ") + y);
text("MouseX : " + round(mouseX + x), 10 + x, 10 + textAscent("FPS : ") + 10 + textAscent("MouseX : ") + y);
text("MouseY : " + round(-mouseY - y), 10 + x, 10 + textAscent("FPS : ") + 10 + textAscent("MouseX : ") + 10 + textAscent("MouseY : ") + y);
pop();
}
function ranBool(chance){
if (!chance) chance = 2;
return Math.floor(Math.random() * chance + 1) == chance ? true : false;
}

@ -0,0 +1,395 @@
class Drawer{
constructor(){
this.dim = {x: 5000, y: 5000};
this.viewport = {
x: (this.dim.x - p.width) / 2,
y: (this.dim.y - p.height) / 2,
maxX: this.dim.x,
maxY: this.dim.y,
scroll: 10,
zoom: 1,
zoomV: 2
};
this.oldMouseX = this.mouseX;
this.oldMouseY = this.mouseY;
this.map = new MiniMap($("#minimap"), "#minimap > canvas", true);
//Free draw
this.lines = [];
this.linesToDraw = [];
this.linesImage = p.createGraphics(this.dim.x, this.dim.y);
this.linesImage.background(255);
//Pixel art
this.pixels = [];
this.pixelsToFill = [];
this.gridPixelsToFill = [];
this.gridActive = false;
this.pixelsImage = p.createGraphics(this.dim.x, this.dim.y);
this.pixelsImage.background(255);
this.gridImage = p.createGraphics(this.dim.x, this.dim.y);
this.gridImage.background(255, 0);
this.gridImage.noStroke();
this.gridImage.fill(0, 255 * 0.1);
this.pixelCount = {x: 1000, y: 1000};
this.pixelSize = {x: this.dim.x / this.pixelCount.x, y: this.dim.y / this.pixelCount.y};
for (let x = 0; x < this.pixelCount.x; x++){
let column = [];
for (let y = 0; y < this.pixelCount.y; y++){
let pixel = {x: x, y: y, c: "#FFFFFF"};
column.push(pixel);
if (x % 2 == 0 && (y + 1) % 2 == 0
|| y % 2 == 0 && (x + 1) % 2 == 0)
this.gridPixelsToFill.push(pixel);
}
this.pixels.push(column);
}
this.isCopying = false;
this.drawType = null;
this.thickness = parseInt($("#thickness").val());
this.pixelsMap = new MiniMap($("#pixels_map_holder"), "#pixels_map_holder > canvas", false);
this.linesMap = new MiniMap($("#lines_map_holder"), "#lines_map_holder > canvas", false);
}
get image(){
if (this.type === 'free') return this.linesImage;
if (this.type === 'pixel') return this.pixelsImage;
}
get scrollSpeed(){
return this.viewport.scroll;
}
get mouseX(){
return p.mouseX * this.viewport.zoom + this.viewport.x;
}
get mouseY(){
return p.mouseY * this.viewport.zoom + this.viewport.y;
}
sendDrawing(){
if (this.isDrawing && this.hasDrawnAllLines && this.hasFilledAllPixels){
if (this.type === 'free'){
let pos1 = {x: p.round(this.oldMouseX), y: p.round(this.oldMouseY)};
let pos2 = {x: p.round(this.mouseX), y: p.round(this.mouseY)};
let color = colorPicker.getColor();
let line = {p1: pos1, p2: pos2, c: color, t: this.thickness};
socket.emit('add-line', line);
}
if (this.type === 'pixel'){
let x = p.floor(this.mouseX / (this.dim.x / this.pixelCount.x));
x = p.constrain(x, 0, this.pixelCount.x);
let y = p.floor(this.mouseY / (this.dim.y / this.pixelCount.y));
y = p.constrain(y, 0, this.pixelCount.y);
let color = colorPicker.getColor();
if (color === this.pixels[x][y].c)
return;
let pixel = {x: x, y: y, c: color};
socket.emit('fill-pixel', pixel);
}
let color = colorPicker.getColor();
if (colorPicker.lastSetUsedColor != color)
colorPicker.setUsed(color);
}
}
draw(){
p.clear();
if (this.hasDrawnAllLines && this.hasFilledAllPixels){
this.moveViewport();
this.map.draw(this.image, this.dim, this.viewport);
let x = this.viewport.x;
let y = this.viewport.y;
let w = p.width * this.viewport.zoom;
let h = p.height * this.viewport.zoom;
p.image(this.image, 0, 0, p.width, p.height, x, y, w, h);
if (this.type === 'pixel' && this.gridActive){
p.image(this.gridImage, 0, 0, p.width, p.height, x, y, w, h);
}
}
else {
this.pixelsMap.draw(this.pixelsImage, this.dim, this.viewport);
this.linesMap.draw(this.linesImage, this.dim, this.viewport);
this.updateProgress();
}
let drawSpeed = 800;
for (let line of this.linesToDraw.slice(0, drawSpeed))
this.drawLine(line);
this.linesToDraw.splice(0, drawSpeed);
if (this.linesToDraw.length === 0 && !this.hasDrawnAllLines && this.receivedLines && this.receivedPixels){
this.hasDrawnAllLines = true;
if (this.hasFilledAllPixels)
$("#loading_drawings").fadeOut(200);
}
for (let pixel of this.pixelsToFill.slice(0, drawSpeed))
this.drawPixel(pixel);
this.pixelsToFill.splice(0, drawSpeed);
if (this.pixelsToFill.length === 0 && !this.hasFilledAllPixels && this.receivedPixels && this.receivedLines){
this.hasFilledAllPixels = true;
if (this.hasDrawnAllLines)
$("#loading_drawings").fadeOut(200);
}
for (let pixel of this.gridPixelsToFill.slice(0, drawSpeed))
this.drawGridPixel(pixel);
this.gridPixelsToFill.splice(0, drawSpeed);
this.oldMouseX = this.mouseX;
this.oldMouseY = this.mouseY;
}
moveViewport(){
if (givesFeedback)
return;
let y = 0, x = 0;
if (p.keyIsDown(40) || p.keyIsDown(83))
y += this.scrollSpeed;
if (p.keyIsDown(38) || p.keyIsDown(87))
y -= this.scrollSpeed;
if (p.keyIsDown(37) || p.keyIsDown(65))
x -= this.scrollSpeed;
if (p.keyIsDown(39) || p.keyIsDown(68))
x += this.scrollSpeed;
this.viewport.x += x;
this.viewport.y += y;
let maxX = this.viewport.maxX - p.width * this.viewport.zoom;
let maxY = this.viewport.maxY - p.height * this.viewport.zoom;
if (this.viewport.x > maxX) this.viewport.x = maxX;
if (this.viewport.y > maxY) this.viewport.y = maxY;
if (this.viewport.x < 0) this.viewport.x = 0;
if (this.viewport.y < 0) this.viewport.y = 0;
if (x != 0 || y != 0)
this.sendDrawing();
}
addAll(lines){
for (let line of lines)
this.addLine(line);
}
drawLine(line){
this.linesImage.strokeWeight(line.t);
this.linesImage.stroke(line.c);
this.linesImage.line(line.p1.x, line.p1.y, line.p2.x, line.p2.y);
}
addLine(line){
this.lines.push(line);
this.linesToDraw.push(line);
}
fillPixel(pixel){
this.pixels[pixel.x][pixel.y].c = pixel.c
this.pixelsToFill.push(pixel);
}
fillAll(pixels){
pixels.forEach(c => {
if (!c) return;
c.forEach(p => {
if (!p) return;
this.fillPixel(p);
});
});
}
drawPixel(pixel){
let px = pixel.x * this.pixelSize.x;
let py = pixel.y * this.pixelSize.y;
let w = this.pixelSize.x;
let h = this.pixelSize.y;
this.pixelsImage.fill(pixel.c);
this.pixelsImage.strokeWeight(1);
this.pixelsImage.noStroke();
this.pixelsImage.rect(px, py, w, h);
}
drawGridPixel(pixel){
let px = pixel.x * this.pixelSize.x;
let py = pixel.y * this.pixelSize.y;
let w = this.pixelSize.x;
let h = this.pixelSize.y;
this.gridImage.rect(px, py, w, h);
}
onLinesLoaded(lines){
this.receivedLines = lines;
console.log('Received all lines from server');
if (this.receivedPixels)
this.startDrawing();
}
onPixelsLoaded(pixels){
this.receivedPixels = pixels;
console.log('Received all pixels from server');
if (this.receivedLines)
this.startDrawing();
}
startDrawing(){
$("#action").html('Drawing...');
$('#loading_drawings > *:not(#action)').show();
this.receivedPixels.forEach((c, i, a) => {
a[i] = c.filter(p => p.c !== '#ffffff' && p.c !== '#FFFFFF');
});
this.addAll(this.receivedLines);
this.fillAll(this.receivedPixels);
console.log('Started drawing...');
}
updateProgress(){
if (!(this.receivedLines && this.receivedPixels))
return;
let pixelCountDraw = 0;
this.receivedPixels.forEach(c => pixelCountDraw += c.length);
let allThingsToDraw = this.receivedLines.length + pixelCountDraw;
let thingsToDraw = this.pixelsToFill.length + this.linesToDraw.length;
let progress = thingsToDraw / allThingsToDraw;
progress = progress * 100;
progress = isNaN(progress) ? 0 : progress;
$("#loading_drawings > progress").val(progress);
}
isLineVisible(line){
return this.isPointVisible(line.pos1) || this.isPointVisible(line.pos2);
}
isPointVisible(point){
if (!point) return;
let viewX = this.viewport.x;
let viewY = this.viewport.y;
return point.x - viewX > 0 && point.x - viewX < p.width
&& point.y - viewY > 0 && point.y - viewY < p.height
}
onMouseDown(){
if (p.mouseX > 0 && p.mouseX < p.width){
if (this.isCopying){
this.isCopying = false;
$("body").css('cursor', 'default');
} else {
this.isDrawing = true;
this.sendDrawing();
}
}
this.map.onMouseDown();
}
onMouseUp(){
this.isDrawing = false;
this.map.onMouseUp();
}
onMouseDragged(){
if (this.isDrawing){
this.sendDrawing();
}
this.map.onMouseDragged();
}
onMouseMoved(){
if (this.isCopying){
let vp = this.viewport;
let image = this.image.get(vp.x, vp.y, p.width * vp.zoom, p.height * vp.zoom);
let pixel = image.get(p.mouseX * vp.zoom, p.mouseY * vp.zoom);
colorPicker.updateFromRGB(pixel);
}
}
zoom(delta){
let oldZoom = this.viewport.zoom;
this.viewport.zoom += delta / 100 * this.viewport.zoomV;
if (p.width * this.viewport.zoom > this.dim.x)
this.viewport.zoom = this.dim.x / p.width;
if (p.height * this.viewport.zoom > this.dim.y)
this.viewport.zoom = this.dim.y / height;
if (this.viewport.zoom < 0.1)
this.viewport.zoom = 0.1;
let addZoom = this.viewport.zoom - oldZoom;
this.viewport.x -= addZoom * p.mouseX;
this.viewport.y -= addZoom * p.mouseY;
}
skipDrawing(html){
if (!this.receivedLines || !this.receivedPixels)
return;
$(html).attr('disabled', 'disabled');
setTimeout(() => {
for (let pixel of this.pixelsToFill){
this.drawPixel(pixel);
}
this.pixelsToFill = [];
for (let line of this.linesToDraw){
this.drawLine(line);
}
this.linesToDraw = [];
}, 0);
}
requestServerSave(html){
$(html).attr('disabled', 'disabled');
socket.emit('save-all');
}
answerServerSave(){
console.log('Drawings successfully saved on server');
$('#server_answer').fadeIn(200, 'swing', () => {
setTimeout(() =>
$('#server_answer').fadeOut(200)
, 1000 * 5);
setTimeout(() =>
$('#server_save').attr('disabled', false)
, 1000 * 60 * 5);
});
}
}
function updateDrawType(drawType){
drawer.type = drawType;
if (drawType === 'pixel'){
p.noSmooth();
$("#free_settings").hide();
$('#pixel_settings').show();
}
if (drawType === 'free'){
p.smooth();
$("#free_settings").show();
$('#pixel_settings').hide();
}
}
function startCopyColor(){
drawer.isCopying = true;
$("body").css('cursor', 'crosshair');
}

@ -0,0 +1,81 @@
'use strict';
let GRID_HOTKEY;
p.keyPressed = () => {
if (givesFeedback)
return;
if (p.keyCode === GRID_HOTKEY && drawer)
drawer.gridActive = !drawer.gridActive;
}
p.keyTyped = () => {
if (givesFeedback)
return;
if (settingHotkey){
GRID_HOTKEY = p.keyCode;
//p.storeItem('gridHotkey', GRID_HOTKEY);
$('#grid_hotkey').html(String.fromCharCode(p.keyCode));
settingHotkey = false;
}
}
p.mouseMoved = () => {
if (givesFeedback)
return;
if (drawer)
drawer.onMouseMoved();
}
p.mouseDragged = () => {
if (givesFeedback)
return;
if (drawer)
if (!drawer.hasDrawnAllLines || !drawer.hasFilledAllPixels)
return;
if (colorPicker)
colorPicker.mouseDragged();
if (drawer)
drawer.onMouseDragged();
}
p.mousePressed = () => {
if (drawer)
if (!drawer.hasDrawnAllLines || !drawer.hasFilledAllPixels)
return;
if (givesFeedback)
return;
if (drawer)
drawer.onMouseDown();
if (colorPicker)
colorPicker.mousePressed();
}
p.mouseReleased = () => {
if (drawer)
if (!drawer.hasDrawnAllLines || !drawer.hasFilledAllPixels)
return;
if (givesFeedback)
return;
if (drawer)
drawer.onMouseUp();
if (colorPicker)
colorPicker.mouseReleased();
}
p.mouseWheel = (e) => {
if (drawer)
if (drawer.hasDrawnAllLines && drawer.hasFilledAllPixels)
drawer.zoom(e.delta);
e.preventDefault();
}
window.onresize = () => {
p.resizeCanvas($("#canvas-holder").width(), $("#canvas-holder").height());
}
let settingHotkey = false;
function setGridHotkey(dom){
$(dom).blur();
settingHotkey = true;
}

@ -0,0 +1,125 @@
'use strict';
let projectName = "project_pattern";
let debug = false,
font,
localSettings,
loader;
let p;
let processor = new p5((p5Instance) => p = p5Instance);
//Only for online games
let socket;
let drawer;
let colorPicker;
let givesFeedback = false;
p.preload = () => {
localSettings = p.loadJSON('data/settings/settings.json', (json) => {
console.log('Local settings loaded: ', json)
}, (error) => {
console.log('Local settings failed: ', error)
});
font = p.loadFont('data/styles/font.ttf', (json) => {
console.log('Local font loaded: ', json)
}, (error) => {
console.log('Local font failed: ', error)
});
p.loadJSON('data/settings/libraries.json', json => {
loadScripts(json)
console.log('BenjoCraeft library scripts loaded: ', json)
});
}
p.setup = () => {
canvasSetup(localSettings.frameWork);
interfaceSetup();
drawer = new Drawer();
socketConnect(localSettings.project);
}
p.draw = () => {
if (loader){
loader.update();
loader.display();
}
if (drawer)
drawer.draw();
if (debug) debugInformation();
}
function canvasSetup(frameWork){
p.setFrameRate(frameWork.frameRate);
let w = frameWork.width,
h = frameWork.height;
if (w == null)
w = $("#canvas-holder").width();
if (h == null)
h = $("#canvas-holder").height();
let canvas = p.createCanvas(w, h);
canvas.parent('canvas-holder');
p.textFont(font);
p.noSmooth();
}
function interfaceSetup(){
updateThicknessPreview();
$('input[type=radio][name=type]').change(function() {
updateDrawType(this.value);
$(this).blur();
});
GRID_HOTKEY = p.getItem('gridHotkey');
if (!GRID_HOTKEY)
GRID_HOTKEY = 'G'.charCodeAt(0);
//p.storeItem('gridHotkey', GRID_HOTKEY);
$('#grid_hotkey').html(String.fromCharCode(GRID_HOTKEY));
let usedColors = $('.used_color').toArray().reverse();
let usedColorsData = p.getItem('usedColors');
if (!usedColorsData)
usedColorsData = [];
for (let used of usedColors){
let index = usedColors.indexOf(used);
let color = usedColorsData[index];
if (!color)
color = '#FFFFFF';
$(used).css('background-color', color);
usedColorsData[index] = color;
}
//p.storeItem('usedColors', usedColorsData);
}
function updateThicknessPreview(){
let size = $("#thickness").val();
if (drawer)
drawer.thickness = parseInt(size);
$("#thickness_preview").css({width: size, height: size});
}
function loadScripts(libs){
for (let script in libs){
if (libs[script]){
let url = 'data/lib/benjocraeft/' + script + '.js'
$.getScript(url, () => {
console.log('Successfully loaded script: ', url)
if (script === 'colorPicker'){
colorPicker = new ColorPicker(null);
colorPicker.hex = "#000";
colorPicker.updateFromHEX(null, true);
console.log('ColorPicker script loaded');
}
});
}
}
}

@ -0,0 +1,67 @@
class MiniMap{
constructor(htmlHolder, htmlCanvas, isMinimap){
let w = $(htmlHolder).width();
let h = $(htmlHolder).height();
this.dim = {x: w, y: h};
this.p = p.createGraphics(w, h);
this.p.parent(htmlHolder.get(0));
this.isMinimap = isMinimap;
$(htmlCanvas).show();
}
get mouseX(){
return p.winMouseX - $("#minimap > canvas").offset().left;
}
get mouseY(){
return p.winMouseY - $("#minimap > canvas").offset().top;
}
draw(image, dim, vp){
this.p.clear();
this.p.image(image, 0, 0, this.dim.x, this.dim.y, 0, 0, dim.x, dim.y);
if (this.isMinimap){
let x = p.map(vp.x, 0, vp.maxX, 0, this.dim.x);
let y = p.map(vp.y, 0, vp.maxY, 0, this.dim.y);
let w = p.map(p.width * vp.zoom, 0, vp.maxX, 0, this.dim.x);
let h = p.map(p.height * vp.zoom, 0, vp.maxY, 0, this.dim.y);
let r = (w + h) / 2 * 0.02;
this.p.noFill();
this.p.rect(x, y, w, h, r);
}
}
onMouseDown(){
if (this.mouseX > 0 && this.mouseX < this.p.width &&
this.mouseY > 0 && this.mouseY < this.p.height){
this.isDrawing = true;
this.moveViewport();
}
}
onMouseUp(){
this.isDrawing = false;
}
onMouseDragged(){
if (this.isDrawing)
this.moveViewport();
}
moveViewport(){
if (!this.isMinimap)
return;
let vp = drawer.viewport;
let mapX = this.mouseX - p.map(p.width * vp.zoom / 2, 0, drawer.dim.x, 0, this.dim.x);
let mapY = this.mouseY - p.map(p.height * vp.zoom / 2, 0, drawer.dim.y, 0, this.dim.y);
let x = p.map(mapX, 0, this.dim.x, 0, vp.maxX);
let y = p.map(mapY, 0, this.dim.y, 0, vp.maxY);
drawer.viewport.x = x;
drawer.viewport.y = y;
}
}

@ -0,0 +1,82 @@
'use strict';
function socketConnect(project, name = "noone"){
let urlQueries = '?game=' + project.name + '&name=' + name;
$.get('data/settings/get_port.php', port => {
let url = 'https://' + location.hostname + ':' + port + urlQueries;
socket = io.connect(url);
socket.on('connect', () => {
console.log('Connected to ', url);
socket.emit('join-lobby', 'global-draw-room');
socket.on('add-line', (lobby, line) => drawer.addLine(line));
socket.on('fill-pixel', (lobby, pixel) => drawer.fillPixel(pixel));
socket.on('add-all', (lines) => drawer.onLinesLoaded(lines));
socket.on('fill-all', (pixels) => drawer.onPixelsLoaded(pixels));
socket.on('member-joined', (lobby, clientId) => {
if (clientId !== socket.id)
return;
if (drawer)
if (drawer.lines.length !== 0)
return;
socket.emit('request-all-lines');
socket.emit('request-all-pixels');
$("#action").html("Downloading...");
});
socket.on('all-saved', (_lobby) => drawer.answerServerSave());
updateDrawType($("input[type=radio][name=type]:checked").val());
});
});
}
function sendFeedback(){
$.post('/php/post_feedback.php', {content: $('#user_feedback > textarea').val(), projectName: localSettings.project.name});
}
function createLobby(dom){
if (inputIsValid('create')){
onlineRequestFrontend(dom);
//TODO
}
}
function joinLobby(dom){
if (inputIsValid('join')){
onlineRequestFrontend(dom);
//TODO
}
}
function onlineRequestFrontend(dom){
$(dom).blur();
$(dom).attr('disabled', 'disabled');
if (loader) loader.destroy();
loader = new Loader($('#loader').get(0));
}
function inputIsValid(type){
let valid = true;
$('.error-label').html('');
if (type === 'create'){
if ($('#player-name > input').val() === ''){
valid = false;
$('#player-name > .error-label').html('Please enter a name!');
}
}
if (type === 'join'){
if ($('#player-name > input').val() === ''){
valid = false;
$('#player-name > .error-label').html('Please enter a name!');
}
if ($('#lobby-code > input').val() === ''){
valid = false;
$('#lobby-code > .error-label').html('Please enter your code!');
}
}
return valid;
}

@ -0,0 +1,3 @@
<?php
echo parse_ini_file("../../../server/.env")["HTTPS_PORT"];

@ -0,0 +1,8 @@
{
"collision": false,
"colorPicker": true,
"cookie": true,
"loader": true,
"prototypes": true,
"technical": true
}

@ -0,0 +1,22 @@
{
"project": {
"name": "global_draw",
"author": "BenjoCraeft",
"playerCounts": [],
"online": {
"iceServers": [
{"urls": "stun:stun.l.google.com:19302"},
{
"urls": "turn:numb.viagenie.ca",
"credential": "muazkh",
"username": "webrtc@live.com"
}
]
}
},
"frameWork": {
"frameRate": 60,
"width": null,
"height": null
}
}

@ -0,0 +1,76 @@
#color_picker{
margin: 20px;
border: 5px solid #000;
background-color: #000;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
position: relative;
}
#color_picker_numeric{
border-radius: 10px;
overflow: hidden;
}
#color_picker_rgb > input, #color_picker_hsv > input{
width: 80px;
float: left;
height: 35px;
font-size: 25px;
color: #000;
margin-top: 5px;
border-radius: 10px;
}
#color_picker_rgb > input:nth-child(1), #color_picker_hsv > input:nth-child(1){
margin-right: 10%;
margin-left: 3%;
}
#color_picker_rgb > input:nth-child(3), #color_picker_hsv > input:nth-child(3){
margin-left: 10%;
}
#color_picker_hex{
width: calc(100% - 5px);
height: 30px;
font-size: 25px;
border-radius: 10px;
}
#saturation{
position: relative;
width: calc(100% - 33px);
height: 100%;
background: linear-gradient(to right, #FFF 0%, #F00 100%);
float: left;
margin-right: 6px;
}
#value {
width: 100%;
height: 100%;
background: linear-gradient(to top, #000 0%, rgba(255,255,255,0) 100%);
}
#sb_picker{
border: 2px solid;
border-color: #FFF;
position: absolute;
width: 14px;
height: 14px;
border-radius: 10px;
bottom: -7px;
left: -8px;
box-sizing: border-box;
z-index: 10;
}
#hue {
width: 27px;
height: 100%;
position: relative;
float: left;
background: linear-gradient(to bottom, #F00 0%, #F0F 17%, #00F 34%, #0FF 50%, #0F0 67%, #FF0 84%, #F00 100%);
}
#hue_picker {
position: absolute;
background: #000;
border-bottom: 1px solid #000;
top: 0;
width: 27px;
height: 2px;
}

Binary file not shown.

@ -0,0 +1,86 @@
input[type=range] {
-webkit-appearance: none;
margin: 18px 0;
width: 100%;
background-color: inherit;
}
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 8.4px;
cursor: pointer;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
background: #3071a9;
border-radius: 1.3px;
border: 0.2px solid #010101;
}
input[type=range]::-webkit-slider-thumb {
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
border: 1px solid #000000;
height: 36px;
width: 16px;
border-radius: 3px;
background: none;
cursor: pointer;
-webkit-appearance: none;
margin-top: -14px;
}
input[type=range]:focus::-webkit-slider-runnable-track {
background: #367ebd;
}
input[type=range]::-moz-range-track {
width: 100%;
height: 8.4px;
cursor: pointer;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
background: #3071a9;
border-radius: 1.3px;
border: 0.2px solid #010101;
}
input[type=range]::-moz-range-thumb {
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
border: 1px solid #000000;
height: 36px;
width: 16px;
border-radius: 3px;
background: #264eff;
cursor: pointer;
}
input[type=range]::-ms-track {
width: 100%;
height: 8.4px;
cursor: pointer;
background: transparent;
border-color: transparent;
border-width: 16px 0;
color: transparent;
}
input[type=range]::-ms-fill-lower {
background: #2a6495;
border: 0.2px solid #010101;
border-radius: 2.6px;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
}
input[type=range]::-ms-fill-upper {
background: #3071a9;
border: 0.2px solid #010101;
border-radius: 2.6px;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
}
input[type=range]::-ms-thumb {
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
border: 1px solid #000000;
height: 36px;
width: 16px;
border-radius: 3px;
background: none;
cursor: pointer;
}
input[type=range]:focus::-ms-fill-lower {
background: #3071a9;
}
input[type=range]:focus::-ms-fill-upper {
background: #367ebd;
}

@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="https://cdn.socket.io/4.4.1/socket.io.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.min.js" type="text/javascript"></script>
<script src="https://code.jquery.com/jquery-3.6.4.min.js" type="text/javascript"></script>
<script src="data/scripts/init.js" type="text/javascript"></script>
<script src="data/scripts/events.js" type="text/javascript"></script>
<script src="data/scripts/online.js" type="text/javascript"></script>
<script src="data/scripts/drawer.js" type="text/javascript"></script>
<script src="data/scripts/minimap.js" type="text/javascript"></script>
<link href="styles.css" rel="stylesheet">
<link href="data/styles/color_picker.css" rel="stylesheet">
<link href="data/styles/range_input.css" rel="stylesheet">
<link href="data/images/favicon.ico" rel="icon" type="image/x-icon">
<title>Global Draw</title>
</head>
<body>
<div id="p5_loading"></div>
<div id="content">
<div id="interface">
<div id="color_picker">
<div id="saturation">
<div id="value">
<div id="sb_picker"></div>
</div>
</div>
<div id="hue">
<div id="hue_picker"></div>
</div>
</div>
<div id="color_picker_numeric_holder">
<div id="color_picker_numeric">
<div id="color_picker_rgb">
<input max="255" min="0" onchange="colorPicker.updateFromRGB()" oninput="colorPicker.updateFromRGB()" type="number"/>
<input max="255" min="0" onchange="colorPicker.updateFromRGB()" oninput="colorPicker.updateFromRGB()" type="number"/>
<input max="255" min="0" onchange="colorPicker.updateFromRGB()" oninput="colorPicker.updateFromRGB()" type="number"/>
</div>
<div id="color_picker_hsv">
<input max="360" min="0" onchange="colorPicker.updateFromHSV()" oninput="colorPicker.updateFromHSV()" type="number"/>
<input max="100" min="0" onchange="colorPicker.updateFromHSV()" oninput="colorPicker.updateFromHSV()" type="number"/>
<input max="100" min="0" onchange="colorPicker.updateFromHSV()" oninput="colorPicker.updateFromHSV()" type="number"/>
</div>
<input id="color_picker_hex" onchange="colorPicker.updateFromHEX(this, false)" style="background-color: #000000;" style="background-color: #000000;" type="text"/>
<div id="color_picker_final"></div>
</div>
</div>
<div id="used_colors">
<button class="used_color" onclick="colorPicker.setFromUsed(this)"></button>
<button class="used_color" onclick="colorPicker.setFromUsed(this)"></button>
<button class="used_color" onclick="colorPicker.setFromUsed(this)"></button>
<button class="used_color" onclick="colorPicker.setFromUsed(this)"></button>
<button class="used_color" onclick="colorPicker.setFromUsed(this)"></button>
<button class="used_color" onclick="colorPicker.setFromUsed(this)"></button>
<button class="used_color" onclick="colorPicker.setFromUsed(this)"></button>
</div>
<button id="get_color" onclick="startCopyColor();">Pick color from screen</button>
<div id="free_settings">
<div id="thickness_holder">
<input id="thickness" max="25" min="1" oninput="updateThicknessPreview();" type="range" value="2"/>
<div id="thickness_preview_holder">
<div id="thickness_preview"></div>
</div>
</div>
</div>
<div id="pixel_settings">
<span>Grid Toggle: [</span><span id="grid_hotkey"></span><span>]</span>
<button id="set_hotkey" onclick="setGridHotkey(this);">Change</button>
</div>
<div id="minimap">
</div>
<form id="draw_type">
<label>
<input checked name="type" type="radio" value="free">
Draw Freely
</label>
<br>
<label>
<input name="type" type="radio" value="pixel">
Pixel Art
</label>
</form>
<button id="server_save" onclick="drawer.requestServerSave(this);">Server save</button>
<span id="server_answer" style="display: none;">Drawings saved!</span>
<button id="start_user_feedback" onclick="$('#user_feedback').show(); givesFeedback = true;">Feedback</button>
</div>
<div id="canvas-holder"></div>
<div id="loading_drawings">
<span id="action">Connecting...</span>
<div id="loading_maps" style="display: none;">
<div id="pixels_map_holder"></div>
<div id="lines_map_holder"></div>
</div>
<progress max="100" style="display: none;" value="100"></progress>
<br>
<button id="skip_drawing" onclick="if (drawer) drawer.skipDrawing(this)" style="display: none;">Skip Drawing</button>
<br>
<span id="warning" style="display: none;">Warning: Do not click this unless you have a proper Graphic Processor!</span>
</div>
<div id="user_feedback" style="display: none;">
<span>Write me feedback!</span>
<textarea cols="50" rows="10"></textarea>
<button onclick="sendFeedback();$('#user_feedback').hide();givesFeedback = false;">Submit</button>
<button onclick="$('#user_feedback').hide(); givesFeedback = false;">Cancel</button>
</div>
</div>
</body>
</html>

@ -0,0 +1,226 @@
a:link, a:hover, a:active, a:visited{color: #000;}
html, body{margin: 0; padding: 0; height: 100%; width: 100%; overflow: hidden;}
canvas{margin: 0; padding: 0; border: none; display: block;}
button:hover{cursor: pointer;}
@font-face{
font-family: "Rametto";
src: url("data/styles/font.ttf");
}
*{
font-family: "Rametto";
color: #000;
font-size: 17px;
}
:root{
--if-background-color: rgb(0, 100, 131);
--if-width: 300px;
--if-border-width: 5px;
--if-padding: 25px;
--if-content-width: calc(var(--if-width) - var(--if-padding) * 2);
}
/**
* Standard styles
*/
#content{
display: grid;
grid-template-columns: calc(var(--if-width) + var(--if-border-width)) calc(100% - var(--if-width));
grid-template-rows: 100%;
width: 100%;
height: 100%;
overflow: hidden;
}
button{
background-color: rgb(67, 202, 67);
margin: 5px;
border-radius: 5px;
}
button:hover:not(.used_color):not([disabled]){
background-color: rgb(49, 150, 49);
}
button:disabled{
background-color: rgb(121, 121, 121);
}
#loading_drawings{
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(50, 182, 138, 0.699);
text-align: center;
}
#action{
font-size: 50px;
}
#loading_drawings > progress{
width: 700px;
height: 30px;
}
#warning{
font-size: 10px;
}
#p5_loading{
display: none;
}
#loading_maps{
width: 100%;
display: flex;
justify-content: center;
}
#pixels_map_holder, #lines_map_holder{
width: 500px;
height: 500px;
border-radius: 5px;
border: 3px solid #000;
background-color: #FFF;
margin: 50px;
}
#user_feedback{
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(50, 182, 138, 0.699);
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#user_feedback > *{
width: 50%;
}
#interface{
background-color: var(--if-background-color);
border: var(--if-border-width) solid #000;
border-width: 0 5px 0 0;
}
#version{
position: absolute;
bottom: 55px;
}
#changelog{
position: absolute;
bottom: 30px;
}
#start_user_feedback{
position: absolute;
bottom: 0;
height: 25px;
font-size: 15px;
}
#color_picker{
width: var(--if-content-width);
height: var(--if-content-width);
}
#color_picker_numeric_holder{
margin: 10px var(--if-padding);
width: var(--if-content-width);
background-color: none;
}
#color_picker_numeric{
background-color: none;
}
#color_picker_rgb, #color_picker_hsv{
display: none;
}
#used_colors{
width: var(--if-content-width);
margin: 0 var(--if-padding);
display: flex;
justify-content: space-between;
}
.used_color{
width: 30px;
height: 30px;
margin: 0;
background-color: white;
}
#get_color{
width: calc(var(--if-content-width));
margin: 0 var(--if-padding) 15px;
font-size: 11px;
border-radius: 3px;
background-color: white;
}
#get_color:hover{
background-color: rgb(150, 150, 150);
}
#free_settings, #pixel_settings{
margin: 0px var(--if-padding);
width: var(--if-content-width);
height: 80px;
}
#thickness_holder{
display: flex;
align-items: center;
}
#thickness{
width: calc(var(--if-content-width) - 50px);
}
#thickness_preview_holder{
position: relative;
margin: 0 0 0 10px;
width: 40px;
height: 40px;
}
#thickness_preview{
border: 1px solid #000;
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
}
#set_hotkey{
float: right;
margin: 0;
font-size: 12px;
height: 28px;
}
#minimap{
width: var(--if-content-width);
height: var(--if-content-width);
margin: 22px;
border-radius: 5px;
border: 3px solid #000;
}
#minimap > canvas{
border-radius: inherit;
}
#draw_type{
width: var(--if-content-width);
margin: 0 var(--if-padding);
text-align: center;
}
#server_save{
width: calc(var(--if-content-width) * 0.6);
margin: 10px calc(var(--if-padding) + var(--if-content-width) * 0.2)
}
#server_answer{
display: block;
width: var(--if-content-width);
font-size: 15px;
text-align: center;
margin: 0 var(--if-padding);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

5
server/.gitignore vendored

@ -0,0 +1,5 @@
out
json_data
logs
node_modules
.env

@ -0,0 +1,424 @@
{
"name": "global-draw-sheet",
"version": "2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "global-draw-sheet",
"version": "2.0",
"dependencies": {
"dotenv": "^16.0.3",
"https": "^1.0.0",
"socket.io": "^4.4.1",
"typescript": "^5.0.2"
},
"devDependencies": {
"@types/node": "^18.15.3"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"node_modules/@types/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
},
"node_modules/@types/cors": {
"version": "2.8.13",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz",
"integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "18.15.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.3.tgz",
"integrity": "sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw=="
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/cookie": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/dotenv": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/engine.io": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz",
"integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==",
"dependencies": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.11.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/https": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
"integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg=="
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/socket.io": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz",
"integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.2",
"engine.io": "~6.4.1",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz",
"integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==",
"dependencies": {
"ws": "~8.11.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz",
"integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/typescript": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz",
"integrity": "sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=12.20"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
},
"dependencies": {
"@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"@types/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
},
"@types/cors": {
"version": "2.8.13",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz",
"integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==",
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "18.15.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.3.tgz",
"integrity": "sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw=="
},
"accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"requires": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
}
},
"base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
},
"cookie": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="
},
"cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"requires": {
"object-assign": "^4",
"vary": "^1"
}
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"requires": {
"ms": "2.1.2"
}
},
"dotenv": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ=="
},
"engine.io": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz",
"integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==",
"requires": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.11.0"
}
},
"engine.io-parser": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw=="
},
"https": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
"integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg=="
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"requires": {
"mime-db": "1.52.0"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
},
"socket.io": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz",
"integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==",
"requires": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.2",
"engine.io": "~6.4.1",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.1"
}
},
"socket.io-adapter": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz",
"integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==",
"requires": {
"ws": "~8.11.0"
}
},
"socket.io-parser": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz",
"integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
}
},
"typescript": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz",
"integrity": "sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
},
"ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"requires": {}
}
}
}

@ -0,0 +1,17 @@
{
"name": "global-draw",
"version": "2.0",
"private": true,
"scripts": {
"start": "node out/index.js"
},
"dependencies": {
"dotenv": "^16.0.3",
"https": "^1.0.0",
"socket.io": "^4.4.1",
"typescript": "^5.0.2"
},
"devDependencies": {
"@types/node": "^18.15.3"
}
}

@ -0,0 +1,169 @@
import {Room} from "./room.js"
import {ConnectionManager, serializeObject} from "./manager.js"
import {log} from "./logger.js";
import * as SocketIO from "socket.io";
import {Settings} from "./definitions/settings";
export class Client {
socket: SocketIO.Socket;
name: string;
game: string;
id: string;
isReady: boolean;
isPlayer: boolean;
isSpectator: boolean;
constructor(socket: SocketIO.Socket, manager: ConnectionManager) {
this.socket = socket;
// @ts-ignore
this.name = socket.handshake.query.name;
// @ts-ignore
this.game = socket.handshake.query.game;
this.id = socket.id;
this.setEvents(manager)
}
get serialized(): Serialized.Client {
return {
id: this.id,
name: this.name,
game: this.game,
isReady: this.isReady,
isPlayer: this.isPlayer,
isSpectator: this.isSpectator
};
}
setEvents(mng: ConnectionManager): void {
let s = this.socket;
s.on('room-list', () => this.sendRoomList());
s.on('client-list', () => this.sendClientList());
s.on('set-ready', ready => this.setReady(ready));
s.on('game-settings', settings => this.setGameSettings(settings));
s.on('create-lobby', (settings, name) => this.createRoom(settings, name));
s.on('join-lobby', roomId => this.joinRoom(roomId));
s.on('leave-lobby', roomId => this.leaveRoom(roomId));
s.on('join-spectators', () => this.joinSpectators());
s.on('join-players', () => this.joinPlayers());
s.on('start-game', lobbyId => mng.startGame(this, lobbyId));
s.on('stop-game', lobbyId => mng.stopGame(this, lobbyId));
s.on('feedback', content => mng.saveFeedbackToFile(this, content));
s.on('disconnect', () => mng.disconnected(this));
this.send('connected')
}
sendRoomList(): void {
let rooms = ConnectionManager.RoomListByGame(this.game);
this.send('room-list', rooms)
}
sendClientList(): void {
let clients = ConnectionManager.ClientListByClientId(this.id);
this.send('client-list', clients)
}
setReady(ready: boolean): void {
let room = Room.getByClientId(this.id, ConnectionManager.Instance.rooms);
if (room) {
this.isReady = ready;
room.toAll('client-list', room.clients)
}
}
setGameSettings(settings: any): void {
let room = Room.getByClientId(this.id, ConnectionManager.Instance.rooms);
if (room) {
room.gameSettings = settings;
room.toAll('game-settings', settings)
}
}
createRoom(settings: Settings.Global, name: string): void {
let room = ConnectionManager.Instance.createRoom(settings, name);
room.add(this);
this.send('created-lobby', room);
log('lobby-created', this, room)
}
joinRoom(roomId: string): Room {
let room = Room.getByRoomId(roomId, ConnectionManager.Instance.rooms);
if (!room) {
this.send('join-failed', 'Room does not exist!');
log('join-non-existent', this, new Room('not-existent', roomId))
} else if (room.hasStarted && !room.settings.spectators) {
this.send('join-failed', 'Game has started yet!');
log('join-started', this, room)
} else {
room.add(this);
log('member-joined', this, room)
}
return room
}
leaveRoom(_roomId: string): void {
let room = Room.getByClientId(this.id, ConnectionManager.Instance.rooms);
if (!room)
return;
this.leave(room.id);
if (room.runningGame)
room.runningGame.removeClient(this);
room.clients.splice(room.clients.indexOf(this), 1);
room.toAll('member-left', this.id, this.name);
room.toAll('client-list', room.clients);
this.send('left-lobby');
log('member-left', this, room);
if (room.isEmpty && !room.settings.always) {
ConnectionManager.Instance.deleteRoom(room)
}
}
joinSpectators() {
let room = Room.getByClientId(this.id, ConnectionManager.Instance.rooms);
if (!room)
return;
this.isSpectator = true;
this.isPlayer = false;
room.toAll('client-list', room.clients)
}
joinPlayers() {
let room = Room.getByClientId(this.id, ConnectionManager.Instance.rooms);
if (!room)
return;
if (room.hasStarted)
return;
this.isSpectator = false;
this.isPlayer = true;
room.toAll('client-list', room.clients)
}
send(event: string, ...args: any[]): void {
this.socket.emit(event, ...serializeObject(args))
}
join(roomId: string): void {
this.socket.join(roomId)
}
leave(roomId: string): void {
this.socket.leave(roomId)
}
}

@ -0,0 +1,20 @@
declare namespace Serialized {
interface Lobby {
id: string
name: string
game: string
clientCounts: number[]
clients: Client[]
hasStarted: boolean
}
interface Client {
id: string
name: string
game: string
isReady: boolean
isPlayer: boolean
isSpectator: boolean
}
}

@ -0,0 +1,29 @@
import {ServerGame} from "../game_standard";
declare module Settings {
interface Global {
project: Project
frameWork: FrameWork
game: any
always: boolean
spectators: boolean
}
interface Project {
name: string
author: string
playerCounts: number[]
}
interface FrameWork {
frameRate: number
updateRate: number
width: number
height: number
}
interface Server {
useP2P: boolean
gameClass: typeof ServerGame
}
}

@ -0,0 +1,38 @@
import {Room} from "./room.js"
import {Client} from "./client.js"
import {Settings} from "./definitions/settings";
export class ServerGame {
room: Room;
settings: Settings.Global;
game: any;
constructor(room: Room, settings: Settings.Global) {
this.settings = settings;
this.room = room;
this.room.clients.forEach(c => this.addClient(c))
}
addClient(client: Client): void {
this.setEvents(client)
}
removeClient(client: Client): void {
this.removeEvents(client)
}
gameAction(action: string, ...args: any[]): void {
}
setEvents(client: Client): void {
let socket = client.socket;
socket.on('game-action', (action, ...args) => this.gameAction(action, ...args))
}
removeEvents(client: Client): void {
let socket = client.socket;
socket.removeAllListeners('game-action')
}
}

@ -0,0 +1,149 @@
import {ServerGame} from "./game_standard"
import {Room} from "./room";
import {Settings} from "./definitions/settings";
import {log} from "./logger";
import {Client} from "./client";
import * as fs from "fs";
export class GlobalDraw extends ServerGame {
lines: any[];
pixels: any[][];
linesPath = "json_data/global_draw/lines.json";
pixelsPath = "json_data/global_draw/pixels.json";
pixelCount = 1000;
constructor(lobby: Room, settings: Settings.Global) {
super(lobby, settings);
this.lines = [];
this.pixels = [];
for (let x = 0; x < this.pixelCount; x++) {
let column = [];
for (let y = 0; y < this.pixelCount; y++) {
column.push({x: x, y: y, c: "#ffffff"});
}
this.pixels.push(column);
}
let linesLoaded = false;
let pixelsLoaded = false;
this.loadDrawingsFromFile(this.linesPath, (data: any[]) => {
this.lines = data;
}, () => {
linesLoaded = true;
if (pixelsLoaded) {
this.startSaveInterval();
}
});
this.loadDrawingsFromFile(this.pixelsPath, (data: any[]) => {
for (let x = 0; x < this.pixelCount; x++) {
for (let y = 0; y < this.pixelCount; y++) {
if (data[x])
if (data[x][y])
this.pixels[x][y].c = data[x][y].c
}
}
}, () => {
pixelsLoaded = true;
if (linesLoaded) {
this.startSaveInterval();
}
});
}
startSaveInterval() {
this.saveAllDrawingsToFile();
//Saves once every day
setInterval(() => this.saveAllDrawingsToFile(), 1000 * 60 * 60 * 24);
}
addLine(line: any) {
this.lines.push(line);
this.room.toAll('add-line', line)
}
fillPixel(pixel: any) {
this.pixels[pixel.x][pixel.y].c = pixel.c;
this.room.toAll('fill-pixel', pixel)
}
loadDrawingsFromFile(drawingsPath: string, successs: (data: any[]) => void, done: () => void) {
fs.readFile(drawingsPath, 'utf8', (err, data) => {
if (err)
log('load-error', null, this.room, err.message);
else {
try {
let parsed = JSON.parse(data);
log('load-success', null, this.room);
successs(parsed);
} catch (e) {
log('parse-error', null, this.room, e.message);
}
}
done();
});
}
saveDrawingsToFile(drawings: any[], drawingsPath: string, callback: (err: any) => void) {
let splits = drawingsPath.split('/');
let path = splits.slice(0, splits.length - 1).reduce((prev, curr) => prev + '/' + curr);
let name = splits[splits.length - 1];
if (!fs.existsSync(path)) {
fs.mkdirSync(path, {recursive: true});
}
fs.writeFile(drawingsPath, JSON.stringify(drawings), callback);
}
saveAllDrawingsToFile() {
let linesSaved = false;
let pixelsSaved = false;
this.saveDrawingsToFile(this.lines, this.linesPath, (err) => {
if (err)
log('save-error', null, this.room, err.message);
else {
linesSaved = true;
if (pixelsSaved) {
this.room.toAll('all-saved');
linesSaved = false;
pixelsSaved = false
}
log('save-success', null, this.room, 'Successfully saved lines to file')
}
});
this.saveDrawingsToFile(this.pixels, this.pixelsPath, (err) => {
if (err)
log('save-error', null, this.room, err.message);
else {
pixelsSaved = true;
if (linesSaved) {
this.room.toAll('all-saved');
pixelsSaved = false;
linesSaved = false
}
log('save-success', null, this.room, 'Successfully saved pixels to file')
}
});
}
addClient(client: Client): void {
this.setEvents(client);
}
setEvents(client: Client): void {
super.setEvents(client);
let socket = client.socket;
socket.on('add-line', (line) => this.addLine(line));
socket.on('fill-pixel', (pixel) => this.fillPixel(pixel));
socket.on('request-all-lines', () => socket.emit('add-all', this.lines));
socket.on('request-all-pixels', () => socket.emit('fill-all', this.pixels));
socket.on('save-all', () => this.saveAllDrawingsToFile());
}
}

@ -0,0 +1,7 @@
import {GlobalDraw} from "./global_draw";
import {StartServer} from "./start";
StartServer({
useP2P: false,
gameClass: GlobalDraw
});

@ -0,0 +1,105 @@
import {Room} from "./room.js"
import {Client} from "./client.js"
import * as fs from "fs";
import * as util from "util";
let logFolder = "./logs";
if (!fs.existsSync(logFolder)) {
fs.mkdirSync(logFolder);
}
let logFile = fs.createWriteStream(logFolder + '/' + new Date().getTime() + '.log', {flags: 'a'});
let logStdout = process.stdout;
console.log = function () {
logFile.write(util.format.apply(null, arguments) + '\n');
logStdout.write(util.format.apply(null, arguments) + '\n');
};
console.error = console.log;
process.on('uncaughtException', err => {
console.error('Uncaught error: ', err);
process.exit(1);
});
process.stdin.pipe(logFile);
export function log(type: string, client: Client, lobby?: Room, msg?: string) {
let now = new Date(Date.now()).toString(), message, name, game;
let date = '[' + now.substring(0, now.indexOf('GMT') - 1) + ']';
if (client) {
game = '[' + client.game + ']';
let short = client.id.substring(0, Math.round(client.id.length / 3));
name = '"' + client.name + '(' + short + '...)"';
} else {
if (type === 'lobby-deleted') {
game = '[' + lobby.gameName + ']';
} else {
game = '[undefined]';
}
name = 'UNKNOWN';
}
if (lobby) {
game = '[' + lobby.gameName + ']';
}
switch (type) {
case 'join-non-existent':
message = name + ' tried to join non-existent lobby "' + lobby.id + '"';
break;
case 'join-started':
message = name + ' tried to join the started game "' + lobby.id + '"';
break;
case 'lobby-created':
message = name + ' created new lobby: "' + lobby.id + '"';
break;
case 'game-started':
message = name + ' started the game: "' + lobby.id + '"';
break;
case 'game-stopped':
message = name + ' stopped the game: "' + lobby.id + '"';
break;
case 'member-joined':
message = name + ' joined the lobby "' + lobby.id + '"';
break;
case 'member-left':
message = name + ' left the lobby "' + lobby.id + '"';
break;
case 'lobby-deleted':
message = 'Lobby "' + lobby.id + '" was deleted';
break;
case 'save-success':
message = msg;
break;
case 'save-error':
message = 'Failed to save contents to file: ' + msg;
break;
case 'load-success':
message = 'Successfully loaded and parsed file contents';
break;
case 'load-error':
message = 'Failed to load file: ' + msg;
break;
case 'parse-error':
message = 'Failed to parse contents: ' + msg;
break;
case 'feedback':
message = 'Saved feedback to file: ' + msg;
break;
case 'connection':
message = name + ' connected';
break;
case 'disconnection':
message = name + ' disconnected';
break;
case 'startup':
message = msg;
break;
}
console.log(date + game + ' ---> {' + message + '}');
}

@ -0,0 +1,149 @@
import {Room} from "./room.js"
import {Client} from "./client.js"
import {log} from "./logger.js"
import * as fs from "fs";
import * as SocketIO from "socket.io"
import {Settings} from "./definitions/settings";
export class ConnectionManager {
static Instance: ConnectionManager;
io: SocketIO.Server;
rooms: Room[];
constructor(io: SocketIO.Server) {
ConnectionManager.Instance = this;
this.io = io;
this.rooms = [];
let drawSettings = {
project: {
name: 'global-draw',
playerCounts: null
},
always: true,
spectators: true
};
let drawRoom = this.createRoom(drawSettings, '');
drawRoom.id = 'global-draw-room';
drawRoom.startGame();
this.rooms.push(drawRoom);
}
static RoomListByGame(game: string): Room[] {
return this.Instance.rooms.filter(l => l.gameName === game)
}
static ClientListByClientId(clientId: string): Client[] {
let room = Room.getByClientId(clientId, this.Instance.rooms);
return room.clients
}
newSocket(socket: SocketIO.Socket): void {
let client = new Client(socket, this);
log('connection', client)
}
roomListUpdate(): void {
this.io.sockets.emit('room-list', serializeObject(this.rooms))
}
createRoom(settings: Settings.Global | any, name: string): Room {
let roomId = Room.generateCode(10);
let room = new Room(name, roomId, settings, this.io);
this.rooms.push(room);
this.roomListUpdate();
return room
}
deleteRoom(room: Room): void {
this.rooms.splice(this.rooms.indexOf(room), 1);
this.roomListUpdate();
log('lobby-deleted', null, room)
}
//Starts the game of a room with given id
startGame(client: Client, _roomId: string): void {
let lobby = Room.getByClientId(client.id, this.rooms);
if (!lobby) return;
if (!lobby.hasStarted) {
lobby.startGame();
log('game-started', client, lobby)
}
this.io.sockets.emit('room-list', serializeObject(this.rooms))
}
//Stops the game of a lobby with given id
stopGame(client: Client, lobbyId: string): void {
let lobby = Room.getByRoomId(lobbyId, this.rooms);
if (!lobby) return;
lobby.stopGame(client);
log('game-stopped', client, lobby)
}
//Saves user feedback to a file
saveFeedbackToFile(client: Client, content: string): void {
let date = new Date(Date.now()).toString();
let path = "feedback/" + client.game + '.txt';
let saveToFile = (content: string) => {
fs.writeFile(path, content, (err: any) => {
if (err)
log('save-error', client, null, err.message);
else
log('feedback', client, null, path)
});
};
if (fs.existsSync(path)) {
fs.readFile(path, 'utf8', (err, data) => {
if (err)
log('load-error', client, null, err.message);
else {
log('load-success', client, null);
let newContent = data + '\n\n\n\n' + date + '\n\n' + content;
saveToFile(newContent)
}
})
} else {
saveToFile(date + '\n' + content)
}
}
//Removes a disconnected client from all references
disconnected(client: Client): void {
let room = Room.getByClientId(client.id, this.rooms);
if (room)
client.leaveRoom(room.id);
log('disconnection', client)
}
}
export function serializeObject(object: any): any {
function serialize(obj: any) {
if (!obj)
return obj;
if (obj.serialized)
return obj.serialized;
else if (obj instanceof Array) {
let content = [];
obj.forEach(o => {
content.push(serialize(o))
});
return content
}
return obj
}
return serialize(object)
}

@ -0,0 +1,136 @@
import {Client} from "./client.js"
import {ServerGame} from "./game_standard.js"
import {serializeObject} from "./manager.js";
import {Server} from "socket.io";
import {Settings} from "./definitions/settings";
export class Room {
id: string;
gameName: string;
clientCounts: number[];
io: Server;
clients: Client[];
runningGame: ServerGame;
settings: Settings.Global;
gameSettings: any;
name: string;
static GameClass: typeof ServerGame
constructor(name: string, id: string, settings?: Settings.Global, io?: Server) {
this.id = id;
this.name = name;
if (!io || !settings) return;
this.settings = settings;
this.gameName = settings.project.name;
this.clientCounts = settings.project.playerCounts;
this.io = io;
this.clients = [];
this.gameSettings = {}
}
get leader(): Client {
return this.players[0]
}
get players(): Client[] {
return this.clients.filter(c => c.isPlayer)
}
get spectators(): Client[] {
return this.clients.filter(c => c.isSpectator)
}
get serialized(): Serialized.Lobby {
return {
id: this.id,
name: this.name,
game: this.gameName,
clientCounts: this.clientCounts,
clients: serializeObject(this.clients),
hasStarted: this.hasStarted
};
}
get isEmpty(): boolean {
return !(this.clients.length)
}
get hasStarted(): boolean {
return this.runningGame != null
}
static getByRoomId(id: string, lobbies: Room[]): Room {
for (let l of lobbies) {
if (l.id === id)
return l
}
return null;
}
static getByClientId(id: string, lobbies: Room[]): Room {
for (let l of lobbies) {
for (let c of l.clients) {
if (c.id === id)
return l
}
}
return null;
}
static generateCode(elements: number): string {
let code = '';
let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
while (elements--) {
code += possible.charAt(Math.floor(Math.random() * possible.length))
}
return code
}
startGame(): void {
let seed = Math.random() * 10000;
this.toAll('start-game', seed);
this.runGame()
}
stopGame(client: Client): void {
this.toAll('stop-game', client);
this.runningGame = null
}
add(client: Client): void {
this.clients.push(client);
let isPlayer = !this.hasStarted && this.hasValidPlayerCount();
client.isPlayer = isPlayer;
client.isSpectator = !isPlayer;
client.isReady = false;
client.join(this.id);
this.toAll('member-joined', client.id, client.name);
this.toAll('client-list', this.clients);
this.toAll('game-settings', this.gameSettings);
if (this.hasStarted)
this.runningGame.addClient(client)
}
hasValidPlayerCount(): boolean {
let valid = false;
this.clientCounts.forEach(c => {
if (c === this.clients.length)
valid = true
});
return valid
}
runGame(): void {
this.runningGame = new Room.GameClass(this, this.settings);
}
toAll(event: string, ...args: any[]): void {
this.io.to(this.id).emit(event, serializeObject(this), ...serializeObject(args))
}
}

@ -0,0 +1,39 @@
import {ConnectionManager} from "./manager.js";
import {log} from "./logger.js";
import {Server} from 'socket.io';
import {Room} from "./room.js";
import * as https from "https";
import * as fs from "fs";
export function StartServer(settings: any){
require("dotenv").config();
const httpsPort = parseInt(process.env.HTTPS_PORT);
let cert = fs.readFileSync(`${process.env.SSL_PATH}/cert.pem`);
let key = fs.readFileSync(`${process.env.SSL_PATH}/key.pem`);
let httpsServer = https.createServer({key: key, cert: cert});
let sIO = new Server(httpsServer, {
cors: {
origin: ["https://play.benjamin-kraft.local", "https://play.benjamin-kraft.eu"]
}
});
if (settings.useP2P){
const p2p = require('socket.io-p2p-server').Server;
sIO.use(p2p);
}
httpsServer.listen(httpsPort);
Room.GameClass = settings.gameClass;
let connectionManager = new ConnectionManager(sIO);
// On new connection
sIO.on('connection', socket => connectionManager.newSocket(socket));
log('startup', null, null, 'Server is listening on port ' + httpsPort);
}

@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "CommonJS",
"alwaysStrict": true,
"sourceMap": true,
"outDir": "./out"
},
"include": [
"./src"
]
}
Loading…
Cancel
Save