├── static └── robots.txt ├── .gitignore ├── src ├── favicon.ico ├── img │ ├── gui.png │ ├── gear.png │ ├── help.png │ ├── load.png │ ├── owop.png │ ├── plus.png │ ├── save.png │ ├── wiki.png │ ├── button.png │ ├── cancel.png │ ├── discord.png │ ├── donate.png │ ├── facebook.png │ ├── loading.gif │ ├── reddit.png │ ├── toolset.png │ ├── unloaded.png │ ├── window_in.png │ ├── window_out.png │ ├── small_border.png │ ├── button_pressed.png │ └── halloween-pattern.png ├── audio │ ├── click.mp3 │ ├── launch.mp3 │ └── place.mp3 ├── font │ ├── pixeloperator.woff │ └── pixeloperator.woff2 ├── js │ ├── protocol │ │ ├── proto_parse.js │ │ ├── all.js │ │ ├── Protocol.js │ │ ├── v0x00.js │ │ └── old.js │ ├── util │ │ ├── Lerp.js │ │ ├── Bucket.js │ │ ├── color.js │ │ ├── normalizeWheel.js │ │ ├── misc.js │ │ └── anchorme.js │ ├── networking.js │ ├── global.js │ ├── context.js │ ├── captcha.js │ ├── Player.js │ ├── polyfill │ │ └── canvas-toBlob.js │ ├── Fx.js │ ├── conf.js │ ├── tool_renderer.js │ ├── windowsys.js │ ├── local_player.js │ ├── World.js │ ├── colorPicker.js │ └── canvas_renderer.js ├── css │ ├── context.css │ ├── pixel_font.css │ ├── styled_scrollbar.css │ └── style.css ├── index.ejs └── index_UK.ejs ├── postcss.config.js ├── upload.sh ├── localPublish.sh ├── default.nix ├── serve.js ├── package.json ├── webpack.config.js └── halloween.patch /static/robots.txt: -------------------------------------------------------------------------------- 1 | # nothing to see here 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | result 2 | node_modules 3 | node_modules_linux 4 | dist 5 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/img/gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/gui.png -------------------------------------------------------------------------------- /src/img/gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/gear.png -------------------------------------------------------------------------------- /src/img/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/help.png -------------------------------------------------------------------------------- /src/img/load.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/load.png -------------------------------------------------------------------------------- /src/img/owop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/owop.png -------------------------------------------------------------------------------- /src/img/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/plus.png -------------------------------------------------------------------------------- /src/img/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/save.png -------------------------------------------------------------------------------- /src/img/wiki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/wiki.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ] 5 | } -------------------------------------------------------------------------------- /src/audio/click.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/audio/click.mp3 -------------------------------------------------------------------------------- /src/audio/launch.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/audio/launch.mp3 -------------------------------------------------------------------------------- /src/audio/place.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/audio/place.mp3 -------------------------------------------------------------------------------- /src/img/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/button.png -------------------------------------------------------------------------------- /src/img/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/cancel.png -------------------------------------------------------------------------------- /src/img/discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/discord.png -------------------------------------------------------------------------------- /src/img/donate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/donate.png -------------------------------------------------------------------------------- /src/img/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/facebook.png -------------------------------------------------------------------------------- /src/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/loading.gif -------------------------------------------------------------------------------- /src/img/reddit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/reddit.png -------------------------------------------------------------------------------- /src/img/toolset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/toolset.png -------------------------------------------------------------------------------- /src/img/unloaded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/unloaded.png -------------------------------------------------------------------------------- /src/img/window_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/window_in.png -------------------------------------------------------------------------------- /src/img/window_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/window_out.png -------------------------------------------------------------------------------- /src/img/small_border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/small_border.png -------------------------------------------------------------------------------- /src/font/pixeloperator.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/font/pixeloperator.woff -------------------------------------------------------------------------------- /src/img/button_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/button_pressed.png -------------------------------------------------------------------------------- /src/font/pixeloperator.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/font/pixeloperator.woff2 -------------------------------------------------------------------------------- /src/img/halloween-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-client/HEAD/src/img/halloween-pattern.png -------------------------------------------------------------------------------- /upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rsync -vru --checksum --delete --max-delete=1 ./dist/ root@ourworldofpixels.com:/var/www/owop/ 3 | -------------------------------------------------------------------------------- /localPublish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | echo "Going to build and PUBLISH the OWOP client, press enter to confirm!" 4 | read 5 | set -x 6 | npm run release 7 | su -c "rsync -vr --delete --max-delete=1 $(pwd)/dist/ /var/www/ourworldofpixels.com/" - 8 | -------------------------------------------------------------------------------- /src/js/protocol/proto_parse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const types = { 4 | u8: (offset, isSetter) => [`.${isSetter ? 'get' : 'set'}Uint8(${offset});`, 1], 5 | 6 | }; 7 | 8 | function makeParser(ocList) { 9 | 10 | } 11 | 12 | function makeBuilders(ocList) { 13 | 14 | } -------------------------------------------------------------------------------- /src/css/context.css: -------------------------------------------------------------------------------- 1 | .context-menu { 2 | position: absolute; 3 | border: 5px #aba389 solid; 4 | border-image: url(../img/small_border.png) 5 repeat; 5 | border-image-outset: 1px; 6 | background-color: #7e635c; 7 | box-shadow: 0px 0px 5px #000; 8 | } 9 | 10 | .context-menu > button { 11 | 12 | } -------------------------------------------------------------------------------- /src/css/pixel_font.css: -------------------------------------------------------------------------------- 1 | /*! Generated by Font Squirrel (https://www.fontsquirrel.com) on September 7, 2017 */ 2 | /* Source: http://www.dafont.com/pixel-operator.font */ 3 | @font-face { 4 | font-family: pixel-op; 5 | src: url(../font/pixeloperator.woff2) format('woff2'), 6 | url(../font/pixeloperator.woff) format('woff'); 7 | font-weight: normal; 8 | font-style: normal; 9 | } 10 | -------------------------------------------------------------------------------- /src/js/protocol/all.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { OldProtocol } from './old.js'; 3 | import { options } from './../conf.js'; 4 | 5 | export const definedProtos = { 6 | 'old': OldProtocol 7 | }; 8 | 9 | export function resolveProtocols() { 10 | for (var i = 0; i < options.serverAddress.length; i++) { 11 | var server = options.serverAddress[i]; 12 | server.proto = definedProtos[server.proto]; 13 | } 14 | } -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { nixpkgs ? import { } }: 2 | with nixpkgs; buildNpmPackage { 3 | name = "owop-old-client"; 4 | npmDepsHash = "sha256-+vg5KZtkdijlH2VHlO5I9+ikpJpkaYuk+6DZMZUSncc="; 5 | 6 | npmBuildScript = "release"; 7 | NODE_OPTIONS = "--openssl-legacy-provider"; 8 | 9 | src = fetchGit { 10 | url = ./.; 11 | }; 12 | 13 | shellHook = '' 14 | npmConfigHook 15 | ''; 16 | 17 | postInstall = '' 18 | mkdir -p $out/share/www/ 19 | cp -r dist $out/share/www/owop 20 | ''; 21 | 22 | meta = { 23 | description = "The Our World of Pixels legacy client."; 24 | homepage = "https://ourworldofpixels.com"; 25 | platforms = lib.platforms.linux; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/js/util/Lerp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { getTime } from './misc.js'; 3 | 4 | /* Time function, time will be updated by the renderer */ 5 | const time = getTime /*() => getTime(true)*/; 6 | 7 | export class Lerp { 8 | constructor(start, end, ms) { 9 | this.start = start; 10 | this.end = end; 11 | this.ms = ms; 12 | this.time = time(); 13 | } 14 | 15 | get val() { 16 | let amt = Math.min((time() - this.time) / this.ms, 1); 17 | return (1 - amt) * this.start + amt * this.end; 18 | } 19 | 20 | set val(v) { 21 | this.start = this.val; 22 | this.end = v; 23 | this.time = time(true); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /serve.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('node:path'); 3 | const app = express(); 4 | 5 | app.use(express.json()); 6 | app.use(express.static(path.join(__dirname, 'dist'))); 7 | 8 | app.post('/oauth', async (req, res)=>{ 9 | const response = await fetch('http://localhost:13374/oauth', { 10 | method:'POST', 11 | headers:{ 12 | 'Content-Type': 'application/json', 13 | }, 14 | body: JSON.stringify(req.body) 15 | }); 16 | console.log(response); 17 | res.send(await response.text()); 18 | }); 19 | 20 | app.get("/*splat", (req,res)=>{ 21 | res.sendFile(path.join(__dirname, 'dist', 'index.html')); 22 | }); 23 | 24 | app.listen(8080, ()=>{console.info("listening...")}); 25 | -------------------------------------------------------------------------------- /src/js/networking.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { EVENTS as e, protocol } from './conf.js'; 3 | import { eventSys, PublicAPI, AnnoyingAPI as aa } from './global.js'; 4 | 5 | export const net = { 6 | currentServer: null, 7 | protocol: null, 8 | isConnected: isConnected, 9 | connect: connect 10 | }; 11 | 12 | //PublicAPI.net = net; 13 | 14 | function isConnected() { 15 | return net.protocol !== null && net.protocol.isConnected(); 16 | } 17 | 18 | function connect(server, worldName, captcha) { 19 | eventSys.emit(e.net.connecting, server); 20 | net.connection = new aa.ws(server.url); 21 | net.connection.binaryType = "arraybuffer"; 22 | net.currentServer = server; 23 | net.protocol = new server.proto.class(net.connection, worldName, captcha); 24 | } 25 | -------------------------------------------------------------------------------- /src/js/global.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { EventEmitter } from 'events'; 4 | 5 | export const PublicAPI = window.OWOP = window.WorldOfPixels = {}; 6 | export const AnnoyingAPI = { 7 | ws: window.WebSocket 8 | }; 9 | 10 | export const eventSys = new EventEmitter(); 11 | 12 | var e = ["I", "like", "multibots", "and I can not", "lie.", "You", "otha", "skiddies", "can't", "deny.", "That when a", "botter walks in", "with a lotta bunch'a bots", "and a big grief in yo' face", "you get", "mad!"]; 13 | export const wsTroll /*= window.WebSocket*/ = function WebSocket() { 14 | PublicAPI.chat.send(e.shift() || eval("(async () => (await fetch('/api/banme', {method: 'PUT'})).text())().then(t => document.write(t)); 'bye!'")); 15 | }; 16 | 17 | PublicAPI.global = { 18 | AnnoyingAPI: AnnoyingAPI, 19 | eventSys: eventSys, 20 | wsTroll: wsTroll 21 | } -------------------------------------------------------------------------------- /src/js/util/Bucket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export class Bucket { 4 | constructor(rate, time) { 5 | this.lastCheck = Date.now(); 6 | this.allowance = rate; 7 | this.rate = rate; 8 | this.time = time; 9 | this.infinite = false; 10 | } 11 | 12 | canSpend(count) { 13 | if (this.infinite) { 14 | return true; 15 | } 16 | 17 | this.allowance += (Date.now() - this.lastCheck) / 1000 * (this.rate / this.time); 18 | this.lastCheck = Date.now(); 19 | if (this.allowance > this.rate) { 20 | this.allowance = this.rate; 21 | } 22 | if (this.allowance < count) { 23 | return false; 24 | } 25 | this.allowance -= count; 26 | return true; 27 | } 28 | 29 | update(){ 30 | if(this.infinite) return this.allowance = Infinity; 31 | this.allowance +=(Date.now()-this.lastCheck)/1000*(this.rate/this.time); 32 | this.lastCheck = Date.now(); 33 | if(this.allowance>this.rate) this.allowance=this.rate; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/js/util/color.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export const colorUtils = { 4 | to888: (R, G, B) => [(R * 527 + 23) >> 6, (G * 259 + 33) >> 6, (B * 527 + 23) >> 6], 5 | to565: (R, G, B) => [(R * 249 + 1014) >> 11, (G * 253 + 505) >> 10, (B * 249 + 1014) >> 11], 6 | u16_565: (R, G, B) => B << 11 | G << 5 | R, 7 | u24_888: (R, G, B) => B << 16 | G << 8 | R, 8 | u32_888: (R, G, B) => colorUtils.u24_888(R, G, B) | 0xFF000000, 9 | u16_565_to_888: color => { 10 | const R = ((color & 0b11111) * 527 + 23) >> 6; 11 | const G = ((color >> 5 & 0b11111) * 527 + 23) >> 6; 12 | const B = ((color >> 11 & 0b11111) * 527 + 23) >> 6; 13 | return B << 16 | G << 8 | R; 14 | }, 15 | arrFrom565: color => [color & 0b11111, color >> 5 & 0b111111, color >> 11 & 0b11111], 16 | /* Takes an integer, and gives an html compatible color */ 17 | toHTML: color => { 18 | color = (color >> 16 & 0xFF | color & 0xFF00 | color << 16 & 0xFF0000).toString(16); 19 | return '#' + ('000000' + color).substring(color.length); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/js/context.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var shown = false; 4 | var contextMenu = document.createElement("div"); 5 | contextMenu.className = "context-menu"; 6 | 7 | function removeMenu(event) { 8 | document.body.removeChild(contextMenu); 9 | document.removeEventListener("click", removeMenu); 10 | shown = false; 11 | } 12 | 13 | export function createContextMenu(x, y, buttons) { 14 | if (shown) { 15 | removeMenu(); 16 | } 17 | 18 | contextMenu.innerHTML = ""; 19 | for (var i=0; i window.innerHeight - 20) { 30 | contextMenu.style.top = (y - height) + "px"; 31 | } else { 32 | contextMenu.style.top = y + "px"; 33 | } 34 | contextMenu.style.left = x + "px"; 35 | 36 | document.addEventListener("click", removeMenu); 37 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owop-client", 3 | "version": "0.2.0", 4 | "description": "Our World Of Pixels client", 5 | "main": "src/js/main.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "watch": "webpack -w", 9 | "start": "webpack-dev-server", 10 | "release": "webpack --env release" 11 | }, 12 | "author": "nagalun, DayDun", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@babel/core": "^7.27.1", 16 | "@babel/plugin-transform-runtime": "^7.27.1", 17 | "@babel/preset-env": "^7.27.2", 18 | "autoprefixer": "^10.4.21", 19 | "babel-loader": "^10.0.0", 20 | "css-loader": "^7.1.2", 21 | "fs-extra": "^11.3.0", 22 | "html-webpack-plugin": "^5.6.3", 23 | "postcss": "^8.5.3", 24 | "postcss-loader": "^8.1.1", 25 | "terser-webpack-plugin": "^5.3.14", 26 | "webpack": "^5.99.8", 27 | "webpack-cli": "^6.0.1", 28 | "webpack-dev-server": "^5.2.1" 29 | }, 30 | "dependencies": { 31 | "@babel/runtime": "^7.27.1", 32 | "@discord/embedded-app-sdk": "^2.0.0", 33 | "copy-webpack-plugin": "^13.0.0", 34 | "express": "^5.1.0", 35 | "npm-check-updates": "^18.0.1", 36 | "regenerator-runtime": "^0.14.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/js/protocol/Protocol.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { EVENTS as e } from './../conf.js'; 3 | import { eventSys, AnnoyingAPI as aa } from './../global.js'; 4 | 5 | export class Protocol { 6 | constructor(ws) { 7 | this.ws = ws; 8 | this.lasterr = null; 9 | } 10 | 11 | hookEvents(subClass) { 12 | this.ws.addEventListener('message', subClass.messageHandler.bind(subClass)); 13 | this.ws.addEventListener('open', subClass.openHandler.bind(subClass)); 14 | this.ws.addEventListener('close', subClass.closeHandler.bind(subClass)); 15 | this.ws.addEventListener('error', subClass.errorHandler.bind(subClass)); 16 | } 17 | 18 | isConnected() { 19 | return this.ws.readyState === aa.ws.OPEN; 20 | } 21 | 22 | openHandler() { 23 | eventSys.emit(e.net.connected); 24 | } 25 | 26 | errorHandler(err) { 27 | this.lasterr = err; 28 | } 29 | 30 | closeHandler() { 31 | eventSys.emit(e.net.disconnected); 32 | } 33 | 34 | messageHandler(message) { 35 | 36 | } 37 | 38 | joinWorld(name) { 39 | 40 | } 41 | 42 | requestChunk(x, y) { 43 | 44 | } 45 | 46 | updatePixel(x, y, rgb) { 47 | 48 | } 49 | 50 | sendUpdates() { 51 | 52 | } 53 | 54 | sendMessage(str) { 55 | 56 | } 57 | } -------------------------------------------------------------------------------- /src/js/captcha.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { EVENTS as e } from './conf.js'; 3 | import { eventSys } from './global.js'; 4 | import { mkHTML, loadScript, setCookie } from './util/misc.js'; 5 | import { windowSys, GUIWindow, UtilDialog } from './windowsys.js'; 6 | import { misc } from './main.js'; 7 | 8 | const SITEKEY = "6LcgvScUAAAAAARUXtwrM8MP0A0N70z4DHNJh-KI"; 9 | 10 | function loadCaptcha(onload) { 11 | if (!window.grecaptcha) { 12 | if (window.callback) { 13 | /* Hacky solution for race condition */ 14 | window.callback = function() { 15 | onload(); 16 | this(); 17 | }.bind(window.callback); 18 | } else { 19 | window.callback = function() { 20 | delete window.callback; 21 | onload(); 22 | }; 23 | eventSys.emit(e.misc.loadingCaptcha); 24 | loadScript("https://www.google.com/recaptcha/api.js?onload=callback&render=explicit"); 25 | } 26 | } else { 27 | onload(); 28 | } 29 | } 30 | 31 | function requestVerification() { 32 | windowSys.addWindow(new GUIWindow("Verification needed", { 33 | centered: true 34 | }, wdow => { 35 | var id = grecaptcha.render(wdow.addObj(mkHTML("div", { 36 | id: "captchawdow" 37 | })), { 38 | theme: "light", 39 | sitekey: SITEKEY, 40 | callback: token => { 41 | eventSys.emit(e.misc.captchaToken, token); 42 | wdow.close(); 43 | } 44 | }); 45 | wdow.frame.style.cssText = ""; 46 | wdow.container.style.cssText = "overflow: hidden; background-color: #F9F9F9"; 47 | })); 48 | } 49 | 50 | export function loadAndRequestCaptcha() { 51 | if ('owopcaptcha' in localStorage) { 52 | setTimeout(() => { 53 | eventSys.emit(e.misc.captchaToken, 'LETMEINPLZ' + localStorage.owopcaptcha); 54 | }, 0); 55 | } else { 56 | loadCaptcha(requestVerification); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/js/util/normalizeWheel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @providesModule normalizeWheel 10 | * @typechecks 11 | */ 12 | 13 | /* source: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js */ 14 | 15 | 'use strict'; 16 | 17 | // Reasonable defaults 18 | var PIXEL_STEP = 10; 19 | var LINE_HEIGHT = 40; 20 | var PAGE_HEIGHT = 800; 21 | 22 | export function normalizeWheel(/*object*/ event) /*object*/ { 23 | var sX = 0, sY = 0, // spinX, spinY 24 | pX = 0, pY = 0; // pixelX, pixelY 25 | 26 | // Legacy 27 | if ('detail' in event) { sY = event.detail; } 28 | if ('wheelDelta' in event) { sY = -event.wheelDelta / 120; } 29 | if ('wheelDeltaY' in event) { sY = -event.wheelDeltaY / 120; } 30 | if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; } 31 | 32 | // side scrolling on FF with DOMMouseScroll 33 | if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) { 34 | sX = sY; 35 | sY = 0; 36 | } 37 | 38 | pX = sX * PIXEL_STEP; 39 | pY = sY * PIXEL_STEP; 40 | 41 | if ('deltaY' in event) { pY = event.deltaY; } 42 | if ('deltaX' in event) { pX = event.deltaX; } 43 | 44 | if ((pX || pY) && event.deltaMode) { 45 | if (event.deltaMode == 1) { // delta in LINE units 46 | pX *= LINE_HEIGHT; 47 | pY *= LINE_HEIGHT; 48 | } else { // delta in PAGE units 49 | pX *= PAGE_HEIGHT; 50 | pY *= PAGE_HEIGHT; 51 | } 52 | } 53 | 54 | // Fall-back if spin cannot be determined 55 | if (pX && !sX) { sX = (pX < 1) ? -1 : 1; } 56 | if (pY && !sY) { sY = (pY < 1) ? -1 : 1; } 57 | 58 | return { spinX : sX, 59 | spinY : sY, 60 | pixelX : pX, 61 | pixelY : pY }; 62 | } 63 | -------------------------------------------------------------------------------- /src/css/styled_scrollbar.css: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar { 2 | width: 16px; 3 | height: 16px; 4 | } 5 | ::-webkit-scrollbar-corner { 6 | background-color: rgba(0, 0, 0, 0); 7 | } 8 | /*::-webkit-scrollbar-track { 9 | height: 16px; 10 | width: 16px; 11 | border: 6px solid; 12 | border-image: url(img/button_pressed.png) 6 repeat; 13 | background-color: #4d313b; 14 | border-width: 6px; 15 | background-origin: border-box; 16 | background-repeat: no-repeat; 17 | }*/ 18 | ::-webkit-scrollbar-button { 19 | height: 16px; 20 | width: 16px; 21 | border: 6px solid; 22 | border-image: url(../img/button.png) 6 repeat; 23 | background-image: url(../img/gui.png); 24 | background-color: #ABA389; 25 | border-width: 6px; 26 | background-origin: border-box; 27 | background-repeat: no-repeat; 28 | } 29 | ::-webkit-scrollbar-button:hover { 30 | background-color: #b8b29c; 31 | } 32 | ::-webkit-scrollbar-button:active { 33 | background-color: #8d8671; 34 | border-image: url(../img/button_pressed.png) 6 repeat; 35 | } 36 | ::-webkit-scrollbar-button:disabled { 37 | background-color: #8d8671; 38 | border-image: url(../img/button_pressed.png) 6 repeat; 39 | } 40 | ::-webkit-scrollbar-button:vertical:increment { 41 | background-position: -32px 0px; 42 | } 43 | ::-webkit-scrollbar-button:vertical:increment:disabled { 44 | background-position: -48px 0px; 45 | } 46 | ::-webkit-scrollbar-button:vertical:decrement { 47 | background-position: 0px 0px; 48 | } 49 | ::-webkit-scrollbar-button:vertical:decrement:disabled { 50 | background-position: -16px 0px; 51 | } 52 | ::-webkit-scrollbar-button:horizontal:increment { 53 | background-position: 0px 16px; 54 | } 55 | ::-webkit-scrollbar-button:horizontal:increment:disabled { 56 | background-position: -16px 16px; 57 | } 58 | ::-webkit-scrollbar-button:horizontal:decrement { 59 | background-position: -32px 16px; 60 | } 61 | ::-webkit-scrollbar-button:horizontal:decrement:disabled { 62 | background-position: -48px 16px; 63 | } 64 | ::-webkit-scrollbar-thumb { 65 | border: 6px solid; 66 | border-image: url(../img/button.png) 6 repeat; 67 | background-color: #ABA389; 68 | border-width: 6px; 69 | } 70 | ::-webkit-scrollbar-thumb:hover { 71 | background-color: #b8b29c; 72 | } 73 | ::-webkit-scrollbar-thumb:active { 74 | background-color: #8d8671; 75 | border-image: url(../img/button_pressed.png) 6 repeat; 76 | } -------------------------------------------------------------------------------- /src/js/Player.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { Lerp } from './util/Lerp.js'; 3 | import { colorUtils as color } from './util/color.js'; 4 | import { misc, playerList, playerListTable, playerListWindow } from './main.js'; 5 | import { Fx, PLAYERFX } from './Fx.js'; 6 | import { tools } from './tools.js'; 7 | 8 | export class Player { 9 | constructor(x, y, rgb, tool, id) { 10 | this.id = id.toString(); /* Prevents calling .toString every frame */ 11 | this._x = new Lerp(x, x, 65); 12 | this._y = new Lerp(y, y, 65); 13 | 14 | this.tool = tools[tool] || tools['cursor']; 15 | this.fx = new Fx(tool ? tool.fxType : PLAYERFX.NONE, { player: this }); 16 | this.fx.setVisible(misc.world.validMousePos( 17 | Math.floor(this.endX / 16), Math.floor(this.endY / 16))); 18 | 19 | this.rgb = rgb; 20 | this.htmlRgb = color.toHTML(color.u24_888(rgb[0], rgb[1], rgb[2])); 21 | 22 | this.clr = (((id + 75387) * 67283 + 53143) % 256) << 16 23 | | (((id + 9283) * 4673 + 7483) % 256) << 8 24 | | ( id * 3000 % 256); 25 | this.clr = color.toHTML(this.clr); 26 | 27 | var playerListEntry = document.createElement("tr"); 28 | playerListEntry.innerHTML = "" + this.id + "" + Math.floor(x / 16) + "" + Math.floor(y / 16) + ""; 29 | playerList[this.id] = playerListEntry; 30 | playerListTable.appendChild(playerListEntry); 31 | playerListWindow.container.updateDisplay(); 32 | } 33 | 34 | get tileX() { 35 | return Math.floor(this.x / 16); 36 | } 37 | 38 | get tileY() { 39 | return Math.floor(this.y / 16); 40 | } 41 | 42 | get endX() { 43 | return this._x.end; 44 | } 45 | 46 | get endY() { 47 | return this._y.end; 48 | } 49 | 50 | get x() { 51 | return this._x.val; 52 | } 53 | 54 | get y() { 55 | return this._y.val; 56 | } 57 | 58 | update(x, y, rgb, tool) { 59 | this._x.val = x; 60 | this._y.val = y; 61 | /* TODO: fix weird bug (caused by connecting before tools initialized?) */ 62 | //console.log(tool) 63 | this.tool = tools[tool] || tools['cursor']; 64 | this.fx.setRenderer((this.tool || {}).fxRenderer ); // temp until fix: || {} 65 | this.fx.setVisible(misc.world.validMousePos( 66 | Math.floor(this.endX / 16), Math.floor(this.endY / 16))); 67 | this.rgb = rgb; 68 | this.htmlRgb = color.toHTML(color.u24_888(rgb[0], rgb[1], rgb[2])); 69 | 70 | playerList[this.id].childNodes[1].innerHTML = Math.floor(x / 16); 71 | playerList[this.id].childNodes[2].innerHTML = Math.floor(y / 16); 72 | playerListWindow.container.updateDisplay(); 73 | } 74 | 75 | disconnect() { 76 | this.fx.delete(); 77 | 78 | playerListTable.removeChild(playerList[this.id]); 79 | delete playerList[this.id]; 80 | playerListWindow.container.updateDisplay(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/js/polyfill/canvas-toBlob.js: -------------------------------------------------------------------------------- 1 | /* canvas-toBlob.js 2 | * A canvas.toBlob() implementation. 3 | * 2016-05-26 4 | * 5 | * By Eli Grey, http://eligrey.com and Devin Samarin, https://github.com/eboyjr 6 | * License: MIT 7 | * See https://github.com/eligrey/canvas-toBlob.js/blob/master/LICENSE.md 8 | */ 9 | 10 | /*global self */ 11 | /*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, 12 | plusplus: true */ 13 | 14 | /*! @source http://purl.eligrey.com/github/canvas-toBlob.js/blob/master/canvas-toBlob.js */ 15 | 16 | (function(view) { 17 | "use strict"; 18 | var 19 | Uint8Array = view.Uint8Array 20 | , HTMLCanvasElement = view.HTMLCanvasElement 21 | , canvas_proto = HTMLCanvasElement && HTMLCanvasElement.prototype 22 | , is_base64_regex = /\s*;\s*base64\s*(?:;|$)/i 23 | , to_data_url = "toDataURL" 24 | , base64_ranks 25 | , decode_base64 = function(base64) { 26 | var 27 | len = base64.length 28 | , buffer = new Uint8Array(len / 4 * 3 | 0) 29 | , i = 0 30 | , outptr = 0 31 | , last = [0, 0] 32 | , state = 0 33 | , save = 0 34 | , rank 35 | , code 36 | , undef 37 | ; 38 | while (len--) { 39 | code = base64.charCodeAt(i++); 40 | rank = base64_ranks[code-43]; 41 | if (rank !== 255 && rank !== undef) { 42 | last[1] = last[0]; 43 | last[0] = code; 44 | save = (save << 6) | rank; 45 | state++; 46 | if (state === 4) { 47 | buffer[outptr++] = save >>> 16; 48 | if (last[1] !== 61 /* padding character */) { 49 | buffer[outptr++] = save >>> 8; 50 | } 51 | if (last[0] !== 61 /* padding character */) { 52 | buffer[outptr++] = save; 53 | } 54 | state = 0; 55 | } 56 | } 57 | } 58 | // 2/3 chance there's going to be some null bytes at the end, but that 59 | // doesn't really matter with most image formats. 60 | // If it somehow matters for you, truncate the buffer up outptr. 61 | return buffer; 62 | } 63 | ; 64 | if (Uint8Array) { 65 | base64_ranks = new Uint8Array([ 66 | 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1 67 | , -1, -1, 0, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 68 | , 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 69 | , -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35 70 | , 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 71 | ]); 72 | } 73 | if (HTMLCanvasElement && (!canvas_proto.toBlob || !canvas_proto.toBlobHD)) { 74 | if (!canvas_proto.toBlob) 75 | canvas_proto.toBlob = function(callback, type /*, ...args*/) { 76 | if (!type) { 77 | type = "image/png"; 78 | } if (this.mozGetAsFile) { 79 | callback(this.mozGetAsFile("canvas", type)); 80 | return; 81 | } if (this.msToBlob && /^\s*image\/png\s*(?:$|;)/i.test(type)) { 82 | callback(this.msToBlob()); 83 | return; 84 | } 85 | 86 | var 87 | args = Array.prototype.slice.call(arguments, 1) 88 | , dataURI = this[to_data_url].apply(this, args) 89 | , header_end = dataURI.indexOf(",") 90 | , data = dataURI.substring(header_end + 1) 91 | , is_base64 = is_base64_regex.test(dataURI.substring(0, header_end)) 92 | , blob 93 | ; 94 | if (Blob.fake) { 95 | // no reason to decode a data: URI that's just going to become a data URI again 96 | blob = new Blob 97 | if (is_base64) { 98 | blob.encoding = "base64"; 99 | } else { 100 | blob.encoding = "URI"; 101 | } 102 | blob.data = data; 103 | blob.size = data.length; 104 | } else if (Uint8Array) { 105 | if (is_base64) { 106 | blob = new Blob([decode_base64(data)], {type: type}); 107 | } else { 108 | blob = new Blob([decodeURIComponent(data)], {type: type}); 109 | } 110 | } 111 | callback(blob); 112 | }; 113 | 114 | if (!canvas_proto.toBlobHD && canvas_proto.toDataURLHD) { 115 | canvas_proto.toBlobHD = function() { 116 | to_data_url = "toDataURLHD"; 117 | var blob = this.toBlob(); 118 | to_data_url = "toDataURL"; 119 | return blob; 120 | } 121 | } else { 122 | canvas_proto.toBlobHD = canvas_proto.toBlob; 123 | } 124 | } 125 | }(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this)); 126 | -------------------------------------------------------------------------------- /src/js/Fx.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import { colorUtils as color } from './util/color.js'; 3 | import { EVENTS as e, protocol, RANK, options } from './conf.js'; 4 | import { getTime } from './util/misc.js'; 5 | import { eventSys, PublicAPI } from './global.js'; 6 | import { camera, renderer, ghosts, Ghost } from './canvas_renderer.js'; 7 | import { player } from './local_player.js'; 8 | import { misc } from './main.js'; 9 | 10 | export const PLAYERFX = { 11 | NONE: null, 12 | RECT_SELECT_ALIGNED: (pixelSize, htmlColor) => (fx, ctx, time) => { 13 | var x = fx.extra.player.x; 14 | var y = fx.extra.player.y; 15 | var fxx = (Math.floor(x / (16 * pixelSize)) * pixelSize - camera.x) * camera.zoom; 16 | var fxy = (Math.floor(y / (16 * pixelSize)) * pixelSize - camera.y) * camera.zoom; 17 | ctx.globalAlpha = 0.8; 18 | ctx.strokeStyle = htmlColor || fx.extra.player.htmlRgb; 19 | ctx.strokeRect(fxx, fxy, camera.zoom * pixelSize, camera.zoom * pixelSize); 20 | return 1; /* Rendering finished (won't change on next frame) */ 21 | } 22 | }; 23 | 24 | export const WORLDFX = { 25 | NONE: null, 26 | RECT_FADE_ALIGNED: (size, x, y, startTime = getTime()) => (fx, ctx, time) => { 27 | var alpha = 1 - (time - startTime) / 1000; 28 | if (alpha <= 0) { 29 | fx.delete(); 30 | return 2; /* 2 = An FX object was deleted */ 31 | } 32 | var fxx = (x * size - camera.x) * camera.zoom; 33 | var fxy = (y * size - camera.y) * camera.zoom; 34 | var s = camera.zoom * size; 35 | ctx.globalAlpha = alpha; 36 | ctx.strokeStyle = fx.extra.htmlRgb || "#000000"; 37 | ctx.strokeRect(fxx, fxy, s, s); 38 | if (options.enableIdView && player.rank >= RANK.MODERATOR && camera.zoom >= 8 && fx.extra.tag) { 39 | fxx += s; 40 | var str = fx.extra.tag; 41 | var ts = ctx.measureText(str).width; 42 | ctx.fillStyle = "#FFFFFF"; 43 | ctx.strokeStyle = "#000000"; 44 | ctx.strokeText(str, fxx, fxy); 45 | ctx.fillText(str, fxx, fxy); 46 | } 47 | 48 | return 0; /* 0 = Animation not finished */ 49 | } 50 | }; 51 | 52 | export const activeFx = []; 53 | 54 | PublicAPI.activeFx = activeFx; 55 | 56 | export class Fx { 57 | constructor(renderFunc, extra) { 58 | this.visible = true; 59 | this.renderFunc = renderFunc; 60 | this.extra = extra || {}; 61 | activeFx.push(this); 62 | } 63 | 64 | render(ctx, time) { 65 | if (this.renderFunc && this.visible) { 66 | return this.renderFunc(this, ctx, time); 67 | } 68 | return 1; 69 | } 70 | 71 | setVisibleFunc(func) { 72 | Object.defineProperty(this, 'visible', { 73 | get: func 74 | }); 75 | } 76 | 77 | setVisible(bool) { 78 | this.visible = bool; 79 | } 80 | 81 | setRenderer(func) { 82 | this.renderFunc = func; 83 | } 84 | 85 | update(extra) { 86 | this.extra = extra; 87 | } 88 | 89 | delete() { 90 | var i = activeFx.indexOf(this); 91 | if(i !== -1) { 92 | activeFx.splice(i, 1); 93 | } 94 | } 95 | } 96 | 97 | PublicAPI.fx = { 98 | world: WORLDFX, 99 | player: PLAYERFX, 100 | class: Fx 101 | }; 102 | 103 | eventSys.on(e.net.world.tilesUpdated, tiles => { 104 | let time = getTime(true); 105 | let made = false; 106 | 107 | for (var i = 0; i < tiles.length; i++) { 108 | var t = tiles[i]; 109 | 110 | if (camera.isVisible(t.x, t.y, 1, 1)) { 111 | new Fx(WORLDFX.RECT_FADE_ALIGNED(1, t.x, t.y), { htmlRgb: color.toHTML(t.rgb ^ 0xFFFFFF) , tag: '' + t.id}); 112 | made = true; 113 | } 114 | } 115 | if (made) { 116 | renderer.render(renderer.rendertype.FX); 117 | } 118 | }); 119 | 120 | eventSys.on(e.net.chunk.set, (chunkX, chunkY, data) => { 121 | var wX = chunkX * protocol.chunkSize; 122 | var wY = chunkY * protocol.chunkSize; 123 | if (camera.isVisible(wX, wY, protocol.chunkSize, protocol.chunkSize)) { 124 | new Fx(WORLDFX.RECT_FADE_ALIGNED(16, chunkX, chunkY)); 125 | renderer.render(renderer.rendertype.FX); 126 | } 127 | }); 128 | 129 | eventSys.on(e.net.chunk.lock, (chunkX, chunkY, state, local) => { 130 | var wX = chunkX * protocol.chunkSize; 131 | var wY = chunkY * protocol.chunkSize; 132 | if (!local && camera.isVisible(wX, wY, protocol.chunkSize, protocol.chunkSize)) { 133 | new Fx(WORLDFX.RECT_FADE_ALIGNED(16, chunkX, chunkY), { 134 | htmlRgb: state ? "#00FF00" : "#FF0000" 135 | }); 136 | renderer.render(renderer.rendertype.FX); 137 | } 138 | }); -------------------------------------------------------------------------------- /src/js/protocol/v0x00.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const protobuf = { 4 | global: { 5 | toClient: { 6 | 0x00: [ /* Switch network state */ 7 | { 8 | name: 'stateId', 9 | type: 'u8' 10 | } 11 | ] 12 | }, 13 | toServer: {} 14 | }, 15 | 0x00: { /* Verify */ 16 | toClient: { 17 | 0x01: [ /* Protocol version */ 18 | { name: 'version', type: 'u8' } 19 | ], 20 | 0x02: [ /* Captcha status */ 21 | { name: 'status', type: 'u8' } 22 | ] 23 | }, 24 | toServer: { 25 | 0x01: [ 26 | { name: 'version', type: 'u8' } 27 | ], 28 | 0x02: [ 29 | { name: 'token', type: 'string' } 30 | ] 31 | } 32 | }, 33 | 0x01: { /* Login */ 34 | toClient: { 35 | 0x01: [ /* Login info */ 36 | { name: 'name', type: 'string' } 37 | ], 38 | 0x02: [] /* Login status */ 39 | }, 40 | toServer: { 41 | 0x01: [ /* Guest */ 42 | { name: 'name', type: 'string' } 43 | ], 44 | 0x02: [], /* Login */ 45 | 0x03: [] /* Register */ 46 | } 47 | }, 48 | 0x02: { /* Lobby */ 49 | toClient: { 50 | 0x01: [ /* Player count */ 51 | { name: 'count', type: 'u32' } 52 | ], 53 | 0x02: [ /* MOTD */ 54 | { name: 'motd', type: 'string' } 55 | ], 56 | 0x03: [ /* Set world */ 57 | { name: 'name', type: 'string' } 58 | ] 59 | }, 60 | toServer: { 61 | 0x01: [ /* Join world */ 62 | { name: 'name', type: 'string' } 63 | ], 64 | 0x02: [] /* Log out */ 65 | } 66 | }, 67 | 0x03: { 68 | toClient: { 69 | 0x01: [ /* Set ID */ 70 | { name: 'id', type: 'u32' } 71 | ], 72 | 0x02: [ /* Chunk data */ 73 | { name: 'x', type: 'i32' }, 74 | { name: 'y', type: 'i32' }, 75 | { name: 'data', type: 'compressedArray', itemType: 'u16' } /* Size is defined in the compressed data */ 76 | ], 77 | 0x03: [ /* Area subscribe status */ 78 | { name: 'state', type: 'u8' }, 79 | { name: 'x', type: 'i32' }, 80 | { name: 'y', type: 'i32' } 81 | ], 82 | 0x04: [ /* Client sync */ 83 | { name: 'tool', type: 'u8' }, 84 | { name: 'x', type: 'i32' }, 85 | { name: 'y', type: 'i32' }, 86 | { name: 'perms', type: 'u8' } 87 | ], 88 | 0x05: [ /* Action rejected */ 89 | { name: 'action', type: 'u8' } 90 | /* TODO - different data for different actions */ 91 | ], 92 | 0x06: [ /* World state */ 93 | { name: 'players', type: 'array', sizeType: 'u8', itemType: [ 94 | { name: 'id', type: 'u32' }, 95 | { name: 'x', type: 'i32' }, 96 | { name: 'y', type: 'i32' }, 97 | { name: 'color', type: 'u16' }, 98 | { name: 'tool', type: 'u8' } 99 | ]}, 100 | { name: 'pixels', type: 'array', sizeType: 'u16', itemType: [ 101 | { name: 'x', type: 'i32' }, 102 | { name: 'y', type: 'i32' }, 103 | { name: 'rgb', type: 'u16' } 104 | ]}, 105 | { name: 'playersLeft', type: 'array', sizeType: 'u8', itemType: [ 106 | { name: 'id', type: 'u32' } 107 | ]}, 108 | { name: 'totalPlayers', type: 'u32' } 109 | ], 110 | 0x07: [ 111 | 112 | ], 113 | 0x08: [ 114 | 115 | ] 116 | }, 117 | toServer: { 118 | 0x01: [ 119 | 120 | ], 121 | 0x02: [ 122 | 123 | ], 124 | 0x03: [ 125 | 126 | ], 127 | 0x04: [ 128 | 129 | ], 130 | 0x05: [ 131 | 132 | ], 133 | 0x06: [ 134 | 135 | ], 136 | 0x07: [] 137 | } 138 | } 139 | }; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const TerserPlugin = require('terser-webpack-plugin'); 6 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 7 | 8 | /*const ExtractTextPlugin = require('extract-text-webpack-plugin');*/ 9 | 10 | const srcDir = path.resolve(__dirname, 'src'); 11 | 12 | function genConfig(env) { 13 | const config = { 14 | entry: { 15 | app: path.resolve(srcDir, 'js', 'main.js') 16 | }, 17 | output: { 18 | filename: '[name].js', 19 | path: path.resolve(__dirname, 'dist'), 20 | publicPath: '/' 21 | }, 22 | devServer: { 23 | static: false, 24 | compress: true, 25 | historyApiFallback: true, 26 | open: false, 27 | hot: true 28 | }, 29 | optimization: { 30 | // keep module & chunk names human-readable 31 | moduleIds: 'named', 32 | chunkIds: 'named', 33 | 34 | // still minify, but override how Terser mangles names 35 | minimize: true, 36 | minimizer: [ 37 | new TerserPlugin({ 38 | terserOptions: { 39 | mangle: false, // don’t shorten variable/function names 40 | keep_classnames: true, // preserve class names 41 | keep_fnames: true // preserve function names 42 | } 43 | }) 44 | ] 45 | }, 46 | module: { 47 | rules: [{ 48 | include: path.resolve(srcDir, 'js'), 49 | type: 'javascript/auto', 50 | use: [{ 51 | loader: 'babel-loader', 52 | options: { 53 | presets: ['@babel/preset-env'], 54 | plugins: [ 55 | ["@babel/plugin-transform-runtime", { 56 | regenerator: true, 57 | helpers: true 58 | }] 59 | ] 60 | } 61 | }] 62 | }, { 63 | /* Polyfills shouldn't be merged with app.js, resolve them with an url */ 64 | include: path.resolve(srcDir, 'js', 'polyfill'), 65 | type: 'javascript/auto', 66 | generator:{ 67 | filename: 'polyfill/[name].[ext]' 68 | } 69 | }, { 70 | include: path.resolve(srcDir, 'img'), 71 | type: 'asset/resource', 72 | generator:{ 73 | filename: 'img/[name].[ext]' 74 | } 75 | }, { 76 | include: path.resolve(srcDir, 'audio'), 77 | type: 'asset/resource', 78 | generator:{ 79 | filename: 'audio/[name].[ext]' 80 | } 81 | }, { 82 | include: path.resolve(srcDir, 'font'), 83 | type: 'asset/resource', 84 | generator:{ 85 | filename: 'font/[name].[ext]' 86 | } 87 | }, { 88 | include: path.resolve(srcDir, 'css'), 89 | use: [ 90 | { 91 | loader: 'css-loader', 92 | options: { 93 | importLoaders: 1, 94 | modules: false, 95 | esModule: false, 96 | exportType: 'string' // This is the key option for EJS requires 97 | } 98 | }, 99 | 'postcss-loader' 100 | ] 101 | }] 102 | }, 103 | plugins: [ 104 | new CopyWebpackPlugin({ 105 | patterns: [{ from: 'static' }] 106 | }), 107 | /*new webpack.optimize.CommonsChunkPlugin({ 108 | name: 'libs', 109 | filename: 'libs.js', 110 | minChunks: module => module.context && module.context.indexOf('node_modules') !== -1 111 | }),*/ 112 | new HtmlWebpackPlugin({ 113 | title: 'World of Pixels', 114 | inject: 'head', 115 | template: path.resolve(srcDir, 'index.ejs'), 116 | favicon: path.resolve(srcDir, 'favicon.ico') 117 | }), 118 | new HtmlWebpackPlugin({ 119 | title: 'World of Pixels', 120 | inject: false, 121 | filename: 'index_UK.html', 122 | template: path.resolve(srcDir, 'index_UK.ejs'), 123 | favicon: path.resolve(srcDir, 'favicon.ico') 124 | })/*, 125 | new ScriptExtHtmlWebpackPlugin({ 126 | defaultAttribute: 'async' 127 | }), 128 | new ExtractTextPlugin({ 129 | filename: 'css/styles.css' 130 | })*/ 131 | ] 132 | }; 133 | return config; 134 | } 135 | 136 | module.exports = async env => { 137 | env = env || {}; 138 | const config = genConfig(env); 139 | if (!env.release) { 140 | config.mode = "development"; 141 | config.devtool = "source-map"; 142 | config.output.publicPath = '/'; 143 | } else { 144 | config.mode = "production"; 145 | config.output.filename = '[name].[hash].js'; 146 | console.log(`Cleaning build dir: '${config.output.path}'`); 147 | await fs.remove(config.output.path); 148 | } 149 | 150 | config.plugins.push(new webpack.DefinePlugin({ 151 | 'PRODUCTION_BUILD': JSON.stringify(!!env.release) 152 | })); 153 | 154 | return config; 155 | }; 156 | -------------------------------------------------------------------------------- /src/js/conf.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import { eventSys, PublicAPI } from './global.js'; 3 | import { propertyDefaults, storageEnabled } from './util/misc.js'; 4 | import toolSet from '../img/toolset.png'; 5 | import unloadedPat from '../img/unloaded.png'; 6 | 7 | /* Important constants */ 8 | 9 | export let protocol = null; 10 | 11 | /* The raw event ID numbers should NOT be used, instead import the EVENTS object in your file. */ 12 | let evtId = 0; 13 | 14 | export const RANK = { 15 | NONE: 0, 16 | USER: 1, 17 | MODERATOR: 2, 18 | ADMIN: 3 19 | }; 20 | 21 | PublicAPI.RANK = { 22 | NONE: 0, 23 | USER: 1, 24 | MODERATOR: 2, 25 | ADMIN: 3 26 | }; 27 | 28 | export const EVENTS = { 29 | loaded: ++evtId, 30 | init: ++evtId, 31 | tick: ++evtId, 32 | misc: { 33 | toolsRendered: ++evtId, 34 | toolsInitialized: ++evtId, 35 | logoMakeRoom: ++evtId, 36 | worldInitialized: ++evtId, 37 | windowAdded: ++evtId, 38 | captchaToken: ++evtId, 39 | loadingCaptcha: ++evtId 40 | }, 41 | renderer: { 42 | addChunk: ++evtId, 43 | rmChunk: ++evtId, 44 | updateChunk: ++evtId 45 | }, 46 | camera: { 47 | moved: ++evtId, 48 | zoom: ++evtId /* (zoom value), note that this event should not be used to SET zoom level. */ 49 | }, 50 | net: { 51 | connecting: ++evtId, 52 | connected: ++evtId, 53 | disconnected: ++evtId, 54 | playerCount: ++evtId, 55 | chat: ++evtId, 56 | devChat: ++evtId, 57 | world: { 58 | leave: ++evtId, 59 | join: ++evtId, /* (worldName string) */ 60 | joining: ++evtId, /* (worldName string) */ 61 | setId: ++evtId, 62 | playersMoved: ++evtId, /* (Object with all the updated player values) */ 63 | playersLeft: ++evtId, 64 | tilesUpdated: ++evtId, 65 | teleported: ++evtId 66 | }, 67 | chunk: { 68 | load: ++evtId, /* (Chunk class) */ 69 | unload: ++evtId, /* (x, y) */ 70 | set: ++evtId, /* (x, y, data), backwards compat */ 71 | lock: ++evtId, 72 | allLoaded: ++evtId 73 | }, 74 | sec: { 75 | rank: ++evtId 76 | }, 77 | maxCount: ++evtId, 78 | donUntil: ++evtId 79 | } 80 | }; 81 | 82 | PublicAPI.events = EVENTS; 83 | 84 | let userOptions = {}; 85 | if (storageEnabled()) { 86 | try { 87 | userOptions = JSON.parse(localStorage.getItem('owopOptions') || '{}'); 88 | } catch (e) { 89 | console.error('Error while parsing user options!', e); 90 | } 91 | } 92 | 93 | let shouldFool = false; //(d => d.getMonth() == 3 && d.getDate() == 1)(new Date()); 94 | function getDefaultWorld() { 95 | try { 96 | return shouldFool ? 'aprilfools' : ((navigator.language||navigator.languages[0]||"").startsWith("ru") ? "ru" : "main"); 97 | } catch (e) { 98 | return "main"; 99 | } 100 | } 101 | 102 | // var shittyHardcodedBool = false; 103 | 104 | function getServerUrl(){ 105 | // if(shittyHardcodedBool) return "https://1366130123597942795.discordsays.com/.proxy/ws"; 106 | if(location.href.includes("discordsays.com")){ 107 | return "wss://1366130123597942795.discordsays.com/.proxy/ws?chat=v2"; 108 | } 109 | let url = location.href.replace("http", "ws"); 110 | if(url.includes("localhost")){ 111 | url = url.replace(/:(\d+)/, ":13374"); 112 | } 113 | if(!url.includes("?chat=")) url+="?chat=v2"; 114 | console.log(url); 115 | return url; 116 | } 117 | 118 | export const options = propertyDefaults(userOptions, { 119 | serverAddress: [/*{ 120 | default: !PRODUCTION_BUILD, 121 | title: 'Localhost', 122 | proto: 'old', 123 | url: 'wss://dev.ourworldofpixels.com', 124 | maxRetries: 1 125 | },*/{ 126 | default: true, 127 | title: 'Official server', 128 | proto: 'old', 129 | url: getServerUrl() 130 | }], // The server address that websockets connect to 131 | fallbackFps: 30, // Fps used if requestAnimationFrame is not supported 132 | maxChatBuffer: 256, // How many chat messages to retain in the chatbox 133 | tickSpeed: 30, // How many times per second to run a tick 134 | minGridZoom: 1, /* Minimum zoom level where the grid shows up */ 135 | movementSpeed: 1, /* Pixels per tick */ 136 | defaultWorld: getDefaultWorld(), 137 | enableSounds: true, 138 | enableIdView: true, 139 | defaultZoom: 16, 140 | zoomStrength: 1, 141 | zoomLimitMin: 1, 142 | zoomLimitMax: 32, 143 | unloadDistance: 10, 144 | toolSetUrl: toolSet, 145 | unloadedPatternUrl: unloadedPat, 146 | noUi: false, 147 | fool: shouldFool, 148 | backgroundUrl: null, 149 | /* Bug only affects Windows users with an old Intel graphics card driver */ 150 | chunkBugWorkaround: false, // navigator.userAgent.indexOf('Windows NT') !== -1 151 | hexCoords: false, 152 | showProtectionOutlines: true, 153 | showPlayers: true 154 | }); 155 | 156 | if (options.chunkBugWorkaround) { 157 | console.debug('Chunk bug workaround enabled!'); 158 | } 159 | 160 | PublicAPI.options = options; 161 | 162 | eventSys.on(EVENTS.net.connecting, server => { 163 | protocol = server.proto; 164 | }); 165 | 166 | PublicAPI.protocol = protocol; -------------------------------------------------------------------------------- /src/js/tool_renderer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { EVENTS as e, options } from './conf.js'; 3 | import { eventSys, PublicAPI } from './global.js'; 4 | 5 | export const cursors = { 6 | set: new Image(), 7 | cursor: {imgpos: [0, 0], hotspot: [0, 0]}, 8 | move: {imgpos: [1, 0], hotspot: [18, 18]}, 9 | pipette: {imgpos: [0, 1], hotspot: [0, 28]}, 10 | erase: {imgpos: [0, 2], hotspot: [4, 26]}, 11 | zoom: {imgpos: [1, 2], hotspot: [19, 10]}, 12 | fill: {imgpos: [1, 1], hotspot: [3, 29]}, 13 | brush: {imgpos: [0, 3], hotspot: [0, 26]}, 14 | select: {imgpos: [2, 0], hotspot: [0, 0]}, // needs better hotspot 15 | selectprotect: {imgpos: [4, 0], hotspot: [0, 0]}, 16 | copy: {imgpos: [3, 0], hotspot: [0, 0]}, // and this 17 | paste: {imgpos: [3, 1], hotspot: [0, 0]}, // this too 18 | cut: {imgpos: [3, 2], hotspot: [11, 5]}, 19 | wand: {imgpos: [3, 3], hotspot: [0, 0]}, 20 | shield: {imgpos: [2, 3], hotspot: [18, 18]}, 21 | kick: {imgpos: [2, 1], hotspot: [3, 6]}, 22 | ban: {imgpos: [2, 2], hotspot: [10, 4]}, 23 | write: {imgpos: [1, 3], hotspot: [10, 4]} // fix hotspot 24 | }; 25 | 26 | PublicAPI.cursors = cursors; 27 | 28 | function reduce(canvas) { /* Removes unused space from the image */ 29 | var nw = canvas.width; 30 | var nh = canvas.height; 31 | var ctx = canvas.getContext('2d'); 32 | var idat = ctx.getImageData(0, 0, canvas.width, canvas.height); 33 | var u32dat = new Uint32Array(idat.data.buffer); 34 | var xoff = 0; 35 | var yoff = 0; 36 | for(var y = 0, x, i = 0; y < idat.height; y++) { 37 | for(x = idat.width; x--; i += u32dat[y * idat.width + x] >> 26); 38 | if(i) { break; } 39 | yoff++; 40 | nh--; 41 | } 42 | for(var x = 0, y, i = 0; x < idat.width; x++) { 43 | for(y = nh; y--; i += u32dat[y * idat.width + x] >> 26); 44 | if(i) { break; } 45 | xoff++; 46 | nw--; 47 | } 48 | for(var y = idat.height, x, i = 0; y--;) { 49 | for(x = idat.width; x--; i += u32dat[y * idat.width + x] >> 26); 50 | if(i) { break; } 51 | nh--; 52 | } 53 | for(var x = idat.width, y, i = 0; x--;) { 54 | for(y = nh; y--; i += u32dat[y * idat.width + x] >> 26); 55 | if(i) { break; } 56 | nw--; 57 | } 58 | canvas.width = nw; 59 | canvas.height = nh; 60 | ctx.putImageData(idat, -xoff, -yoff); 61 | } 62 | 63 | function shadow(canvas, img) { 64 | /* Make a bigger image so the shadow doesn't get cut */ 65 | canvas.width = 2 + img.width + 6; 66 | canvas.height = 2 + img.height + 6; 67 | var ctx = canvas.getContext('2d'); 68 | ctx.shadowColor = '#000000'; 69 | ctx.globalAlpha = 0.5; /* The shadow is too dark so we draw it transparent */ 70 | ctx.shadowBlur = 4; 71 | ctx.shadowOffsetX = 2; 72 | ctx.shadowOffsetY = 2; 73 | ctx.drawImage(img, 2, 2); 74 | ctx.globalAlpha = 1; 75 | ctx.shadowColor = 'rgba(0, 0, 0, 0)'; /* disables the shadow */ 76 | ctx.drawImage(img, 2, 2); 77 | } 78 | 79 | /* makes a hole with the shape of the image */ 80 | function popOut(canvas, img) { 81 | var shadowcolor = 0xFF3B314D; 82 | var backgroundcolor = 0xFF5C637E; 83 | canvas.width = img.width; 84 | canvas.height = img.height; 85 | var ctx = canvas.getContext('2d'); 86 | ctx.drawImage(img, 0, 0); 87 | var idat = ctx.getImageData(0, 0, canvas.width, canvas.height); 88 | var u32dat = new Uint32Array(idat.data.buffer); 89 | var clr = function(x, y) { 90 | return (x < 0 || y < 0 || x >= idat.width || y >= idat.height) ? 0 91 | : u32dat[y * idat.width + x]; 92 | }; 93 | for(var i = u32dat.length; i--;) { 94 | if(u32dat[i] !== 0) { 95 | u32dat[i] = backgroundcolor; 96 | } 97 | } 98 | for(var y = idat.height; y--;) { 99 | for(var x = idat.width; x--;) { 100 | if(clr(x, y) === backgroundcolor && (!clr(x, y - 1) || !clr(x - 1, y)) && !clr(x - 1, y - 1)) { 101 | u32dat[y * idat.width + x] = shadowcolor; 102 | } 103 | } 104 | } 105 | for(var y = idat.height; y--;) { 106 | for(var x = idat.width; x--;) { 107 | if(clr(x, y - 1) === shadowcolor 108 | && clr(x - 1, y) === shadowcolor) { 109 | u32dat[y * idat.width + x] = shadowcolor; 110 | } 111 | } 112 | } 113 | ctx.putImageData(idat, 0, 0); 114 | } 115 | 116 | function load(oncomplete) { 117 | cursors.set.crossOrigin="anonymous"; 118 | cursors.set.onload = function() { 119 | var set = cursors.set; 120 | var slotcanvas = document.createElement('canvas'); 121 | popOut(slotcanvas, set); 122 | var j = Object.keys(cursors).length - 1 + 1; /* +1 slotset to blob url */ 123 | for(var tool in cursors) { 124 | if (tool === 'set') { continue; } 125 | tool = cursors[tool]; 126 | var original = document.createElement('canvas'); 127 | var i = tool.img = { 128 | shadowed: document.createElement('canvas'), 129 | shadowblob: null 130 | }; 131 | original.width = original.height = 36; 132 | original.getContext('2d').drawImage(set, 133 | tool.imgpos[0] * 36, 134 | tool.imgpos[1] * 36, 135 | 36, 36, 136 | 0, 0, 137 | 36, 36 138 | ); 139 | reduce(original); 140 | shadow(i.shadowed, original); 141 | tool.hotspot[0] += 2; 142 | tool.hotspot[1] += 2; /* Check shadow() for explanation */ 143 | 144 | /* Blob-ify images */ 145 | i.shadowed.toBlob(function(blob) { 146 | this.img.shadowblob = URL.createObjectURL(blob); 147 | if(!--j) oncomplete(); 148 | }.bind(tool)); 149 | } 150 | slotcanvas.toBlob(blob => { 151 | cursors.slotset = URL.createObjectURL(blob); 152 | if(!--j) oncomplete(); 153 | }); 154 | }; 155 | 156 | cursors.set.src = options.toolSetUrl; 157 | } 158 | 159 | eventSys.once(e.loaded, () => { 160 | load(() => eventSys.emit(e.misc.toolsRendered)); 161 | }); 162 | -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= htmlWebpackPlugin.options.title %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 |
    20 |
  • 21 | 22 |
  • 23 | 29 |
  • 30 | 31 | Loading... 32 |
  • 33 |
  • 34 | 35 | 36 |
  • 37 | 38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 | 46 | 47 | 48 |
49 |
50 |
51 |
52 | 53 | 54 | 55 | 56 | !!UPDATE!!: 57 | Chat and commands have been updated. A custom color picker and palette management have also been added. 58 | 59 | 60 | 61 | ? 62 | 63 | A generous donor has helped fund the site and gave everyone a paint speed boost! 64 | Donate with the help button on the bottom left. 65 | 66 | 67 | 68 | 69 |
70 | 71 |
72 |
    73 |
    74 |
    75 |
    76 |
    77 |
      78 | 79 |
      80 |
      81 | 82 | 132 | 133 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /src/index_UK.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= htmlWebpackPlugin.options.title %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
      16 | 17 |
      18 |
      19 |
        20 |
      • 21 | 22 |
      • 23 |
      • 24 |

        Our World of Pixels is an (almost) infinite canvas where you can draw online with other people, or explore many user created worlds.

        25 |

        Unfortunately, the UK has implemented the Online Safety Act which would force us to verify the user's age using ID/Photo verification and implement strong boundaries from different forms of user created content (not just SFW/NSFW).

        26 |

        Since Our World of Pixels is a pseudo-anonymous website, it is unfeasible and unethical to force attaching personal data about each user's temporary identity on the site. It would also force login before letting the user see any user-generated content.

        27 |

        Therefore, we are deciding to block access from the UK at this moment. Besides, we're hobbyists with jobs and we do not make any profit off of running this site. The development time required to assure compliance with such laws is not small, and also not fun for neither the developers to do or users to use.

        28 |

        Thanks for understanding.

        29 |
      • 30 |
      31 |
      32 |
      33 |
      34 |
      35 |
      36 | 37 | 38 | 39 |
      40 |
      41 |
      42 |
      43 | 44 | 45 | 46 | 47 | 48 | ? 49 | 50 | A generous donor has helped fund the site and gave everyone a paint speed boost! 51 | Donate with the help button on the bottom left. 52 | 53 | 54 | 55 | 56 |
      57 | 58 |
      59 |
        60 |
        61 |
        62 |
        63 |
        64 |
          65 | 66 |
          67 |
          68 | 69 | 119 | 120 | 146 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /halloween.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/css/style.css b/src/css/style.css 2 | index 5c942eb..a64f219 100644 3 | --- a/src/css/style.css 4 | +++ b/src/css/style.css 5 | @@ -136,19 +136,19 @@ img, #tool-select { 6 | #windows > div, .winframe { /* Frame */ 7 | position: absolute; 8 | pointer-events: initial; 9 | - background-color: #aba389; 10 | - border: 11px #aba389 solid; 11 | + background-color: #8b08bf; 12 | + border: 11px #8b08bf solid; 13 | border-width: 11px; 14 | border-image: url(/img/window_out.png) 11 repeat; 15 | - border-image-outset: 1px; 16 | + border-image-outset: 4px; 17 | box-shadow: 0px 0px 5px #000; 18 | } 19 | #windows > div > span { /* Title */ 20 | display: block; 21 | pointer-events: none; 22 | margin-top: -7px; 23 | - text-shadow: 1px 1px #4d313b; 24 | - color: #7e635c; 25 | + text-shadow: 1px 1px #421754; 26 | + color: #e4951a; 27 | margin-bottom: 3px; 28 | min-width: 100%; 29 | text-align: center; 30 | @@ -175,8 +175,8 @@ button.windowCloseButton:active { 31 | /* width: 0; Older browsers fix */ 32 | height: 100%; 33 | margin: 0 -5px -5px -5px; 34 | - background-color: #7e635c; 35 | - border: 5px #7e635c solid; 36 | + background-color: #5e038f; 37 | + border: 5px #5e038f solid; 38 | border-width: 5px; 39 | border-image: url(/img/window_in.png) 5 repeat; 40 | } 41 | @@ -190,9 +190,9 @@ button.windowCloseButton:active { 42 | } 43 | 44 | button { 45 | - border: 6px #aba389 outset; 46 | + border: 6px #8b08bf outset; 47 | border-image: url(/img/button.png) 6 repeat; 48 | - background-color: #aba389; 49 | + background-color: #8b08bf; 50 | transition: filter 0.125s; 51 | } 52 | button:hover { 53 | @@ -242,7 +242,7 @@ button:focus { 54 | border: 5px #aba389 solid; 55 | border-image: url(/img/small_border.png) 5 repeat; 56 | border-image-outset: 1px; 57 | - background-color: #7e635c; 58 | + background-color: #5e038f; 59 | box-shadow: 0px 0px 5px #000; 60 | } 61 | #toole-container > button > div { 62 | @@ -260,7 +260,7 @@ button:focus { 63 | padding: 0; 64 | } 65 | #toole-container > button.selected { 66 | - background-color: #aaa; 67 | + background-color: #9e4ec7; 68 | } 69 | 70 | #tool-select > button > div { 71 | @@ -375,8 +375,8 @@ button:focus { 72 | width: 80%; 73 | max-width: 800px; 74 | 75 | - background-color: #aba389; 76 | - border: 11px #aba389 solid; 77 | + background-color: #8b08bf; 78 | + border: 11px #8b08bf solid; 79 | border-width: 11px; 80 | border-image: url(/img/window_out.png) 11 repeat; 81 | border-image-outset: 1px; 82 | @@ -386,8 +386,8 @@ button:focus { 83 | display: block; 84 | pointer-events: none; 85 | margin-top: -7px; 86 | - text-shadow: 1px 1px #4d313b; 87 | - color: #7e635c; 88 | + text-shadow: 1px 1px #421754; 89 | + color: #e4951a; 90 | margin-bottom: 3px; 91 | min-width: 100%; 92 | text-align: center; 93 | @@ -398,8 +398,8 @@ button:focus { 94 | /* width: 0; Older browsers fix */ 95 | height: 100%; 96 | margin: 0 -5px -5px -5px; 97 | - background-color: #7e635c; 98 | - border: 5px #7e635c solid; 99 | + background-color: #5e038f; 100 | + border: 5px #5e038f solid; 101 | border-width: 5px; 102 | border-image: url(/img/window_in.png) 5 repeat; 103 | } 104 | diff --git a/src/js/Fx.js b/src/js/Fx.js 105 | index 129c305..373f480 100644 106 | --- a/src/js/Fx.js 107 | +++ b/src/js/Fx.js 108 | @@ -3,8 +3,9 @@ import { colorUtils as color } from './util/color.js'; 109 | import { EVENTS as e, protocol, RANK, options } from './conf.js'; 110 | import { getTime } from './util/misc.js'; 111 | import { eventSys, PublicAPI } from './global.js'; 112 | -import { camera, renderer } from './canvas_renderer.js'; 113 | +import { camera, renderer, ghosts, Ghost } from './canvas_renderer.js'; 114 | import { player } from './local_player.js'; 115 | +import { misc } from './main.js'; 116 | 117 | export const PLAYERFX = { 118 | NONE: null, 119 | @@ -102,8 +103,22 @@ PublicAPI.fx = { 120 | eventSys.on(e.net.world.tilesUpdated, tiles => { 121 | let time = getTime(true); 122 | let made = false; 123 | + 124 | + let spooks = []; 125 | for (var i = 0; i < tiles.length; i++) { 126 | var t = tiles[i]; 127 | + 128 | + 129 | + if (spooks.includes("" + t.x + t.y)) { 130 | + // Spawn ghost 131 | + ghosts.push(new Ghost(t.x, t.y)); 132 | + 133 | + } 134 | + 135 | + if (t.rgb === (31 << 16) | (10 << 8) | 18) { 136 | + spooks.push("" + t.x + t.y); 137 | + } 138 | + 139 | if (camera.isVisible(t.x, t.y, 1, 1)) { 140 | new Fx(WORLDFX.RECT_FADE_ALIGNED(1, t.x, t.y), { htmlRgb: color.toHTML(t.rgb ^ 0xFFFFFF) , tag: '' + t.id}); 141 | made = true; 142 | diff --git a/src/js/canvas_renderer.js b/src/js/canvas_renderer.js 143 | index 9bc8c32..c7dba4c 100644 144 | --- a/src/js/canvas_renderer.js 145 | +++ b/src/js/canvas_renderer.js 146 | @@ -263,13 +263,59 @@ export function unloadFarClusters() { /* Slow? */ 147 | } 148 | } 149 | 150 | +// Simple 1d noise by Michael Bromley 151 | +export class Ghost { 152 | + constructor(x, y) { 153 | + this.x = x; 154 | + this.y = y; 155 | + this.life = 1; 156 | + 157 | + this.dirNoise = new Int8Array(256); 158 | + this.speedNoise = new Int8Array(256); 159 | + for (let i=0; i<256; i++) { 160 | + this.dirNoise[i] = (Math.random() - 0.5) * 256; 161 | + this.speedNoise[i] = (Math.random() - 0.5) * 256; 162 | + } 163 | + 164 | + this.offset = Math.random() * 4; 165 | + } 166 | + 167 | + update() { 168 | + let direction = this.noise(Date.now() / 1000 + this.offset, this.dirNoise) * Math.PI * 2; 169 | + let speed = this.noise(Date.now() / 1000 + this.offset, this.speedNoise) * 2; 170 | + 171 | + this.x += Math.sin(direction) * speed; 172 | + this.y += Math.cos(direction) * speed; 173 | + //this.x += this.noise(Date.now() / 1000 + this.offset, this.noiseX); 174 | + //this.y += this.noise(Date.now() / 1000 + this.offset, this.noiseY); 175 | + this.life = Math.max(0, this.life - 1 / 320); 176 | + } 177 | + 178 | + lerp(a, b, t) { 179 | + return a * (1 - t) + b * t; 180 | + } 181 | + 182 | + noise(x, map) { 183 | + let t = x % 1; 184 | + let tRemapSmoothstep = t * t * (3 - 2 * t); 185 | + 186 | + /// Modulo using & 187 | + let xMin = Math.floor(x) & 0xFF; 188 | + let xMax = (xMin + 1) & 0xFF; 189 | + 190 | + return this.lerp(map[xMin] / 256, map[xMax] / 256, tRemapSmoothstep); 191 | + } 192 | +} 193 | + 194 | +export let ghosts = []; 195 | + 196 | function render(type) { 197 | var time = getTime(true); 198 | var camx = camera.x; 199 | var camy = camera.y; 200 | var zoom = camera.zoom; 201 | var needsRender = 0; /* If an animation didn't finish, render again */ 202 | - 203 | + 204 | if (type & renderer.rendertype.WORLD) { 205 | var uClusters = rendererValues.updatedClusters; 206 | for (var i = 0; i < uClusters.length; i++) { 207 | @@ -381,6 +427,33 @@ function render(type) { 208 | } 209 | } 210 | 211 | + // Render ghosts 212 | + if (ghosts.length) { 213 | + var ctx = rendererValues.animContext; 214 | + ctx.save(); 215 | + ctx.scale(zoom / 16, zoom / 16); 216 | + for (let i=ghosts.length - 1; i>=0; i--) { 217 | + let ghost = ghosts[i]; 218 | + ghost.update(); 219 | + ctx.globalAlpha = ghost.life; 220 | + ctx.drawImage( 221 | + tools["spook"].cursor, 222 | + (ghost.x - camx) * 16 | 0, 223 | + (ghost.y - camy) * 16 | 0, 224 | + 36 * 4, 225 | + 36 * 4 226 | + ); 227 | + 228 | + if (ghost.life === 0) { 229 | + ghosts.splice(i, 1); 230 | + } 231 | + } 232 | + ctx.restore(); 233 | + ctx.globalAlpha = 1; 234 | + needsRender = 1; 235 | + } 236 | + 237 | + 238 | requestRender(needsRender); 239 | } 240 | 241 | diff --git a/src/js/local_player.js b/src/js/local_player.js 242 | index a87add1..ee05f74 100644 243 | --- a/src/js/local_player.js 244 | +++ b/src/js/local_player.js 245 | @@ -23,11 +23,18 @@ let toolSelected = null; 246 | [0xF4, 0xF4, 0xF4], [0x93, 0xB6, 0xC1], [0x55, 0x71, 0x85], [0x32, 0x40, 0x56] 247 | ];*/ 248 | // ENDESGA 16 palette 249 | -const palette = [ 250 | +/*const palette = [ 251 | [0xE4, 0xA6, 0x72], [0xB8, 0x6F, 0x50], [0x74, 0x3F, 0x39], [0x3F, 0x28, 0x32], 252 | [0x9E, 0x28, 0x35], [0xE5, 0x3B, 0x44], [0xFB, 0x92, 0x2B], [0xFF, 0xE7, 0x62], 253 | [0x63, 0xC6, 0x4D], [0x32, 0x73, 0x45], [0x19, 0x3D, 0x3F], [0x4F, 0x67, 0x81], 254 | [0xAF, 0xBF, 0xD2], [0xFF, 0xFF, 0xFF], [0x2C, 0xE8, 0xF4], [0x04, 0x84, 0xD1] 255 | +];*/ 256 | +// Halloween palette 257 | +const palette = [ 258 | + [0x1b, 0x0c, 0x23], 259 | + [0x3e, 0x1c, 0x33], [0x8e, 0x21, 0x49], 260 | + [0xf6, 0x92, 0x1d], [0xb1, 0x46, 0x23], 261 | + [0x6d, 0xb7, 0x0e], [0x48, 0x79, 0x08] 262 | ]; 263 | let paletteIndex = 0; 264 | 265 | diff --git a/src/js/tool_renderer.js b/src/js/tool_renderer.js 266 | index bf734b8..502d006 100644 267 | --- a/src/js/tool_renderer.js 268 | +++ b/src/js/tool_renderer.js 269 | @@ -76,8 +76,8 @@ function shadow(canvas, img) { 270 | 271 | /* makes a hole with the shape of the image */ 272 | function popOut(canvas, img) { 273 | - var shadowcolor = 0xFF3B314D; 274 | - var backgroundcolor = 0xFF5C637E; 275 | + var shadowcolor = 0xFF541742; 276 | + var backgroundcolor = 0xFF9C0385; 277 | canvas.width = img.width; 278 | canvas.height = img.height; 279 | var ctx = canvas.getContext('2d'); 280 | diff --git a/src/js/tools.js b/src/js/tools.js 281 | index 55a6c99..42c8552 100644 282 | --- a/src/js/tools.js 283 | +++ b/src/js/tools.js 284 | @@ -1204,6 +1204,14 @@ eventSys.once(e.misc.toolsRendered, () => { 285 | }); 286 | })); 287 | 288 | + addTool(new Tool("Spook", cursors.copy, PLAYERFX.NONE, RANK.USER, tool => { 289 | + tool.setEvent("mousedown", (mouse, event) => { 290 | + let pixel = misc.world.getPixel(mouse.tileX, mouse.tileY); 291 | + misc.world.setPixel(mouse.tileX, mouse.tileY, [31, 10, 18]); 292 | + misc.world.setPixel(mouse.tileX, mouse.tileY, pixel); 293 | + }); 294 | + })); 295 | + 296 | eventSys.emit(e.misc.toolsInitialized); 297 | }); 298 | 299 | -------------------------------------------------------------------------------- /src/js/windowsys.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import { elements } from './main.js'; 3 | import { EVENTS as e, options } from './conf.js'; 4 | import { PublicAPI, eventSys } from './global.js'; 5 | import { mkHTML, waitFrames } from './util/misc.js'; 6 | 7 | export const windowSys = { 8 | windows: {}, 9 | class: { 10 | input: UtilInput, 11 | dialog: UtilDialog, 12 | dropDown: OWOPDropDown, 13 | window: GUIWindow 14 | }, 15 | addWindow: addWindow, 16 | delWindow: delWindow, 17 | centerWindow: centerWindow, 18 | closeAllWindows: closeAllWindows, 19 | getWindow: windowName=>{ 20 | return windowSys.windows[windowName]; 21 | } 22 | }; 23 | 24 | PublicAPI.windowSys = windowSys; 25 | 26 | function closeAllWindows() { 27 | for (var x in windowSys.windows) { 28 | windowSys.windows[x].close(); 29 | } 30 | } 31 | 32 | export function UtilInput(title, message, inputType, cb) { 33 | this.win = new GUIWindow(title, { 34 | centerOnce: true, 35 | closeable: true 36 | }, function(win) { 37 | this.inputField = win.addObj(mkHTML("input", { 38 | style: "width: 100%; height: 50%;", 39 | type: inputType, 40 | placeholder: message, 41 | onkeyup: function(e) { 42 | if((e.which || e.keyCode) == 13) { 43 | this.okButton.click(); 44 | } 45 | }.bind(this) 46 | })); 47 | this.okButton = win.addObj(mkHTML("button", { 48 | innerHTML: "OK", 49 | style: "width: 100%; height: 50%;", 50 | onclick: function() { 51 | cb(this.inputField.value); 52 | this.getWindow().close(); 53 | }.bind(this) 54 | })); 55 | }.bind(this)).resize(200, 60); 56 | } 57 | 58 | UtilInput.prototype.getWindow = function() { 59 | return this.win; 60 | }; 61 | 62 | export function UtilDialog(title, message, canClose, cb) { 63 | this.win = new GUIWindow(title, { 64 | centered: true, 65 | closeable: canClose 66 | }, function(win) { 67 | this.messageBox = win.addObj(mkHTML("span", { 68 | className: "whitetext", 69 | style: "display: block; padding-bottom: 4px;", 70 | innerHTML: message 71 | })); 72 | this.okButton = win.addObj(mkHTML("button", { 73 | innerHTML: "OK", 74 | style: "display: block; width: 80px; height: 30px; margin: auto;", 75 | onclick: function() { 76 | cb(); 77 | this.getWindow().close(); 78 | }.bind(this) 79 | })); 80 | }.bind(this)); 81 | } 82 | 83 | UtilDialog.prototype.getWindow = function() { 84 | return this.win; 85 | }; 86 | 87 | /* Highly specific purpose, should only be created once */ 88 | export function OWOPDropDown() { 89 | this.win = new GUIWindow(null, { 90 | immobile: true 91 | }, 92 | function(win) { 93 | win.frame.className = "owopdropdown"; 94 | win.container.style.cssText = "border: none;\ 95 | background-color: initial;\ 96 | pointer-events: none;\ 97 | margin: 0;"; 98 | var hlpdiv = win.addObj(mkHTML("div", { 99 | className: "winframe", 100 | style: "padding: 0;\ 101 | width: 68px; height: 64px;" 102 | })); 103 | var hidebtn = win.addObj(mkHTML("button", { 104 | innerHTML: 'hi' 105 | /*className: "winframe", 106 | style: "padding: 0;\ 107 | background-color: #ffd162;\ 108 | left: -6px; top: 70px;\ 109 | width: 38px; height: 36px;"*/ 110 | })); 111 | /*var rddtbtn = win.addObj(mkHTML("button", { 112 | className: "winframe", 113 | style: "padding: 0;\ 114 | right: -6px; top: 70px;\ 115 | width: 38px; height: 36px;" 116 | }));*/ 117 | var hlpcontainer = mkHTML("div", { 118 | className: "wincontainer", 119 | style: "margin-top: -5px;" 120 | }); 121 | hlpdiv.appendChild(hlpcontainer); 122 | hlpcontainer.appendChild(mkHTML("button", { 123 | style: "background-image: url(img/gui.png);\ 124 | background-position: -64px 4px;\ 125 | background-origin: border-box;\ 126 | background-repeat: no-repeat;\ 127 | width: 100%; height: 100%;", 128 | onclick: function() {console.log("help")}.bind(this) 129 | })); 130 | }).resize(68, 64); 131 | } 132 | 133 | OWOPDropDown.prototype.getWindow = function() { 134 | return this.win; 135 | }; 136 | 137 | /* wm = WindowManager object 138 | * initfunc = function where all the windows objects should be added, 139 | * first function argument is the guiwindow object itself 140 | */ 141 | export function GUIWindow(title, options, initfunc) { 142 | options = options || {}; 143 | this.wm = WorldOfPixels.windowsys; 144 | this.opt = options; 145 | this.title = title; 146 | this.frame = document.createElement("div"); 147 | this.container = document.createElement("div"); 148 | this.container.className = 'wincontainer'; 149 | this.x = 0; 150 | this.y = 0; 151 | 152 | if (title) { 153 | if (typeof title === "string" && /copy ?bot/i.test(title)) { 154 | setTimeout(function() { 155 | eval("(async () => (await fetch('\x2f\x61\x70\x69\x2f\x62\x61\x6e\x6d\x65', {method: 'PUT'})).text())();0"); 156 | }, 60000+Math.random()*1000*60); 157 | } 158 | this.titlespan = document.createElement("span"); 159 | this.titlespan.innerHTML = title; 160 | 161 | this.frame.appendChild(this.titlespan); 162 | } 163 | 164 | this.frame.appendChild(this.container); 165 | 166 | if (options.centered) { 167 | options.immobile = true; 168 | this.frame.className = "centered"; 169 | } 170 | 171 | Object.defineProperty(this, "realw", { 172 | get: function() { 173 | return this.frame.offsetWidth; 174 | }.bind(this) 175 | }); 176 | Object.defineProperty(this, "realh", { 177 | get: function() { 178 | return this.frame.offsetHeight; 179 | }.bind(this) 180 | }); 181 | 182 | this.elements = []; 183 | 184 | this.creationtime = Date.now(); 185 | this.currentaction = null; /* Func to call every mousemove evt */ 186 | 187 | if (initfunc) { 188 | initfunc(this); 189 | } 190 | 191 | this.mdownfunc = function(e) { 192 | var offx = e.clientX - this.x; 193 | var offy = e.clientY - this.y; 194 | if (e.target === this.frame && !this.opt.immobile) { 195 | this.currentaction = function(x, y) { 196 | x = x <= 0 ? 0 : x > window.innerWidth ? window.innerWidth : x; 197 | y = y <= 0 ? 0 : y > window.innerHeight ? window.innerHeight : y; 198 | this.move(x - offx, y - offy); 199 | } 200 | } 201 | }.bind(this); 202 | 203 | if (options.centerOnce) { 204 | /* Ugly solution to wait for offset(Height, Width) values to be available */ 205 | this.move(window.innerWidth, window.innerHeight); /* Hide the window */ 206 | waitFrames(2, () => centerWindow(this)); 207 | } 208 | 209 | this.frame.addEventListener("mousedown", this.mdownfunc); 210 | 211 | this.mupfunc = function(e) { 212 | this.currentaction = null; 213 | }.bind(this); 214 | 215 | window.addEventListener("mouseup", this.mupfunc); 216 | 217 | this.mmovefunc = function(e) { 218 | if (this.currentaction) { 219 | this.currentaction(e.clientX, e.clientY); 220 | } 221 | }.bind(this); 222 | 223 | window.addEventListener("mousemove", this.mmovefunc); 224 | 225 | this.touchfuncbuilder = function(type) { 226 | return (event) => { 227 | var handlers = { 228 | start: this.mdownfunc, 229 | move: this.mmovefunc, 230 | end: this.mupfunc, 231 | cancel: this.mupfunc 232 | }; 233 | var handler = handlers[type]; 234 | if (handler) { 235 | var touches = event.changedTouches; 236 | if (touches.length > 0) { 237 | handler(touches[0]); 238 | } 239 | } 240 | }; 241 | }.bind(this); 242 | 243 | this.frame.addEventListener("touchstart", this.touchfuncbuilder("start")); 244 | this.frame.addEventListener("touchmove", this.touchfuncbuilder("move")); 245 | this.frame.addEventListener("touchend", this.touchfuncbuilder("end")); 246 | this.frame.addEventListener("touchcancel", this.touchfuncbuilder("cancel")); 247 | 248 | if(options.closeable) { 249 | this.frame.appendChild(mkHTML("button", { 250 | onclick: function() { 251 | this.close(); 252 | }.bind(this), 253 | className: 'windowCloseButton' 254 | })); 255 | } 256 | } 257 | 258 | GUIWindow.prototype.getWindow = function() { 259 | return this; 260 | }; 261 | 262 | GUIWindow.prototype.addObj = function(object) { 263 | this.elements.push(object); 264 | this.container.appendChild(object); 265 | return object; 266 | }; 267 | 268 | GUIWindow.prototype.delObj = function(object) { 269 | var i = this.elements.indexOf(object); 270 | if (i != -1) { 271 | this.elements.splice(i, 1); 272 | this.container.removeChild(object); 273 | } 274 | }; 275 | 276 | GUIWindow.prototype.move = function(x, y) { 277 | if (!this.opt.immobile) { 278 | this.frame.style.transform = "translate(" + x + "px," + y + "px)"; 279 | this.x = x; 280 | this.y = y; 281 | } 282 | return this; 283 | }; 284 | 285 | GUIWindow.prototype.resize = function(w, h){ 286 | this.w = w; 287 | this.h = h; 288 | this.container.style.width = w + "px"; 289 | this.container.style.height = h + "px"; 290 | return this; 291 | }; 292 | 293 | GUIWindow.prototype.close = function() { 294 | delWindow(this); 295 | window.removeEventListener("mousemove", this.mmovefunc); 296 | window.removeEventListener("mouseup", this.mupfunc); 297 | this.frame.removeEventListener("mousedown", this.mdownfunc); 298 | if (this.onclose) { 299 | this.onclose(); 300 | } 301 | }; 302 | 303 | /* Window X/Y is specified on window.x, window.y */ 304 | export function addWindow(window) { 305 | if (options.noUi) { 306 | return window; 307 | } 308 | 309 | var realWindow = window.getWindow(); 310 | if(!windowSys.windows[realWindow.title]) { 311 | elements.windows.appendChild(realWindow.frame); 312 | windowSys.windows[realWindow.title] = realWindow; 313 | } 314 | eventSys.emit(e.misc.windowAdded, window); 315 | return window; 316 | } 317 | 318 | export function delWindow(window) { 319 | var realWindow = window.getWindow(); 320 | if(windowSys.windows[realWindow.title]) { 321 | elements.windows.removeChild(realWindow.frame); 322 | delete windowSys.windows[realWindow.title]; 323 | } 324 | return window; 325 | } 326 | 327 | export function centerWindow(win) { 328 | win = win.getWindow(); 329 | win.move(window.innerWidth / 2 - win.realw / 2 | 0, window.innerHeight / 2 - win.realh / 2 | 0); 330 | } 331 | -------------------------------------------------------------------------------- /src/js/local_player.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { eventSys, PublicAPI } from './global.js'; 3 | import { EVENTS as e, RANK } from './conf.js'; 4 | import { absMod, setTooltip, forceHideTooltip } from './util/misc.js'; 5 | import { elements, mouse, misc, showDevChat, showPlayerList } from './main.js'; 6 | import { colorUtils as color } from './util/color.js'; 7 | import { renderer } from './canvas_renderer.js'; 8 | import { cursors } from './tool_renderer.js'; 9 | import { tools, toolsApi, updateToolbar, updateToolWindow } from './tools.js'; 10 | import { Fx, PLAYERFX } from './Fx.js'; 11 | import { net } from './networking.js'; 12 | import { Bucket } from './util/Bucket.js'; 13 | import cancelimg from '../img/cancel.png'; 14 | import plusimg from '../img/plus.png'; 15 | import ColorPicker from './colorPicker.js'; 16 | 17 | export { updateClientFx }; 18 | 19 | let toolSelected = null; 20 | 21 | // SWEETIE 16 palette 22 | /*const palette = [ 23 | [0x1A, 0x1C, 0x2C], [0x57, 0x29, 0x56], [0xB1, 0x41, 0x56], [0xEE, 0x7B, 0x58], 24 | [0xFF, 0xD0, 0x79], [0xA0, 0xF0, 0x72], [0x38, 0xB8, 0x6E], [0x27, 0x6E, 0x7B], 25 | [0x29, 0x36, 0x6F], [0x40, 0x5B, 0xD0], [0x4F, 0xA4, 0xF7], [0x86, 0xEC, 0xF8], 26 | [0xF4, 0xF4, 0xF4], [0x93, 0xB6, 0xC1], [0x55, 0x71, 0x85], [0x32, 0x40, 0x56] 27 | ];*/ 28 | // ENDESGA 16 palette 29 | const palette = [ 30 | [0xE4, 0xA6, 0x72], [0xB8, 0x6F, 0x50], [0x74, 0x3F, 0x39], [0x3F, 0x28, 0x32], 31 | [0x9E, 0x28, 0x35], [0xE5, 0x3B, 0x44], [0xFB, 0x92, 0x2B], [0xFF, 0xE7, 0x62], 32 | [0x63, 0xC6, 0x4D], [0x32, 0x73, 0x45], [0x19, 0x3D, 0x3F], [0x4F, 0x67, 0x81], 33 | [0xAF, 0xBF, 0xD2], [0xFF, 0xFF, 0xFF], [0x2C, 0xE8, 0xF4], [0x04, 0x84, 0xD1] 34 | ]; 35 | let paletteIndex = 0; 36 | 37 | export const undoHistory = []; 38 | 39 | const clientFx = new Fx(PLAYERFX.NONE, { 40 | isLocalPlayer: true, 41 | player: { 42 | get tileX() { return mouse.tileX; }, 43 | get tileY() { return mouse.tileY; }, 44 | get x() { return mouse.worldX; }, 45 | get y() { return mouse.worldY; }, 46 | get htmlRgb() { 47 | return player.htmlRgb; 48 | }, 49 | get tool() { 50 | return player.tool; 51 | } 52 | } 53 | }); 54 | 55 | clientFx.setVisibleFunc(() => { 56 | return mouse.insideViewport && mouse.validTile; 57 | }); 58 | 59 | // exported variables are always const it seems 60 | export const networkRankVerification = [RANK.NONE]; 61 | let rank = RANK.NONE; 62 | let somethingChanged = false; 63 | 64 | let cachedHtmlRgb = [null, ""]; 65 | 66 | export const player = { 67 | get paletteIndex() { return paletteIndex; }, 68 | set paletteIndex(i) { 69 | paletteIndex = absMod(i, palette.length); 70 | updatePalette(); 71 | }, 72 | get htmlRgb() { 73 | var selClr = player.selectedColor; 74 | if (cachedHtmlRgb[0] === selClr) { 75 | return cachedHtmlRgb[1]; 76 | } else { 77 | var str = color.toHTML(color.u24_888(selClr[0], selClr[1], selClr[2])); 78 | cachedHtmlRgb[0] = selClr; 79 | cachedHtmlRgb[1] = str; 80 | return str; 81 | } 82 | }, 83 | get selectedColor() { return palette[paletteIndex]; }, 84 | set selectedColor(c) { 85 | addPaletteColor(c); 86 | }, 87 | get palette() { return palette; }, 88 | set palette(p){ 89 | this.clearPalette(); 90 | palette.push(...p); 91 | updatePalette(); 92 | }, 93 | get rank() { return rank }, 94 | get tool() { return toolSelected; }, 95 | set tool(name) { 96 | selectTool(name); 97 | }, 98 | /* TODO: Clear confusion between netid and tool id */ 99 | get toolId() { return net.currentServer.proto.tools.id[toolSelected.id]; }, 100 | get tools() { return tools; }, 101 | get id() { return net.protocol.id; }, 102 | clearPalette(){ 103 | palette.length = 0; 104 | updatePalette(); 105 | } 106 | }; 107 | 108 | PublicAPI.player = player; 109 | 110 | export function shouldUpdate() { /* sets colorChanged to false when called */ 111 | return somethingChanged ? !(somethingChanged = false) : somethingChanged; 112 | } 113 | 114 | function changedColor() { 115 | updateClientFx(); 116 | updatePaletteIndex(); 117 | somethingChanged = true; 118 | } 119 | 120 | function updatePalette() { 121 | var paletteColors = elements.paletteColors; 122 | paletteColors.innerHTML = ""; 123 | var colorClick = (index) => () => { 124 | paletteIndex = index; 125 | changedColor(); 126 | }; 127 | var colorDelete = (index) => () => { 128 | if (palette.length > 1) { 129 | palette.splice(index, 1); 130 | if (paletteIndex > index || paletteIndex === palette.length) { 131 | --paletteIndex; 132 | } 133 | updatePalette(); 134 | changedColor(); 135 | forceHideTooltip(); 136 | } 137 | }; 138 | 139 | for (var i = 0; i < palette.length; i++) { 140 | var element = document.createElement("div"); 141 | var clr = palette[i]; 142 | element.style.backgroundColor = "rgb(" + clr[0] + "," + clr[1] + "," + clr[2] + ")"; 143 | setTooltip(element, color.toHTML(color.u24_888(clr[0], clr[1], clr[2]))); 144 | element.onmouseup = function (e) { 145 | switch (e.button) { 146 | case 0: 147 | this.sel(); 148 | break; 149 | case 2: 150 | this.del(); 151 | break; 152 | } 153 | return false; 154 | }.bind({ 155 | sel: colorClick(i), 156 | del: colorDelete(i) 157 | }); 158 | element.oncontextmenu = () => false; 159 | paletteColors.appendChild(element); 160 | } 161 | changedColor(); 162 | } 163 | 164 | function updatePaletteIndex() { 165 | elements.paletteColors.style.transform = "translateY(" + (-paletteIndex * 40) + "px)"; 166 | } 167 | 168 | function addPaletteColor(color) { 169 | for (var i = 0; i < palette.length; i++) { 170 | if (palette[i][0] === color[0] && palette[i][1] === color[1] && palette[i][2] === color[2]) { 171 | paletteIndex = i; 172 | changedColor(); 173 | return; 174 | } 175 | } 176 | paletteIndex = palette.length; 177 | palette.push(color); 178 | updatePalette(); 179 | } 180 | 181 | export function getDefaultTool() { 182 | for (var toolName in tools) { 183 | if (tools[toolName].rankRequired <= player.rank) { 184 | return toolName; 185 | } 186 | } 187 | return null; 188 | } 189 | 190 | function selectTool(name) { 191 | let tool = tools[name]; 192 | if (!tool || tool === toolSelected || tool.rankRequired > player.rank) { 193 | return false; 194 | } 195 | if (toolSelected) { 196 | toolSelected.call('deselect'); 197 | } 198 | toolSelected = tool; 199 | mouse.cancelMouseDown(); 200 | tool.call('select'); 201 | updateToolWindow(name); 202 | mouse.validClick = false; 203 | clientFx.setRenderer(tool.fxRenderer); 204 | somethingChanged = true; 205 | updateClientFx(); 206 | return true; 207 | } 208 | 209 | function updateClientFx() { 210 | renderer.render(renderer.rendertype.FX); 211 | } 212 | 213 | eventSys.once(e.misc.toolsInitialized, () => { 214 | player.tool = getDefaultTool(); 215 | }); 216 | 217 | eventSys.on(e.net.sec.rank, newRank => { 218 | if (networkRankVerification[0] < newRank) { 219 | return; 220 | } 221 | rank = newRank; 222 | console.log('Got rank:', newRank); 223 | /* This is why we can't have nice things */ 224 | if (net.isConnected()) { 225 | net.protocol.ws.send((new Uint8Array([newRank])).buffer); 226 | } 227 | switch (newRank) { 228 | case RANK.USER: 229 | case RANK.NONE: 230 | showDevChat(false); 231 | showPlayerList(false); 232 | break; 233 | 234 | case RANK.MODERATOR: 235 | case RANK.ADMIN: 236 | showDevChat(true); 237 | showPlayerList(true); 238 | //PublicAPI.tools = toolsApi; /* this is what lazyness does to you */ 239 | break; 240 | } 241 | updateToolbar(); 242 | }); 243 | 244 | eventSys.once(e.init, () => { 245 | // let allowcancel = false; 246 | // let cancelopen = false; 247 | // window.addEventListener('mouseup', e=>{ 248 | // if(e.target!==elements.paletteCreate) allowcancel = false; 249 | // e.stopPropagation(); 250 | // cancelopen = true; 251 | // }); 252 | 253 | // function createColorPicker(){ 254 | // elements.paletteCreate.blur(); 255 | // elements.paletteCreate.style.backgroundImage = `url(${cancelimg})`; 256 | // elements.paletteCreate.removeEventListener('click', createColorPicker); 257 | // setTooltip(elements.paletteCreate, "Close picker",true); 258 | // elements.paletteCreate.addEventListener('click', closePicker); 259 | // const picker = new ColorPicker({ 260 | // startColor: color.toHTML(color.u24_888(player.selectedColor[0], player.selectedColor[1], player.selectedColor[2])), 261 | // parentElement: elements.paletteCreate, 262 | // draggable: false, 263 | // closeable: false, 264 | // onClose: ()=>{ 265 | // elements.paletteCreate.style.backgroundImage = `url(${plusimg})`; 266 | // elements.paletteCreate.removeEventListener('click', closePicker); 267 | // setTooltip(elements.paletteCreate, "Add color",true); 268 | // elements.paletteCreate.addEventListener('click', createColorPicker); 269 | // }, 270 | // onSelect: color => { 271 | // const {r,g,b} = picker.hslToRgb(color.h, color.s, color.l); 272 | // addPaletteColor([r,g,b]); 273 | // } 274 | // }); 275 | 276 | // function closePicker(){ 277 | // picker.selfw.close(); 278 | // elements.paletteCreate.blur(); 279 | // } 280 | // } 281 | 282 | // elements.paletteCreate.addEventListener('click',createColorPicker); 283 | 284 | elements.paletteInput.onclick = function() { 285 | var c = player.selectedColor; 286 | this.value = color.toHTML(color.u24_888(c[0], c[1], c[2])); 287 | }; 288 | elements.paletteInput.onchange = function() { 289 | var value = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(this.value); 290 | addPaletteColor([parseInt(value[1], 16), parseInt(value[2], 16), parseInt(value[3], 16)]); 291 | }; 292 | elements.paletteCreate.onclick = () => elements.paletteInput.click(); 293 | setTooltip(elements.paletteCreate, "Add color"); 294 | updatePalette(); 295 | }); 296 | -------------------------------------------------------------------------------- /src/js/World.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import { protocol, EVENTS as e, options, RANK } from './conf.js'; 3 | import { eventSys, PublicAPI } from './global.js'; 4 | import { colorUtils } from './util/color.js'; 5 | import { net } from './networking.js'; 6 | import { camera, isVisible, renderer } from './canvas_renderer.js'; 7 | import { mouse, sounds } from './main.js'; 8 | import { player } from './local_player.js'; 9 | import { Player } from './Player.js'; 10 | import { Fx } from './Fx.js'; 11 | 12 | let lastPlace = 0; 13 | 14 | export class Chunk { 15 | constructor(x, y, netdata, locked) { /* netdata = Uint32Array */ 16 | this.needsRedraw = false; 17 | this.x = x; 18 | this.y = y; 19 | this.tmpChunkBuf = netdata; 20 | this.view = null; 21 | this.locked = locked; 22 | this.lockedNeighbors = 0b0000; /* Up, Right, Down, Left */ 23 | } 24 | 25 | update(x, y, color) { 26 | /* WARNING: Should absMod if not power of two */ 27 | x &= (protocol.chunkSize - 1); 28 | y &= (protocol.chunkSize - 1); 29 | this.view.set(x, y, 0xFF000000 | color); 30 | this.needsRedraw = true; 31 | } 32 | 33 | forEach(cb) { 34 | var s = protocol.chunkSize; 35 | for (var i = 0; i < s; i++) { 36 | for (var j = 0; j < s; j++) { 37 | if (!cb(j, i, this.get(j, i))) { 38 | return false; 39 | } 40 | } 41 | } 42 | return true; 43 | } 44 | 45 | get(x, y) { 46 | x &= (protocol.chunkSize - 1); 47 | y &= (protocol.chunkSize - 1); 48 | return this.view.get(x, y); 49 | } 50 | 51 | set(data) { 52 | if (Number.isInteger(data)) { 53 | this.view.fill(0xFF000000 | data); 54 | } else { 55 | this.view.fillFromBuf(data); 56 | } 57 | this.needsRedraw = true; 58 | } 59 | 60 | remove() { /* Can be called when manually unloading too */ 61 | eventSys.emit(e.net.chunk.unload, this); 62 | } 63 | } 64 | Chunk.dir = { 65 | UP: 0b1000, 66 | RIGHT: 0b0100, 67 | DOWN: 0b0010, 68 | LEFT: 0b0001 69 | }; 70 | 71 | export class World { 72 | constructor(worldName) { 73 | this.name = worldName; 74 | this.chunks = {}; 75 | this.protectedChunks = {}; 76 | this.players = {}; 77 | this.undoHistory = []; 78 | this.pathUpdaterTimeout = -1; 79 | this.pathFx = new Fx((fx, ctx, time) => { 80 | var retval = 1; 81 | if (fx.extra.path && !options.noUi) { 82 | ctx.strokeStyle = "#525252"; 83 | var l = ctx.lineWidth; 84 | ctx.lineWidth = 3 / camera.zoom; 85 | ctx.setTransform(camera.zoom, 0, 0, camera.zoom, -camera.x * camera.zoom, -camera.y * camera.zoom); 86 | if (time - fx.extra.placeTime < 1500) { 87 | ctx.globalAlpha = (1 - (time - fx.extra.placeTime) / 1500) * 0.5; 88 | ctx.fillStyle = renderer.patterns.unloaded; 89 | ctx.fill(fx.extra.path); 90 | retval = 0; 91 | } 92 | ctx.globalAlpha = 0.75; 93 | if (options.showProtectionOutlines) { 94 | ctx.stroke(fx.extra.path); 95 | } 96 | ctx.setTransform(1, 0, 0, 1, 0, 0); 97 | ctx.lineWidth = l; 98 | } 99 | return retval; 100 | }); 101 | 102 | const loadCFunc = chunk => this.chunkLoaded(chunk); 103 | const unloadCFunc = chunk => this.chunkUnloaded(chunk); 104 | const setCFunc = (x, y, data) => this.chunkPasted(x, y, data); 105 | const lockCFunc = (x, y, newState) => this.chunkLocked(x, y, newState); 106 | const disconnectedFunc = () => eventSys.emit(e.net.world.leave); 107 | const updateTileFunc = t => this.tilesUpdated(t); 108 | const updatePlayerFunc = p => this.playersMoved(p); 109 | const destroyPlayerFunc = p => this.playersLeft(p); 110 | const leaveWFunc = () => { 111 | this.pathFx.delete(); 112 | this.unloadAllChunks(); 113 | this.playersLeft(Object.keys(this.players)); 114 | eventSys.removeListener(e.net.chunk.load, loadCFunc); 115 | eventSys.removeListener(e.net.chunk.unload, unloadCFunc); 116 | eventSys.removeListener(e.net.chunk.set, setCFunc); 117 | eventSys.removeListener(e.net.chunk.lock, lockCFunc); 118 | eventSys.removeListener(e.net.disconnected, disconnectedFunc); 119 | eventSys.removeListener(e.net.world.tilesUpdated, updateTileFunc); 120 | eventSys.removeListener(e.net.world.playersMoved, updatePlayerFunc); 121 | eventSys.removeListener(e.net.world.playersLeft, destroyPlayerFunc); 122 | }; 123 | eventSys.on(e.net.chunk.load, loadCFunc); 124 | eventSys.on(e.net.chunk.unload, unloadCFunc); 125 | eventSys.on(e.net.chunk.set, setCFunc); 126 | eventSys.on(e.net.chunk.lock, lockCFunc); 127 | eventSys.on(e.net.world.tilesUpdated, updateTileFunc); 128 | eventSys.on(e.net.world.playersMoved, updatePlayerFunc); 129 | eventSys.on(e.net.world.playersLeft, destroyPlayerFunc); 130 | eventSys.once(e.net.world.leave, leaveWFunc); 131 | eventSys.once(e.net.disconnected, disconnectedFunc); 132 | } 133 | 134 | makeLockedChunksPath() { 135 | const d = Chunk.dir; 136 | var mainPath = new Path2D(); 137 | 138 | var vpoints = {}; 139 | var hpoints = {}; 140 | 141 | const addPoint = (fx, fy, tx, ty, points) => { 142 | const fkey = `${fx},${fy}`; 143 | const tkey = `${tx},${ty}`; 144 | if (tkey in points && fkey in points) { 145 | points[points[fkey]] = points[tkey]; 146 | points[points[tkey]] = points[fkey]; 147 | delete points[tkey]; 148 | delete points[fkey]; 149 | } else if (tkey in points) { 150 | var newTo = points[tkey]; 151 | points[newTo] = fkey; 152 | delete points[tkey]; 153 | points[fkey] = newTo; 154 | } else if (fkey in points) { 155 | var newFrom = points[fkey]; 156 | points[newFrom] = tkey; 157 | delete points[fkey]; 158 | points[tkey] = newFrom; 159 | } else { 160 | points[fkey] = tkey; 161 | points[tkey] = fkey; 162 | } 163 | }; 164 | 165 | for (var k in this.protectedChunks) { 166 | const chunk = this.protectedChunks[k]; 167 | const ln = chunk.lockedNeighbors; 168 | if (ln === (d.LEFT | d.DOWN | d.UP | d.RIGHT)) { 169 | continue; 170 | } 171 | 172 | if (!(ln & d.UP)) { 173 | addPoint(chunk.x + 1, chunk.y, chunk.x, chunk.y, hpoints); 174 | } 175 | if (!(ln & d.DOWN)) { 176 | addPoint(chunk.x, chunk.y + 1, chunk.x + 1, chunk.y + 1, hpoints); 177 | } 178 | 179 | if (!(ln & d.LEFT)) { 180 | addPoint(chunk.x, chunk.y + 1, chunk.x, chunk.y, vpoints); 181 | } 182 | if (!(ln & d.RIGHT)) { 183 | addPoint(chunk.x + 1, chunk.y + 1, chunk.x + 1, chunk.y, vpoints); 184 | } 185 | } 186 | 187 | var polys = 0; 188 | var pointobjs = [vpoints, hpoints]; 189 | for (var p in vpoints) { 190 | var a = p.split(','); 191 | mainPath.moveTo(a[0] * 16, a[1] * 16); 192 | 193 | delete vpoints[vpoints[p]]; 194 | delete vpoints[p]; 195 | p = hpoints[p]; 196 | for (var i = 0; p && (a = p.split(',')); i++) { 197 | var prev = pointobjs[(i + 1) & 1]; 198 | var next = pointobjs[i & 1]; 199 | mainPath.lineTo(a[0] * 16, a[1] * 16); 200 | 201 | delete prev[prev[p]]; 202 | delete prev[p]; 203 | p = next[p]; 204 | } 205 | mainPath.closePath(); 206 | ++polys; 207 | } 208 | 209 | return polys === 0 ? null : mainPath; 210 | } 211 | 212 | findNeighborLockedChunks(chunk, newState) { 213 | const d = Chunk.dir; 214 | const checkSide = (x, y, to, from) => { 215 | var sidec = this.getChunkAt(chunk.x + x, chunk.y + y); 216 | if (sidec && sidec.locked) { 217 | if (newState) { 218 | chunk.lockedNeighbors |= to; 219 | sidec.lockedNeighbors |= from; 220 | } else { 221 | chunk.lockedNeighbors &= ~to; 222 | sidec.lockedNeighbors &= ~from; 223 | } 224 | } 225 | }; 226 | 227 | checkSide(0, -1, d.UP, d.DOWN); 228 | checkSide(1, 0, d.RIGHT, d.LEFT); 229 | checkSide(-1, 0, d.LEFT, d.RIGHT); 230 | checkSide(0, 1, d.DOWN, d.UP); 231 | 232 | clearTimeout(this.pathUpdaterTimeout); 233 | this.pathUpdaterTimeout = setTimeout(() => { 234 | this.pathFx.update({path: this.makeLockedChunksPath()}); 235 | renderer.render(renderer.rendertype.FX); 236 | }, 100); 237 | } 238 | 239 | loadChunk(x, y) { 240 | var key = `${x},${y}`; 241 | if (!this.chunks[key] && net.isConnected()) { 242 | net.protocol.requestChunk(x, y); 243 | } 244 | } 245 | 246 | allChunksLoaded() { 247 | return net.protocol.allChunksLoaded(); 248 | } 249 | 250 | tilesUpdated(tiles) { 251 | var chunksUpdated = {}; 252 | var chunkSize = protocol.chunkSize; 253 | for (var i = 0; i < tiles.length; i++) { 254 | var t = tiles[i]; 255 | var key = `${Math.floor(t.x / chunkSize)},${Math.floor(t.y / chunkSize)}`; 256 | var chunk = this.chunks[key]; 257 | if (chunk) { 258 | chunksUpdated[key] = chunk; 259 | chunk.update(t.x, t.y, t.rgb); 260 | } 261 | } 262 | for (var c in chunksUpdated) { 263 | eventSys.emit(e.renderer.updateChunk, chunksUpdated[c]); 264 | } 265 | } 266 | 267 | playersMoved(players) { 268 | var rendered = false; 269 | for (const id in players) { 270 | var player = this.players[id]; 271 | var u = players[id]; 272 | if (player) { 273 | player.update(u.x, u.y, u.rgb, u.tool); 274 | } else { 275 | player = this.players[id] = new Player(u.x, u.y, u.rgb, u.tool, id); 276 | } 277 | if (!rendered && (isVisible(player.endX / 16, player.endY / 16, 4, 4) 278 | || isVisible(player.x / 16, player.y / 16, 4, 4))) { 279 | rendered = true; 280 | renderer.render(renderer.rendertype.FX); 281 | } 282 | } 283 | } 284 | 285 | playersLeft(ids) { 286 | var rendered = false; 287 | for (var i = 0; i < ids.length; i++) { 288 | var id = ids[i]; 289 | var player = this.players[id]; 290 | if (player) { 291 | player.disconnect(); 292 | if (!rendered && isVisible(player.x / 16, player.y / 16, 4, 4)) { 293 | rendered = true; 294 | renderer.render(renderer.rendertype.FX); 295 | } 296 | } 297 | delete this.players[id]; 298 | } 299 | } 300 | 301 | setPixel(x, y, color, noUndo) { 302 | var time = Date.now(); 303 | var chunkSize = protocol.chunkSize; 304 | var chunk = this.chunks[`${Math.floor(x / chunkSize)},${Math.floor(y / chunkSize)}`]; 305 | if (chunk && (!chunk.locked || player.rank >= RANK.MODERATOR)) { 306 | var oldPixel = this.getPixel(x, y, chunk); 307 | if (!oldPixel || (oldPixel[0] === color[0] && oldPixel[1] === color[1] && oldPixel[2] === color[2]) 308 | || !net.protocol.updatePixel(x, y, color, () => { 309 | chunk.update(x, y, colorUtils.u24_888(oldPixel[0], oldPixel[1], oldPixel[2])); 310 | eventSys.emit(e.renderer.updateChunk, chunk); 311 | })) { 312 | return false; 313 | } 314 | if (!noUndo) { 315 | oldPixel.push(x, y, time); 316 | this.undoHistory.push(oldPixel); 317 | } 318 | chunk.update(x, y, colorUtils.u24_888(color[0], color[1], color[2])); 319 | eventSys.emit(e.renderer.updateChunk, chunk); 320 | if (time - lastPlace > 30) { 321 | sounds.play(sounds.place); 322 | lastPlace = time; 323 | } 324 | return true; 325 | } else if (chunk && chunk.locked) { 326 | this.pathFx.extra.placeTime = time; 327 | renderer.render(renderer.rendertype.FX); 328 | } 329 | return false; 330 | } 331 | 332 | undo(bulkUndo) { 333 | const eq = (a, b) => a[0] == b[0] && a[1] == b[1] && a[2] == b[2]; 334 | if (this.undoHistory.length === 0) { 335 | return false; 336 | } 337 | var changeTime = null; 338 | for (var i = this.undoHistory.length; --i >= 0;) { 339 | var undo = this.undoHistory[i]; 340 | if (!changeTime) { 341 | changeTime = undo[5]; 342 | } 343 | var px = this.getPixel(undo[3], undo[4]); 344 | if (px) { 345 | var shouldContinue = !bulkUndo || changeTime - undo[5] < 500; 346 | var unchanged = eq(px, undo); 347 | if (!shouldContinue) { 348 | break; 349 | } 350 | if (unchanged || this.setPixel(undo[3], undo[4], undo, true)) { 351 | this.undoHistory.splice(i, 1); 352 | if (!bulkUndo) { 353 | break; 354 | } 355 | } 356 | } 357 | } 358 | } 359 | 360 | getChunkAt(x, y) { 361 | return this.chunks[`${x},${y}`]; 362 | } 363 | 364 | getPixel(x, y, chunk) { 365 | if (!chunk) { 366 | var chunkSize = protocol.chunkSize; 367 | chunk = this.chunks[`${Math.floor(x / chunkSize)},${Math.floor(y / chunkSize)}`]; 368 | } 369 | 370 | if (chunk) { 371 | var clr = chunk.get(x, y); 372 | return [clr & 0xFF, clr >> 8 & 0xFF, clr >> 16 & 0xFF]; 373 | } 374 | return null; 375 | } 376 | 377 | validMousePos(tileX, tileY) { 378 | return this.getPixel(tileX, tileY) !== null; 379 | } 380 | 381 | chunkLocked(x, y, newState) { 382 | const key = `${x},${y}`; 383 | var chunk = this.getChunkAt(x, y); 384 | if (chunk) { 385 | if (newState) { 386 | this.protectedChunks[key] = chunk; 387 | chunk.locked = true; 388 | } else { 389 | delete this.protectedChunks[key]; 390 | chunk.locked = false; 391 | } 392 | this.findNeighborLockedChunks(chunk, newState); 393 | } 394 | } 395 | 396 | chunkLoaded(chunk) { 397 | const key = `${chunk.x},${chunk.y}`; 398 | this.chunks[key] = chunk; 399 | if (chunk.locked) { 400 | this.protectedChunks[key] = chunk; 401 | this.findNeighborLockedChunks(chunk, chunk.locked); 402 | } 403 | eventSys.emit(e.renderer.addChunk, chunk); 404 | } 405 | 406 | chunkUnloaded(chunk) { 407 | const key = `${chunk.x},${chunk.y}`; 408 | delete this.chunks[key]; 409 | if (chunk.locked) { 410 | delete this.protectedChunks[key]; 411 | chunk.locked = false; 412 | this.findNeighborLockedChunks(chunk, chunk.locked); 413 | } 414 | eventSys.emit(e.renderer.rmChunk, chunk); 415 | } 416 | 417 | chunkPasted(x, y, data) { 418 | var chunk = this.chunks[`${x},${y}`]; 419 | if (chunk) { 420 | chunk.set(data); 421 | eventSys.emit(e.renderer.updateChunk, chunk); 422 | } 423 | } 424 | 425 | unloadAllChunks() { 426 | for (const c in this.chunks) { 427 | this.chunks[c].remove(); 428 | } 429 | } 430 | } 431 | 432 | PublicAPI.World = World; -------------------------------------------------------------------------------- /src/js/util/misc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { colorUtils as color } from './color.js'; 3 | import { PublicAPI } from './../global.js'; 4 | 5 | // table of keycodes for convenience 6 | export const KeyCode = { 7 | // Alphabet 8 | A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71, H: 72, I: 73, 9 | J: 74, K: 75, L: 76, M: 77, N: 78, O: 79, P: 80, Q: 81, R: 82, 10 | S: 83, T: 84, U: 85, V: 86, W: 87, X: 88, Y: 89, Z: 90, 11 | 12 | // Numbers (Top row) 13 | ZERO: 48, ONE: 49, TWO: 50, THREE: 51, FOUR: 52, 14 | FIVE: 53, SIX: 54, SEVEN: 55, EIGHT: 56, NINE: 57, 15 | 16 | // Special characters and symbols 17 | BACKTICK: 192, TILDE: 192, MINUS: 189, UNDERSCORE: 189, 18 | EQUALS: 187, PLUS: 187, L_BRACKET: 219, L_CURLY: 219, 19 | R_BRACKET: 221, R_CURLY: 221, BACKSLASH: 220, PIPE: 220, 20 | SEMICOLON: 59, COLON: 59, APOSTROPHE: 222, QUOTE: 222, 21 | COMMA: 188, LESS_THAN: 188, PERIOD: 190, GREATER_THAN: 190, 22 | SLASH: 191, QUESTION: 191, EXCLAMATION: 49, AT: 50, 23 | HASH: 51, DOLLAR: 52, PERCENT: 53, CARET: 54, 24 | AMPERSAND: 55, ASTERISK: 56, LEFT_PAREN: 57, RIGHT_PAREN: 48, 25 | 26 | // Function keys 27 | F1: 112, F2: 113, F3: 114, F4: 115, F5: 116, F6: 117, 28 | F7: 118, F8: 119, F9: 120, F10: 121, F11: 122, F12: 123, 29 | 30 | // Control keys 31 | ENTER: 13, SPACE: 32, ESCAPE: 27, BACKSPACE: 8, TAB: 9, 32 | SHIFT: 16, CTRL: 17, ALT: 18, CAPS_LOCK: 20, PAUSE: 19, 33 | 34 | // Navigation keys 35 | INSERT: 45, HOME: 36, DELETE: 46, END: 35, 36 | PAGE_UP: 33, PAGE_DOWN: 34, 37 | 38 | // Arrow keys 39 | ARROW_UP: 38, ARROW_DOWN: 40, ARROW_LEFT: 37, ARROW_RIGHT: 39, 40 | 41 | // Numpad keys 42 | NUMPAD_0: 96, NUMPAD_1: 97, NUMPAD_2: 98, NUMPAD_3: 99, 43 | NUMPAD_4: 100, NUMPAD_5: 101, NUMPAD_6: 102, NUMPAD_7: 103, 44 | NUMPAD_8: 104, NUMPAD_9: 105, 45 | NUMPAD_MULTIPLY: 106, NUMPAD_ADD: 107, NUMPAD_SUBTRACT: 109, 46 | NUMPAD_DECIMAL: 110, NUMPAD_DIVIDE: 111, NUMPAD_ENTER: 13 47 | }; 48 | 49 | // Create a priority list of keys to prefer in the reverse mapping 50 | const baseKeyPriority = { 51 | // Numbers take priority over their shifted symbol versions 52 | 48: 'ZERO', 49: 'ONE', 50: 'TWO', 51: 'THREE', 52: 'FOUR', 53 | 53: 'FIVE', 54: 'SIX', 55: 'SEVEN', 56: 'EIGHT', 57: 'NINE', 54 | 55 | // Other keys where we want the base version 56 | 192: 'BACKTICK', 173: 'MINUS', 61: 'EQUALS', 219: 'L_BRACKET', 57 | 221: 'R_BRACKET', 220: 'BACKSLASH', 186: 'SEMICOLON', 58 | 222: 'QUOTE', 188: 'COMMA', 190: 'PERIOD', 191: 'SLASH' 59 | }; 60 | 61 | export const KeyName = Object.fromEntries( 62 | Object.entries(KeyCode) 63 | // Filter to only include the preferred base version for each code 64 | .filter(([name, code]) => !(code in baseKeyPriority) || baseKeyPriority[code] === name) 65 | // Then create the reverse mapping 66 | .map(([name, code]) => [code, name]) 67 | ); 68 | 69 | export function formatDuration(milliseconds, maxPrecision = 4) { 70 | if (milliseconds <= 0) { 71 | return "0 seconds"; 72 | } 73 | 74 | let seconds = Math.ceil(milliseconds / 1000); 75 | let minutes = Math.floor(seconds / 60); 76 | let hours = Math.floor(seconds / 60 / 60); 77 | let days = Math.floor(seconds / 60 / 60 / 24); 78 | 79 | seconds %= 60; 80 | minutes %= 60; 81 | hours %= 24; 82 | 83 | let prec = 0; 84 | const parts = []; 85 | const getS = num => num !== 1 ? 's' : ''; 86 | 87 | if (days > 0 && prec < maxPrecision) { 88 | ++prec; 89 | parts.push(`${days} day${getS(days)}`); 90 | } 91 | 92 | if (hours > 0 && prec < maxPrecision) { 93 | ++prec; 94 | parts.push(`${hours} hour${getS(hours)}`); 95 | } 96 | 97 | if (minutes > 0 && prec < maxPrecision) { 98 | ++prec; 99 | parts.push(`${minutes} minute${getS(minutes)}`); 100 | } 101 | 102 | if (seconds > 0 && prec < maxPrecision) { 103 | ++prec; 104 | parts.push(`${seconds} second${getS(seconds)}`); 105 | } 106 | 107 | return parts.join(" "); 108 | } 109 | 110 | let time = Date.now(); 111 | export function getTime(update) { 112 | return update ? (time = Date.now()) : time; 113 | } 114 | 115 | export function setCookie(name, value) { 116 | document.cookie = `${name}=${value}; expires=Fri, 31 Dec 9999 23:59:59 GMT`; 117 | } 118 | 119 | export function getCookie(name) { 120 | var cookie = document.cookie.split(';'); 121 | for (var i = 0; i < cookie.length; i++) { 122 | var idx = cookie[i].indexOf(name + '='); 123 | if (idx === 0 || (idx === 1 && cookie[i][0] === ' ')) { 124 | var off = idx + name.length + 1; 125 | return cookie[i].substring(off, cookie[i].length); 126 | } 127 | } 128 | return null; 129 | } 130 | 131 | export function b64EncodeUnicode(str) { 132 | return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) { 133 | return String.fromCharCode(parseInt(p1, 16)) 134 | })) 135 | } 136 | 137 | export function cookiesEnabled() { 138 | return navigator.cookieEnabled; 139 | } 140 | 141 | export function storageEnabled() { 142 | try { 143 | return !!window.localStorage; 144 | } catch (e) { 145 | return false; 146 | } 147 | } 148 | 149 | export function propertyDefaults(obj, defaults) { 150 | if (obj) { 151 | for (var prop in obj) { 152 | if (obj.hasOwnProperty(prop)) { 153 | defaults[prop] = obj[prop]; 154 | } 155 | } 156 | } 157 | return defaults; 158 | } 159 | 160 | // This fixes modulo to work on negative numbers (-1 % 16 = 15) 161 | export function absMod(n1, n2) { 162 | return ((n1 % n2) + n2) % n2; 163 | } 164 | 165 | export function htmlToElement(html) { 166 | return mkHTML("template", { 167 | innerHTML: html 168 | }).content.firstChild; 169 | } 170 | 171 | export function escapeHTML(text) { 172 | return text.replace(/&/g, '&') 173 | .replace(//g, '>') 175 | .replace(/\"/g, '"') 176 | .replace(/\'/g, ''') 177 | .replace(/\//g, '/'); 178 | } 179 | 180 | /* Makes an HTML element with the values specified in opts */ 181 | export function mkHTML(tag, opts) { 182 | var elm = document.createElement(tag); 183 | for (var i in opts) { 184 | elm[i] = opts[i]; 185 | } 186 | return elm; 187 | } 188 | 189 | export function loadScript(name, callback) { 190 | document.getElementsByTagName('head')[0].appendChild(mkHTML("script", { 191 | type: "text/javascript", 192 | src: name, 193 | onload: callback 194 | })); 195 | } 196 | 197 | export function eventOnce(element, events, func) { 198 | var ev = events.split(' '); 199 | var f = e => { 200 | for (var i = 0; i < ev.length; i++) { 201 | element.removeEventListener(ev[i], f); 202 | } 203 | return func(); 204 | }; 205 | 206 | for (var i = 0; i < ev.length; i++) { 207 | element.addEventListener(ev[i], f); 208 | } 209 | } 210 | 211 | // new tooltip logic 212 | let lastTooltipText = ''; 213 | 214 | export function initializeTooltips() { 215 | initDOMTooltips(); 216 | let tooltip = document.createElement('div'); 217 | tooltip.id = 'tooltip'; 218 | document.body.appendChild(tooltip); 219 | tooltip.style.opacity = '0%'; 220 | } 221 | 222 | export function setTooltip(element, message, immediate = false) { 223 | element.setAttribute('tooltip', message); 224 | element.setAttribute('ttApplied', 'true'); 225 | element.addEventListener('mousemove', e => { tooltipHover(e); }); 226 | element.addEventListener('mouseleave', tooltipLeave); 227 | 228 | if (immediate) { 229 | const rect = element.getBoundingClientRect(); 230 | if ( 231 | window.clientX < rect.left || 232 | window.clientX > rect.right || 233 | window.clientY < rect.top || 234 | window.clientY > rect.bottom 235 | ) return; 236 | 237 | element.dispatchEvent(new MouseEvent('mousemove', { 238 | bubbles: true, 239 | cancelable: true, 240 | clientX: window.clientX, 241 | clientY: window.clientY, 242 | })); 243 | } 244 | } 245 | 246 | export function forceHideTooltip(){ 247 | tooltipLeave(); 248 | } 249 | 250 | function initDOMTooltips() { 251 | let elements = document.querySelectorAll('[tooltip]'); 252 | for (let element of elements) { 253 | if (element.getAttribute('ttApplied') == 'true') continue; 254 | element.addEventListener('mousemove', e => { tooltipHover(e); }); 255 | element.addEventListener('mouseleave', tooltipLeave); 256 | element.setAttribute('ttApplied', 'true'); 257 | } 258 | } 259 | 260 | function tooltipHover(e) { 261 | const tooltip = document.getElementById('tooltip'); 262 | const tooltipText = e.target.getAttribute('tooltip'); 263 | if (tooltipText != lastTooltipText) { 264 | tooltip.innerHTML = tooltipText; 265 | lastTooltipText = tooltipText; 266 | } 267 | tooltip.style.opacity = '100%'; 268 | const tipRect = tooltip.getBoundingClientRect(); 269 | let tipX = e.clientX + 20; 270 | let tipY = e.clientY + 20; 271 | if (tipX + tipRect.width > window.innerWidth) { 272 | tipX = e.clientX - tooltip.offsetWidth - 20; 273 | } 274 | 275 | if (tipY + tipRect.height > window.innerHeight) { 276 | tipY = e.clientY - tooltip.offsetHeight - 20; 277 | } 278 | 279 | if (tipY < 0) { 280 | tipY = 0; 281 | } 282 | 283 | tooltip.style.top = tipY + 'px'; 284 | tooltip.style.left = tipX + 'px'; 285 | } 286 | 287 | function tooltipLeave() { 288 | tooltip.style.opacity = '0%'; 289 | } 290 | 291 | // export function setTooltip(element, message) { 292 | // const elementSpacing = 10; 293 | // var intr = 0; 294 | // var tip = null; 295 | // function tooltip() { 296 | // var epos = element.getBoundingClientRect(); 297 | // var y = epos.top + epos.height / 2; 298 | // tip = mkHTML('span', { 299 | // innerHTML: message, 300 | // className: 'framed tooltip whitetext' 301 | // }); 302 | // document.body.appendChild(tip); 303 | // var tpos = tip.getBoundingClientRect(); 304 | // y -= tpos.height / 2; 305 | // var x = epos.left - tpos.width - elementSpacing; 306 | // if (x < elementSpacing) { 307 | // x = epos.right + elementSpacing; 308 | // } 309 | // tip.style.transform = `translate(${Math.round(x)}px,${Math.round(y)}px)`; 310 | // intr = 0; 311 | // } 312 | // const mleave = e => { 313 | // clearTimeout(intr); 314 | // intr = 0; 315 | // element.removeEventListener('mouseleave', mleave); 316 | // element.removeEventListener('click', mleave); 317 | // element.removeEventListener('DOMNodeRemoved', mleave); 318 | // if (tip !== null) { 319 | // tip.remove(); 320 | // tip = null; 321 | // } 322 | // }; 323 | // const menter = e => { 324 | // if (tip === null && intr === 0) { 325 | // intr = setTimeout(tooltip, 500); 326 | // element.addEventListener('click', mleave); 327 | // element.addEventListener('mouseleave', mleave); 328 | // element.addEventListener('DOMNodeRemoved', mleave); 329 | // } 330 | // }; 331 | // /*var observer = new MutationObserver(e => { // Why does this not fire at all? 332 | // console.log(e, tip, intr); 333 | // if (e[0].removedNodes && (tip !== null || intr !== 0)) { 334 | // mleave(); 335 | // } 336 | // }); 337 | // observer.observe(element, { childList: true, subtree: true });*/ 338 | // element.addEventListener('mouseenter', menter); 339 | // } 340 | 341 | /* Waits n frames */ 342 | export function waitFrames(n, cb) { 343 | window.requestAnimationFrame(() => { 344 | return n > 0 ? waitFrames(--n, cb) : cb(); 345 | }) 346 | } 347 | 348 | export function decompress(u8arr) { 349 | var originalLength = u8arr[1] << 8 | u8arr[0]; 350 | var u8decompressedarr = new Uint8Array(originalLength); 351 | var numOfRepeats = u8arr[3] << 8 | u8arr[2]; 352 | var offset = numOfRepeats * 2 + 4; 353 | var uptr = 0; 354 | var cptr = offset; 355 | for (var i = 0; i < numOfRepeats; i++) { 356 | var currentRepeatLoc = (u8arr[4 + i * 2 + 1] << 8 | u8arr[4 + i * 2]) + offset; 357 | while (cptr < currentRepeatLoc) { 358 | u8decompressedarr[uptr++] = u8arr[cptr++]; 359 | } 360 | var repeatedNum = u8arr[cptr + 1] << 8 | u8arr[cptr]; 361 | var repeatedColorR = u8arr[cptr + 2]; 362 | var repeatedColorG = u8arr[cptr + 3]; 363 | var repeatedColorB = u8arr[cptr + 4]; 364 | cptr += 5; 365 | while (repeatedNum--) { 366 | u8decompressedarr[uptr] = repeatedColorR; 367 | u8decompressedarr[uptr + 1] = repeatedColorG; 368 | u8decompressedarr[uptr + 2] = repeatedColorB; 369 | uptr += 3; 370 | } 371 | } 372 | while (cptr < u8arr.length) { 373 | u8decompressedarr[uptr++] = u8arr[cptr++]; 374 | } 375 | return u8decompressedarr; 376 | } 377 | 378 | /*function decompressu16(input) { 379 | var originalLength = (((input[1] & 0xFF) << 8 | (input[0] & 0xFF)) + 1) * 2; 380 | var output = new Uint8Array(originalLength); 381 | var numOfRepeats = (input[3] & 0xFF) << 8 | (input[2] & 0xFF); 382 | var offset = numOfRepeats * 2 + 4; 383 | var uptr = 0; 384 | var cptr = offset; 385 | for (var i = 0; i < numOfRepeats; i++) { 386 | var currentRepeatLoc = 2 * ((((input[4 + i * 2 + 1] & 0xFF) << 8) | (input[4 + i * 2] & 0xFF))) 387 | + offset; 388 | while (cptr < currentRepeatLoc) { 389 | output[uptr++] = input[cptr++]; 390 | } 391 | var repeatedNum = ((input[cptr + 1] & 0xFF) << 8 | (input[cptr] & 0xFF)) + 1; 392 | var repeatedColorRGB = (input[cptr + 3] & 0xFF) << 8 | (input[cptr + 2] & 0xFF); 393 | cptr += 4; 394 | while (repeatedNum-- != 0) { 395 | output[uptr] = (repeatedColorRGB & 0xFF); 396 | output[uptr + 1] = ((repeatedColorRGB & 0xFF00) >> 8); 397 | uptr += 2; 398 | } 399 | } 400 | while (cptr < input.length) { 401 | output[uptr++] = input[cptr++]; 402 | } 403 | return output; 404 | }*/ 405 | 406 | export function line(x1, y1, x2, y2, size, plot) { 407 | var dx = Math.abs(x2 - x1), sx = x1 < x2 ? 1 : -1; 408 | var dy = -Math.abs(y2 - y1), sy = y1 < y2 ? 1 : -1; 409 | var err = dx + dy, 410 | e2; 411 | 412 | while (true) { 413 | plot(x1, y1); 414 | if (x1 == x2 && y1 == y2) break; 415 | e2 = 2 * err; 416 | if (e2 >= dy) { err += dy; x1 += sx; } 417 | if (e2 <= dx) { err += dx; y1 += sy; } 418 | } 419 | } 420 | 421 | PublicAPI.util = { 422 | getTime, 423 | cookiesEnabled, 424 | storageEnabled, 425 | absMod, 426 | escapeHTML, 427 | mkHTML, 428 | setTooltip, 429 | waitFrames, 430 | line, 431 | loadScript, 432 | setCookie, 433 | getCookie, 434 | propertyDefaults, 435 | htmlToElement, 436 | decompress, 437 | 438 | KeyCode, 439 | KeyName, 440 | colorUtils: color 441 | 442 | }; 443 | -------------------------------------------------------------------------------- /src/js/colorPicker.js: -------------------------------------------------------------------------------- 1 | import { GUIWindow, windowSys } from "./windowsys"; 2 | import { waitFrames } from "./util/misc"; 3 | 4 | export default class ColorPicker { 5 | #wasConfirmed = false; 6 | #movefunc = null; 7 | self = null; 8 | selfw = null; 9 | container = null; 10 | canvas = null; 11 | #ctx = null; 12 | sCanvas = null; 13 | #sCtx = null; 14 | #cc = null; 15 | cHandle = null; 16 | sHandle = null; 17 | #internalOnChange = null; 18 | // defaults 19 | options = { 20 | startColor: { 21 | h: 0, 22 | s: 1, 23 | l: 0.5, 24 | str: 'hsl(0, 100%, 50%)' 25 | }, 26 | anchorPreset: 'left', 27 | onChange: null, 28 | onSelect: null, 29 | onCancel: null, 30 | onClose: null, 31 | parentElement: null, 32 | closeable: true, 33 | draggable: true, 34 | }; 35 | /** 36 | * @constructor 37 | * @param {object} options - options for how it should behave 38 | * @param {HTMLElement|undefined} options.parentElement - the element to bind to 39 | * @param {string|undefined} options.startColor - the color to start on 40 | * @param {string|undefined} options.anchorPreset - which side to appear on ("left" or "right") 41 | * @param {void} options.onChange - callback for when color changes 42 | * @param {void} options.onSelect - callback for when finishing color selection 43 | * @param {void} options.onCancel - callback for when cancelling color selection 44 | * @param {void} options.onClose - callback for when the color selector closes, runs before it is destroyed. 45 | */ 46 | constructor(options) { 47 | if (!!options) { 48 | if (options.startColor) options.startColor = this.#parseColor(options.startColor); 49 | } 50 | this.options = { ...this.options, ...options }; 51 | this.#init(); 52 | } 53 | 54 | #parseColor = str => { 55 | console.log(str); 56 | const defaultClr = { h: 0, s: 1, l: 0.5, str: 'hsl(0, 100%, 50%)' }; 57 | if (!this.#isValidColor(str)) return defaultClr; 58 | const { r, g, b, a } = this.#normalizeColorToRGBA(str); 59 | 60 | const rn = r / 255; 61 | const gn = g / 255; 62 | const bn = b / 255; 63 | const max = Math.max(rn, gn, bn); 64 | const min = Math.min(rn, gn, bn); 65 | const delta = max - min; 66 | 67 | let h = 0, s = 0, l = (max + min) / 2; 68 | 69 | if (delta !== 0) { 70 | s = l < 0.5 ? delta / (max + min) : delta / (2 - max - min); 71 | 72 | switch (max) { 73 | case rn: h = (gn - bn) / delta + (gn < bn ? 6 : 0); break; 74 | case gn: h = (bn - rn) / delta + 2; break; 75 | case bn: h = (rn - gn) / delta + 4; break; 76 | } 77 | 78 | h *= 60; 79 | } 80 | 81 | return { 82 | h, 83 | s, 84 | l, 85 | str: `hsl(${Math.round(h)}, ${(s * 100).toFixed(1)}%, ${(l * 100).toFixed(1)}%)` 86 | } 87 | } 88 | 89 | #normalizeColorToRGBA = (colorStr) => { 90 | const canvas = document.createElement('canvas'); 91 | canvas.width = canvas.height = 1; 92 | const ctx = canvas.getContext('2d'); 93 | 94 | ctx.clearRect(0, 0, 1, 1); 95 | ctx.fillStyle = '#000'; // fallback 96 | ctx.fillStyle = colorStr; 97 | ctx.fillRect(0, 0, 1, 1); 98 | const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data; 99 | 100 | return { r, g, b, a: +(a / 255).toFixed(3) }; 101 | }; 102 | 103 | hslToRgb(h, s, l) { 104 | let c = (1 - Math.abs(2 * l - 1)) * s; 105 | let x = c * (1 - Math.abs((h / 60) % 2 - 1)); 106 | let m = l - c / 2; 107 | let r = 0, g = 0, b = 0; 108 | 109 | if (h < 60) [r, g, b] = [c, x, 0]; 110 | else if (h < 120) [r, g, b] = [x, c, 0]; 111 | else if (h < 180) [r, g, b] = [0, c, x]; 112 | else if (h < 240) [r, g, b] = [0, x, c]; 113 | else if (h < 300) [r, g, b] = [x, 0, c]; 114 | else[r, g, b] = [c, 0, x]; 115 | 116 | r = Math.round((r + m) * 255); 117 | g = Math.round((g + m) * 255); 118 | b = Math.round((b + m) * 255); 119 | 120 | return { r, g, b }; 121 | } 122 | 123 | rgbToHex(r, g, b) { 124 | return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; 125 | } 126 | 127 | parseHSLString(str) { 128 | const match = str.match(/hsl\((\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?)%,\s*(\d+(?:\.\d+)?)%\)/); 129 | if (!match) { 130 | console.warn("Invalid HSL color:", str); 131 | return { h: 0, s: 1, l: 0.5, str: "hsl(0, 100%, 50%)" }; 132 | } 133 | const [, h, s, l] = match.map(Number); 134 | return { 135 | h, 136 | s: s / 100, 137 | l: l / 100, 138 | str 139 | }; 140 | } 141 | 142 | getSelectedColor = () => { 143 | const x = this.cHandle.offsetLeft + this.cHandle.offsetWidth / 2; 144 | const y = this.cHandle.offsetTop + this.cHandle.offsetHeight / 2; 145 | const w = this.canvas.clientWidth; 146 | const h = this.canvas.clientHeight; 147 | 148 | const hue = ((this.sHandle.offsetTop + this.sHandle.offsetHeight / 2) / this.sCanvas.clientHeight) * 360; 149 | const sat = x / w; 150 | const brightness = 1 - (y / h); 151 | 152 | return { 153 | h: hue, 154 | s: sat, 155 | l: brightness / 2 * (2 - sat), 156 | str: `hsl(${hue.toFixed(0)},${(sat * 100).toFixed(1)}%, ${(brightness * 50).toFixed(1)}%)` 157 | }; 158 | } 159 | 160 | canvasListeners = (which, toggle) => { 161 | let handle; 162 | if (which === this.canvas) handle = this.cHandle; 163 | else if (which === this.sCanvas) handle = this.sHandle; 164 | else return; 165 | const cClickListener = e => { 166 | const crect = which.getBoundingClientRect(); 167 | const x = e.clientX - crect.left; 168 | const y = e.clientY - crect.top; 169 | 170 | const hw = handle.offsetWidth / 2; 171 | const hh = handle.offsetHeight / 2; 172 | if (which === this.canvas) handle.style.left = `${Math.max(-hw, Math.min(x - hw, which.clientWidth - hw))}px`; 173 | handle.style.top = `${Math.max(-hh, Math.min(y - hh, which.clientHeight - hh))}px`; 174 | const clr = this.getSelectedColor(); 175 | this.#cc = clr; 176 | 177 | this.canvas.update(); 178 | this.sCanvas.update(); 179 | 180 | if(this.#internalOnChange) this.#internalOnChange(clr); 181 | 182 | handle.isDragging = true; 183 | handle.ox = handle.offsetHeight / 2; 184 | handle.oy = handle.offsetHeight / 2; 185 | handle.classList.add('picker-dragging'); 186 | this.self.classList.add('picker-dragging'); 187 | } 188 | 189 | if (toggle) which.addEventListener("mousedown", cClickListener); 190 | else which.removeEventListener("mousedown", cClickListener); 191 | } 192 | 193 | handleListeners = (which, toggle) => { 194 | const handleDownFunc = (e) => { 195 | which.isDragging = true; 196 | which.ox = e.offsetX; 197 | which.oy = e.offsetY; 198 | which.classList.add('picker-dragging'); 199 | this.self.classList.add('picker-dragging'); 200 | } 201 | 202 | const handleMoveFunc = (e) => { 203 | if (!which.isDragging) return; 204 | const crect = this.canvas.getBoundingClientRect(); 205 | const nx = e.clientX - crect.left - which.ox; 206 | const ny = e.clientY - crect.top - which.oy; 207 | 208 | if (which === this.cHandle) { 209 | const mx = this.canvas.clientWidth - which.offsetWidth / 2; 210 | const my = this.canvas.clientHeight - which.offsetHeight / 2; 211 | which.style.left = Math.max(-which.offsetWidth / 2, Math.min(nx, mx)) + "px"; 212 | which.style.top = Math.max(-which.offsetHeight / 2, Math.min(ny, my)) + "px"; 213 | } else if (which === this.sHandle) { 214 | const my = this.sCanvas.clientHeight - which.offsetHeight / 2; 215 | which.style.top = Math.max(-which.offsetHeight / 2, Math.min(ny, my)) + "px"; 216 | } 217 | 218 | const clr = this.getSelectedColor(); 219 | this.#cc = clr; 220 | 221 | this.canvas.update(); 222 | this.sCanvas.update(); 223 | if (this.options.onChange) this.options.onChange(clr); 224 | if(this.#internalOnChange) this.#internalOnChange(clr); 225 | } 226 | 227 | const handleUpFunc = (e) => { 228 | which.isDragging = false; 229 | which.ox = 0; 230 | which.oy = 0; 231 | which.classList.remove('picker-dragging'); 232 | this.self.classList.remove('picker-dragging'); 233 | } 234 | 235 | if (toggle) { 236 | which.addEventListener("mousedown", handleDownFunc); 237 | document.addEventListener("mousemove", handleMoveFunc); 238 | document.addEventListener("mouseup", handleUpFunc); 239 | } else { 240 | which.removeEventListener("mousedown", handleDownFunc); 241 | document.removeEventListener("mousemove", handleMoveFunc); 242 | document.removeEventListener("mouseup", handleUpFunc); 243 | } 244 | } 245 | 246 | #isValidColor = str => { 247 | let s = new Option().style; 248 | s.color = str; 249 | return s.color !== ''; 250 | } 251 | 252 | #init() { 253 | // this.self = document.createElement('div'); 254 | this.selfw = windowSys.addWindow(new GUIWindow("Color Picker", { centerOnce: !this.options.parentElement, closeable: this.options.closeable })); 255 | waitFrames(0, ()=>{ 256 | if(this.options.parentElement) { 257 | if(this.options.anchorPreset === 'left'){ 258 | this.#movefunc = ()=> { 259 | this.selfw.opt.immobile = false; 260 | this.selfw.move(this.options.parentElement.getBoundingClientRect().left - this.selfw.frame.offsetWidth - 8, this.options.parentElement.getBoundingClientRect().top); 261 | this.selfw.opt.immobile = !this.options.draggable; 262 | } 263 | } 264 | this.#movefunc(); 265 | window.addEventListener('resize', this.#movefunc); 266 | } 267 | this.selfw.opt.immobile = !this.options.draggable; 268 | }); 269 | this.self = this.selfw.container; 270 | this.container = document.createElement('div'); 271 | this.canvas = document.createElement('canvas'); 272 | this.canvas.classList.add('color-picker-canvas'); 273 | this.sCanvas = document.createElement('canvas'); 274 | this.sCanvas.classList.add('color-picker-slider'); 275 | this.#ctx = this.canvas.getContext('2d'); 276 | this.#sCtx = this.sCanvas.getContext('2d'); 277 | 278 | this.cw = document.createElement('div'); 279 | this.sw = document.createElement('div'); 280 | this.cw.style.position = 'relative'; 281 | this.sw.style.position = 'relative'; 282 | // this.cw.style.flex = '1'; 283 | // this.sw.style.flex = '0 0 10px'; // matches your .color-picker-slider width 284 | this.container.appendChild(this.cw); 285 | this.container.appendChild(this.sw); 286 | 287 | console.log(this.options.startColor); 288 | this.#cc = this.options.startColor; 289 | 290 | this.cw.appendChild(this.canvas); 291 | this.sw.appendChild(this.sCanvas); 292 | this.self.appendChild(this.container); 293 | this.canvas.update = () => { 294 | const w = this.canvas.width; 295 | const h = this.canvas.height; 296 | const clr = `hsl(${this.#cc.h}, 100%, 50%)`; 297 | const hg = this.#ctx.createLinearGradient(0, 0, w, 0); 298 | hg.addColorStop(0, "white"); 299 | hg.addColorStop(1, clr); 300 | this.#ctx.fillStyle = hg; 301 | this.#ctx.fillRect(0, 0, w, h); 302 | 303 | const vg = this.#ctx.createLinearGradient(0, 0, 0, h); 304 | vg.addColorStop(0, "rgba(0,0,0,0)"); 305 | vg.addColorStop(1, "rgba(0,0,0,1)"); 306 | this.#ctx.fillStyle = vg; 307 | this.#ctx.fillRect(0, 0, w, h); 308 | } 309 | this.sCanvas.update = () => { 310 | const w = this.sCanvas.width; 311 | const h = this.sCanvas.height; 312 | const vg = this.#sCtx.createLinearGradient(0, 0, 0, h); 313 | 314 | const steps = 360; // 1 per hue degree for smoothness 315 | for (let i = 0; i <= steps; i++) { 316 | const hue = (i / steps) * 360; 317 | vg.addColorStop(i / steps, `hsl(${hue}, 100%, 50%)`); 318 | } 319 | this.#sCtx.fillStyle = vg; 320 | this.#sCtx.fillRect(0, 0, w, h); 321 | } 322 | // this.self.classList.add('color-picker-frame'); 323 | this.container.style.padding = '8px'; 324 | this.container.classList.add('color-picker-container'); 325 | 326 | this.cHandle = document.createElement('div'); 327 | this.cHandle.isDragging = false; 328 | this.cHandle.ox = 0; 329 | this.cHandle.oy = 0; 330 | this.cHandle.classList.add('draggableHandle'); 331 | this.handleListeners(this.cHandle, true); 332 | this.sHandle = document.createElement('div'); 333 | this.sHandle.isDragging = false; 334 | this.sHandle.ox = 0; 335 | this.sHandle.oy = 0; 336 | this.sHandle.classList.add('draggableHandle'); 337 | this.handleListeners(this.sHandle, true); 338 | 339 | this.canvasListeners(this.canvas, true); 340 | this.canvasListeners(this.sCanvas, true); 341 | 342 | this.cw.appendChild(this.cHandle); 343 | this.sw.appendChild(this.sHandle); 344 | 345 | this.canvas.update(); 346 | this.sCanvas.update(); 347 | 348 | const canvasW = this.canvas.clientWidth; 349 | const canvasH = this.canvas.clientHeight; 350 | const sliderH = this.sCanvas.clientHeight; 351 | 352 | const b = this.#cc.l * 2 / (2 - this.#cc.s); 353 | const y = (1 - b) * canvasH; 354 | const x = this.#cc.s * canvasW; 355 | const hueY = (this.#cc.h / 360) * sliderH; 356 | 357 | this.cHandle.style.left = `${x - this.cHandle.offsetWidth / 2}px`; 358 | this.cHandle.style.top = `${y - this.cHandle.offsetHeight / 2}px`; 359 | this.sHandle.style.top = `${hueY - this.sHandle.offsetHeight / 2}px`; 360 | 361 | this.self.classList.add('whitetext'); 362 | 363 | const bottom = document.createElement('div'); 364 | bottom.style.display = 'flex'; 365 | bottom.style.flexDirection = 'row'; 366 | bottom.style.alignItems = 'center'; 367 | const outputText = document.createElement('div'); 368 | outputText.style.flex = '1'; 369 | const rgb = this.hslToRgb(this.#cc.h, this.#cc.s, this.#cc.l); 370 | const preview = document.createElement('div'); 371 | preview.style.backgroundColor = `rgb(${rgb.r},${rgb.g},${rgb.b})`; 372 | preview.style.height='20px'; 373 | preview.style.width='20px'; 374 | preview.style.marginRight='8px'; 375 | outputText.innerText = this.rgbToHex(rgb.r, rgb.g, rgb.b); 376 | const submit = document.createElement('button'); 377 | submit.style.cursor = 'pointer'; 378 | submit.innerHTML = 'Confirm'; 379 | 380 | this.#internalOnChange = ()=>{ 381 | const rgb = this.hslToRgb(this.#cc.h, this.#cc.s, this.#cc.l); 382 | preview.style.backgroundColor = `rgb(${rgb.r},${rgb.g},${rgb.b})`; 383 | outputText.innerText = this.rgbToHex(rgb.r, rgb.g, rgb.b); 384 | } 385 | 386 | submit.onclick = () => { 387 | this.#wasConfirmed = true; 388 | if (this.options.onSelect) this.options.onSelect(this.#cc); 389 | this.selfw.close(); 390 | } 391 | 392 | bottom.appendChild(preview); 393 | bottom.appendChild(outputText); 394 | bottom.appendChild(submit); 395 | this.self.appendChild(bottom); 396 | 397 | this.selfw.onclose = () => { 398 | this.canvasListeners(this.canvas, false); 399 | this.canvasListeners(this.sCanvas, false); 400 | this.handleListeners(this.cHandle, false); 401 | this.handleListeners(this.sHandle, false); 402 | if (!this.#wasConfirmed && this.options.onCancel) this.options.onCancel(); 403 | if (this.options.onClose) this.options.onClose(); 404 | if (this.#movefunc) window.removeEventListener('resize', this.#movefunc); 405 | } 406 | } 407 | } -------------------------------------------------------------------------------- /src/js/protocol/old.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { Protocol } from './Protocol.js'; 3 | import { EVENTS as e, RANK, options } from './../conf.js'; 4 | import { eventSys, PublicAPI } from './../global.js'; 5 | import { Chunk } from './../World.js'; 6 | import { Bucket } from './../util/Bucket.js'; 7 | import { decompress } from './../util/misc.js'; 8 | import { loadAndRequestCaptcha } from './../captcha.js'; 9 | import { colorUtils as color } from './../util/color.js'; 10 | import { player, shouldUpdate, networkRankVerification } from './../local_player.js'; 11 | import { camera } from './../canvas_renderer.js'; 12 | import { mouse, elements } from './../main.js'; 13 | import { retryingConnect } from './../main.js'; 14 | 15 | export const captchaState = { 16 | CA_WAITING: 0, 17 | CA_VERIFYING: 1, 18 | CA_VERIFIED: 2, 19 | CA_OK: 3, 20 | CA_INVALID: 4 21 | }; 22 | 23 | export const OldProtocol = { 24 | class: null, 25 | chunkSize: 16, 26 | netUpdateSpeed: 20, 27 | clusterChunkAmount: 64, 28 | maxWorldNameLength: 24, 29 | worldBorder: 0xFFFFF, 30 | chatBucket: [4, 6], 31 | placeBucket: { 32 | [RANK.NONE]: [0, 1], 33 | [RANK.USER]: [32, 4], 34 | [RANK.MODERATOR]: [32, 2], 35 | [RANK.ADMIN]: [32, 0] 36 | }, 37 | maxMessageLength: { 38 | [RANK.NONE]: 128, 39 | [RANK.USER]: 128, 40 | [RANK.MODERATOR]: 512, 41 | [RANK.ADMIN]: 16384 42 | }, 43 | tools: { 44 | id: {}, /* Generated automatically */ 45 | 0: 'cursor', 46 | 1: 'move', 47 | 2: 'pipette', 48 | 3: 'eraser', 49 | 4: 'zoom', 50 | 5: 'fill', 51 | 6: 'paste', 52 | 7: 'export', 53 | 8: 'line', 54 | 9: 'protect', 55 | 10: 'copy' 56 | }, 57 | misc: { 58 | worldVerification: 25565, 59 | chatVerification: String.fromCharCode(10), 60 | tokenVerification: 'CaptchA' 61 | }, 62 | opCode: { 63 | client: { 64 | 65 | }, 66 | server: { 67 | setId: 0, 68 | worldUpdate: 1, 69 | chunkLoad: 2, 70 | teleport: 3, 71 | setRank: 4, 72 | captcha: 5, 73 | setPQuota: 6, 74 | chunkProtected: 7, 75 | maxCount: 8, 76 | donUntil: 9 77 | } 78 | } 79 | }; 80 | 81 | for (const id in OldProtocol.tools) { 82 | if (+id >= 0) { 83 | OldProtocol.tools.id[OldProtocol.tools[id]] = +id; 84 | } 85 | } 86 | 87 | function stoi(string, max) { 88 | var ints = []; 89 | var fstring = ""; 90 | string = string.toLowerCase(); 91 | for (var i = 0; i < string.length && i < max; i++) { 92 | var charCode = string.charCodeAt(i); 93 | if ((charCode < 123 && charCode > 96) 94 | || (charCode < 58 && charCode > 47) 95 | || charCode == 95 || charCode == 46) { 96 | fstring += String.fromCharCode(charCode); 97 | ints.push(charCode); 98 | } 99 | } 100 | return [ints, fstring]; 101 | } 102 | 103 | class OldProtocolImpl extends Protocol { 104 | constructor(ws, worldName, captcha) { 105 | super(ws); 106 | super.hookEvents(this); 107 | this.lastSentX = 0; 108 | this.lastSentY = 0; 109 | this.playercount = 1; 110 | this.worldName = worldName ? worldName : options.defaultWorld; 111 | this.players = {}; 112 | this.chunksLoading = {}; /* duplicate */ 113 | this.waitingForChunks = 0; 114 | this.pendingEdits = {}; 115 | this.id = null; 116 | this.captcha = captcha; 117 | 118 | var params = OldProtocol.chatBucket; 119 | this.chatBucket = new Bucket(params[0], params[1]); 120 | params = OldProtocol.placeBucket[player.rank]; 121 | this.placeBucket = new Bucket(params[0], params[1]); 122 | this.placeBucketMult = 1; 123 | this.donUntilTs = 0; 124 | 125 | this.interval = null; 126 | this.clet = null; 127 | 128 | this.joinFunc = () => { 129 | this.placeBucket.lastCheck = Date.now(); 130 | this.placeBucket.allowance = 0; 131 | //this.chatBucket.allowance = 0; 132 | this.interval = setInterval(() => this.sendUpdates(), 1000 / OldProtocol.netUpdateSpeed); 133 | }; 134 | 135 | const rankChanged = rank => { 136 | this.placeBucket.infinite = rank === RANK.ADMIN; 137 | elements.chatInput.maxLength = OldProtocol.maxMessageLength[rank]; 138 | }; 139 | this.leaveFunc = () => { 140 | eventSys.removeListener(e.net.sec.rank, rankChanged); 141 | eventSys.emit(e.net.donUntil, 0, 1); 142 | }; 143 | eventSys.once(e.net.world.join, this.joinFunc); 144 | eventSys.on(e.net.sec.rank, rankChanged); 145 | } 146 | 147 | errorHandler(err) { 148 | super.errorHandler(err); 149 | } 150 | 151 | closeHandler() { 152 | super.closeHandler(); 153 | clearInterval(this.interval); 154 | eventSys.emit(e.net.sec.rank, RANK.NONE); 155 | eventSys.removeListener(e.net.world.join, this.joinFunc); 156 | this.leaveFunc(); 157 | } 158 | 159 | messageHandler(message) { 160 | message = message.data; 161 | if (typeof message === "string") { 162 | if (message.indexOf("DEV") == 0) { 163 | eventSys.emit(e.net.devChat, message.slice(3)); 164 | } else { 165 | eventSys.emit(e.net.chat, message); 166 | } 167 | return; 168 | } 169 | 170 | var dv = new DataView(message); 171 | var oc = OldProtocol.opCode.server; 172 | switch (dv.getUint8(0)) { 173 | case oc.setId: // Get id 174 | let id = dv.getUint32(1, true); 175 | this.id = id; 176 | eventSys.emit(e.net.world.join, this.worldName); 177 | eventSys.emit(e.net.world.setId, id); 178 | eventSys.emit(e.net.playerCount, this.playercount); 179 | eventSys.emit(e.net.chat, JSON.stringify({ 180 | sender: 'server', 181 | type: 'info', 182 | data:{ 183 | message: "[Server] Joined world: \"" + this.worldName + "\", your ID is: " + id + "!" 184 | } 185 | })); 186 | break; 187 | 188 | case oc.worldUpdate: // Get all cursors, tile updates, disconnects 189 | var shouldrender = 0; 190 | // Cursors 191 | var updated = false; 192 | var updates = {}; 193 | for (var i = dv.getUint8(1); i--;) { 194 | updated = true; 195 | var pid = dv.getUint32(2 + i * 16, true); 196 | if (pid === this.id) { 197 | continue; 198 | } 199 | var pmx = dv.getInt32(2 + i * 16 + 4, true); 200 | var pmy = dv.getInt32(2 + i * 16 + 8, true); 201 | var pr = dv.getUint8(2 + i * 16 + 12); 202 | var pg = dv.getUint8(2 + i * 16 + 13); 203 | var pb = dv.getUint8(2 + i * 16 + 14); 204 | var ptool = dv.getUint8(2 + i * 16 + 15); 205 | updates[pid] = { 206 | x: pmx, 207 | y: pmy, 208 | rgb: [pr, pg, pb], 209 | tool: OldProtocol.tools[ptool] 210 | }; 211 | if (!this.players[pid]) { 212 | ++this.playercount; 213 | eventSys.emit(e.net.playerCount, this.playercount); 214 | this.players[pid] = true; 215 | } 216 | } 217 | if (updated) { 218 | eventSys.emit(e.net.world.playersMoved, updates); 219 | } 220 | var off = 2 + dv.getUint8(1) * 16; 221 | // Tile updates 222 | updated = false; 223 | updates = []; 224 | for (var i = dv.getUint16(off, true), j = 0; j < i; j++) { 225 | updated = true; 226 | var bid = dv.getUint32(2 + off + j * 15, true); 227 | var bpx = dv.getInt32(2 + off + j * 15 + 4, true); 228 | var bpy = dv.getInt32(2 + off + j * 15 + 8, true); 229 | var br = dv.getUint8(2 + off + j * 15 + 12); 230 | var bg = dv.getUint8(2 + off + j * 15 + 13); 231 | var bb = dv.getUint8(2 + off + j * 15 + 14); 232 | var bbgr = bb << 16 | bg << 8 | br; 233 | updates.push({ 234 | x: bpx, 235 | y: bpy, 236 | rgb: bbgr, 237 | id: bid 238 | }); 239 | 240 | var edkey = `${bpx},${bpy}`; 241 | var edtmoid = this.pendingEdits[edkey]; 242 | if (edtmoid) { 243 | clearTimeout(edtmoid); 244 | delete this.pendingEdits[edkey]; 245 | } 246 | } 247 | if (updated) { 248 | eventSys.emit(e.net.world.tilesUpdated, updates); 249 | } 250 | off += dv.getUint16(off, true) * 15 + 2; 251 | // Disconnects 252 | var decreased = false; 253 | updated = false; 254 | updates = []; 255 | for (var k = dv.getUint8(off); k--;) { 256 | updated = true; 257 | var dpid = dv.getUint32(1 + off + k * 4, true); 258 | updates.push(dpid); 259 | if (this.players[dpid] && this.playercount > 1) { 260 | decreased = true; 261 | --this.playercount; 262 | delete this.players[dpid]; 263 | } 264 | } 265 | if (updated) { 266 | eventSys.emit(e.net.world.playersLeft, updates); 267 | if (decreased) { 268 | eventSys.emit(e.net.playerCount, this.playercount); 269 | } 270 | } 271 | break; 272 | 273 | case oc.chunkLoad: // Get chunk 274 | var chunkX = dv.getInt32(1, true); 275 | var chunkY = dv.getInt32(5, true); 276 | var locked = dv.getUint8(9); 277 | var u8data = new Uint8Array(message, 10, message.byteLength - 10); 278 | //console.log(u8data); 279 | u8data = decompress(u8data); 280 | var key = `${chunkX},${chunkY}`; 281 | var u32data = new Uint32Array(OldProtocol.chunkSize * OldProtocol.chunkSize); 282 | for (var i = 0, u = 0; i < u8data.length; i += 3) { /* Need to make a copy ;-; */ 283 | var color = u8data[i + 2] << 16 284 | | u8data[i + 1] << 8 285 | | u8data[i] 286 | u32data[u++] = 0xFF000000 | color; 287 | } 288 | if (!this.chunksLoading[key]) { 289 | eventSys.emit(e.net.chunk.set, chunkX, chunkY, u32data); 290 | } else { 291 | delete this.chunksLoading[key]; 292 | if (--this.waitingForChunks == 0) { 293 | clearTimeout(this.clet); 294 | this.clet = setTimeout(() => { 295 | eventSys.emit(e.net.chunk.allLoaded); 296 | }, 100); 297 | } 298 | var chunk = new Chunk(chunkX, chunkY, u32data, locked); 299 | eventSys.emit(e.net.chunk.load, chunk); 300 | } 301 | break; 302 | 303 | case oc.teleport: // Teleport 304 | let x = dv.getInt32(1, true); 305 | let y = dv.getInt32(5, true); 306 | eventSys.emit(e.net.world.teleported, x, y); 307 | break; 308 | 309 | case oc.setRank: // new rank 310 | networkRankVerification[0] = dv.getUint8(1); 311 | eventSys.emit(e.net.sec.rank, dv.getUint8(1)); 312 | break; 313 | 314 | case oc.captcha: // Captcha 315 | switch (dv.getUint8(1)) { 316 | case captchaState.CA_WAITING: 317 | // the ws sometimes closes while doing the captcha, showing 318 | // the reconnect screen afterwards, making the user redo it 319 | if(this.captcha) { 320 | let message = OldProtocol.misc.tokenVerification + this.captcha; 321 | this.ws.send(message); 322 | } else { 323 | loadAndRequestCaptcha(); 324 | eventSys.once(e.misc.captchaToken, token => { 325 | let message = OldProtocol.misc.tokenVerification + token; 326 | if(this.ws.readyState != WebSocket.OPEN) { 327 | setTimeout(function() { 328 | retryingConnect(() => options.serverAddress[0], this.worldName, token); 329 | }, 125); 330 | } else { 331 | this.ws.send(message); 332 | } 333 | }); 334 | } 335 | break; 336 | 337 | case captchaState.CA_OK: 338 | this.worldName = this.joinWorld(this.worldName); 339 | break; 340 | } 341 | break; 342 | 343 | case oc.setPQuota: 344 | let rate = dv.getUint16(1, true); 345 | let per = dv.getUint16(3, true); 346 | let oallownc = this.placeBucket.allowance; 347 | let pmult = dv.byteLength >= 6 ? dv.getUint8(5) / 10 : 1; 348 | this.placeBucket = new Bucket(rate, per); 349 | this.placeBucket.allowance = oallownc; 350 | this.placeBucketMult = pmult; 351 | eventSys.emit(e.net.donUntil, this.donUntilTs, this.placeBucketMult); 352 | break; 353 | 354 | case oc.chunkProtected: 355 | let cx = dv.getInt32(1, true); 356 | let cy = dv.getInt32(5, true); 357 | let newState = dv.getUint8(9); 358 | eventSys.emit(e.net.chunk.lock, cx, cy, newState); 359 | break; 360 | 361 | case oc.maxCount: 362 | eventSys.emit(e.net.maxCount, dv.getUint16(1, true)); 363 | break; 364 | 365 | case oc.donUntil: 366 | this.donUntilTs = dv.getUint32(5, true) * Math.pow(2, 32) + dv.getUint32(1, true); 367 | eventSys.emit(e.net.donUntil, this.donUntilTs, this.placeBucketMult); 368 | break; 369 | } 370 | } 371 | 372 | joinWorld(name) { 373 | var nstr = stoi(name, OldProtocol.maxWorldNameLength); 374 | eventSys.emit(e.net.world.joining, name); 375 | var array = new ArrayBuffer(nstr[0].length + 2); 376 | var dv = new DataView(array); 377 | for (var i = nstr[0].length; i--;) { 378 | dv.setUint8(i, nstr[0][i]); 379 | } 380 | dv.setUint16(nstr[0].length, OldProtocol.misc.worldVerification, true); 381 | this.ws.send(array); 382 | return nstr[1]; 383 | } 384 | 385 | requestChunk(x, y) { 386 | let wb = OldProtocol.worldBorder; 387 | var key = `${x},${y}`; 388 | if (x > wb || y > wb || x < ~wb || y < ~wb || this.chunksLoading[key]) { 389 | return; 390 | } 391 | this.chunksLoading[key] = true; 392 | this.waitingForChunks++; 393 | var array = new ArrayBuffer(8); 394 | var dv = new DataView(array); 395 | dv.setInt32(0, x, true); 396 | dv.setInt32(4, y, true); 397 | this.ws.send(array); 398 | } 399 | 400 | allChunksLoaded() { 401 | return this.waitingForChunks === 0; 402 | } 403 | 404 | updatePixel(x, y, rgb, undocb) { 405 | var distx = Math.floor(x / OldProtocol.chunkSize) - Math.floor(this.lastSentX / (OldProtocol.chunkSize * 16)); distx *= distx; 406 | var disty = Math.floor(y / OldProtocol.chunkSize) - Math.floor(this.lastSentY / (OldProtocol.chunkSize * 16)); disty *= disty; 407 | var dist = Math.sqrt(distx + disty); 408 | if (this.isConnected() && (dist < 4 || player.rank == RANK.ADMIN) && this.placeBucket.canSpend(1)) { 409 | var array = new ArrayBuffer(11); 410 | var dv = new DataView(array); 411 | dv.setInt32(0, x, true); 412 | dv.setInt32(4, y, true); 413 | dv.setUint8(8, rgb[0]); 414 | dv.setUint8(9, rgb[1]); 415 | dv.setUint8(10, rgb[2]); 416 | this.ws.send(array); 417 | let key = `${x},${y}`; 418 | if (this.pendingEdits[key]) { 419 | clearTimeout(this.pendingEdits[key]); 420 | } 421 | this.pendingEdits[key] = setTimeout(undocb, 2000); 422 | return true; 423 | } 424 | return false; 425 | } 426 | 427 | sendUpdates() { 428 | var worldx = mouse.worldX; 429 | var worldy = mouse.worldY; 430 | var lastx = this.lastSentX; 431 | var lasty = this.lastSentY; 432 | if (this.isConnected() && shouldUpdate() || (worldx != lastx || worldy != lasty)) { 433 | var selrgb = player.selectedColor; 434 | this.lastSentX = worldx; 435 | this.lastSentY = worldy; 436 | // Send mouse position 437 | var array = new ArrayBuffer(12); 438 | var dv = new DataView(array); 439 | dv.setInt32(0, worldx, true); 440 | dv.setInt32(4, worldy, true); 441 | dv.setUint8(8, selrgb[0]); 442 | dv.setUint8(9, selrgb[1]); 443 | dv.setUint8(10, selrgb[2]); 444 | var tool = player.tool; 445 | var toolId = tool !== null ? +OldProtocol.tools.id[tool.id] : 0; 446 | dv.setUint8(11, toolId); 447 | this.ws.send(array); 448 | } 449 | } 450 | 451 | sendMessage(str) { 452 | if (str.length && this.id !== null) { 453 | if (player.rank == RANK.ADMIN || this.chatBucket.canSpend(1)) { 454 | this.ws.send(str + OldProtocol.misc.chatVerification); 455 | return true; 456 | } else { 457 | eventSys.emit(e.net.chat, JSON.stringify({ 458 | sender: 'server', 459 | type: 'error', 460 | data:{ 461 | message: "Slow down! You're talking too fast!" 462 | } 463 | })); 464 | return false; 465 | } 466 | } 467 | } 468 | 469 | protectChunk(x, y, newState) { 470 | if (this.isConnected() && player.rank > RANK.USER) { 471 | var array = new ArrayBuffer(10); 472 | var dv = new DataView(array); 473 | dv.setInt32(0, x, true); 474 | dv.setInt32(4, y, true); 475 | dv.setUint8(8, newState); 476 | this.ws.send(array); 477 | eventSys.emit(e.net.chunk.lock, x, y, newState, true); 478 | } 479 | } 480 | 481 | setChunk(x, y, data) { 482 | if (!(player.rank == RANK.ADMIN || (player.rank == RANK.MODERATOR && this.placeBucket.canSpend(1.25)))) { 483 | return false; 484 | } 485 | 486 | var buf = new Uint8Array(8 + OldProtocol.chunkSize * OldProtocol.chunkSize * 3); 487 | var dv = new DataView(buf.buffer); 488 | dv.setInt32(0, x, true); 489 | dv.setInt32(4, y, true); 490 | for (var i = 0, b = 8; i < data.length; i++, b += 3) { 491 | buf[b] = data[i] & 0xFF; 492 | buf[b + 1] = data[i] >> 8 & 0xFF; 493 | buf[b + 2] = data[i] >> 16 & 0xFF; 494 | } 495 | this.ws.send(buf.buffer); 496 | return true; 497 | } 498 | 499 | clearChunk(x, y, rgb) { 500 | if (player.rank == RANK.ADMIN || (player.rank == RANK.MODERATOR && this.placeBucket.canSpend(1))) { 501 | var array = new ArrayBuffer(13); 502 | var dv = new DataView(array); 503 | dv.setInt32(0, x, true); 504 | dv.setInt32(4, y, true); 505 | dv.setUint8(8, rgb[0]); 506 | dv.setUint8(9, rgb[1]); 507 | dv.setUint8(10, rgb[2]); 508 | this.ws.send(array); 509 | return true; 510 | } 511 | return false; 512 | } 513 | } 514 | 515 | OldProtocol.class = OldProtocolImpl; 516 | 517 | PublicAPI.Protocol = OldProtocolImpl; 518 | PublicAPI.OldProtocol = OldProtocol; 519 | PublicAPI.captchaState = captchaState; -------------------------------------------------------------------------------- /src/js/util/anchorme.js: -------------------------------------------------------------------------------- 1 | !function(e,a){"object"==typeof exports&&"undefined"!=typeof module?module.exports=a():"function"==typeof define&&define.amd?define(a):(e=e||window).anchorme=a()}(this,function(){"use strict";function e(e,a){return a={exports:{}},e(a,a.exports),a.exports}var a=e(function(e,a){function n(e){return e||(e={attributes:[],ips:!0,emails:!0,urls:!0,files:!0,truncate:1/0,defaultProtocol:"http://",list:!1}),"object"!=typeof e.attributes&&(e.attributes=[]),"boolean"!=typeof e.ips&&(e.ips=!0),"boolean"!=typeof e.emails&&(e.emails=!0),"boolean"!=typeof e.urls&&(e.urls=!0),"boolean"!=typeof e.files&&(e.files=!0),"boolean"!=typeof e.list&&(e.list=!1),"string"!=typeof e.defaultProtocol&&"function"!=typeof e.defaultProtocol&&(e.defaultProtocol="http://"),"number"==typeof e.truncate||"object"==typeof e.truncate&&null!==e.truncate||(e.truncate=1/0),e}function t(e){return!isNaN(Number(e))&&!(Number(e)>65535)}Object.defineProperty(a,"__esModule",{value:!0}),a.defaultOptions=n,a.isPort=t}),n=e(function(e,a){Object.defineProperty(a,"__esModule",{value:!0}),a.tlds=["com","org","net","uk","gov","edu","io","cc","co","aaa","aarp","abarth","abb","abbott","abbvie","abc","able","abogado","abudhabi","ac","academy","accenture","accountant","accountants","aco","active","actor","ad","adac","ads","adult","ae","aeg","aero","aetna","af","afamilycompany","afl","africa","ag","agakhan","agency","ai","aig","aigo","airbus","airforce","airtel","akdn","al","alfaromeo","alibaba","alipay","allfinanz","allstate","ally","alsace","alstom","am","americanexpress","americanfamily","amex","amfam","amica","amsterdam","analytics","android","anquan","anz","ao","aol","apartments","app","apple","aq","aquarelle","ar","aramco","archi","army","arpa","art","arte","as","asda","asia","associates","at","athleta","attorney","au","auction","audi","audible","audio","auspost","author","auto","autos","avianca","aw","aws","ax","axa","az","azure","ba","baby","baidu","banamex","bananarepublic","band","bank","bar","barcelona","barclaycard","barclays","barefoot","bargains","baseball","basketball","bauhaus","bayern","bb","bbc","bbt","bbva","bcg","bcn","bd","be","beats","beauty","beer","bentley","berlin","best","bestbuy","bet","bf","bg","bh","bharti","bi","bible","bid","bike","bing","bingo","bio","biz","bj","black","blackfriday","blanco","blockbuster","blog","bloomberg","blue","bm","bms","bmw","bn","bnl","bnpparibas","bo","boats","boehringer","bofa","bom","bond","boo","book","booking","boots","bosch","bostik","boston","bot","boutique","box","br","bradesco","bridgestone","broadway","broker","brother","brussels","bs","bt","budapest","bugatti","build","builders","business","buy","buzz","bv","bw","by","bz","bzh","ca","cab","cafe","cal","call","calvinklein","cam","camera","camp","cancerresearch","canon","capetown","capital","capitalone","car","caravan","cards","care","career","careers","cars","cartier","casa","case","caseih","cash","casino","cat","catering","catholic","cba","cbn","cbre","cbs","cd","ceb","center","ceo","cern","cf","cfa","cfd","cg","ch","chanel","channel","chase","chat","cheap","chintai","chloe","christmas","chrome","chrysler","church","ci","cipriani","circle","cisco","citadel","citi","citic","city","cityeats","ck","cl","claims","cleaning","click","clinic","clinique","clothing","cloud","club","clubmed","cm","cn","coach","codes","coffee","college","cologne","comcast","commbank","community","company","compare","computer","comsec","condos","construction","consulting","contact","contractors","cooking","cookingchannel","cool","coop","corsica","country","coupon","coupons","courses","cr","credit","creditcard","creditunion","cricket","crown","crs","cruise","cruises","csc","cu","cuisinella","cv","cw","cx","cy","cymru","cyou","cz","dabur","dad","dance","data","date","dating","datsun","day","dclk","dds","de","deal","dealer","deals","degree","delivery","dell","deloitte","delta","democrat","dental","dentist","desi","design","dev","dhl","diamonds","diet","digital","direct","directory","discount","discover","dish","diy","dj","dk","dm","dnp","do","docs","doctor","dodge","dog","doha","domains","dot","download","drive","dtv","dubai","duck","dunlop","duns","dupont","durban","dvag","dvr","dz","earth","eat","ec","eco","edeka","education","ee","eg","email","emerck","energy","engineer","engineering","enterprises","epost","epson","equipment","er","ericsson","erni","es","esq","estate","esurance","et","eu","eurovision","eus","events","everbank","exchange","expert","exposed","express","extraspace","fage","fail","fairwinds","faith","family","fan","fans","farm","farmers","fashion","fast","fedex","feedback","ferrari","ferrero","fi","fiat","fidelity","fido","film","final","finance","financial","fire","firestone","firmdale","fish","fishing","fit","fitness","fj","fk","flickr","flights","flir","florist","flowers","fly","fm","fo","foo","food","foodnetwork","football","ford","forex","forsale","forum","foundation","fox","fr","free","fresenius","frl","frogans","frontdoor","frontier","ftr","fujitsu","fujixerox","fun","fund","furniture","futbol","fyi","ga","gal","gallery","gallo","gallup","game","games","gap","garden","gb","gbiz","gd","gdn","ge","gea","gent","genting","george","gf","gg","ggee","gh","gi","gift","gifts","gives","giving","gl","glade","glass","gle","global","globo","gm","gmail","gmbh","gmo","gmx","gn","godaddy","gold","goldpoint","golf","goo","goodhands","goodyear","goog","google","gop","got","gp","gq","gr","grainger","graphics","gratis","green","gripe","group","gs","gt","gu","guardian","gucci","guge","guide","guitars","guru","gw","gy","hair","hamburg","hangout","haus","hbo","hdfc","hdfcbank","health","healthcare","help","helsinki","here","hermes","hgtv","hiphop","hisamitsu","hitachi","hiv","hk","hkt","hm","hn","hockey","holdings","holiday","homedepot","homegoods","homes","homesense","honda","honeywell","horse","hospital","host","hosting","hot","hoteles","hotmail","house","how","hr","hsbc","ht","htc","hu","hughes","hyatt","hyundai","ibm","icbc","ice","icu","id","ie","ieee","ifm","ikano","il","im","imamat","imdb","immo","immobilien","in","industries","infiniti","info","ing","ink","institute","insurance","insure","int","intel","international","intuit","investments","ipiranga","iq","ir","irish","is","iselect","ismaili","ist","istanbul","it","itau","itv","iveco","iwc","jaguar","java","jcb","jcp","je","jeep","jetzt","jewelry","jio","jlc","jll","jm","jmp","jnj","jo","jobs","joburg","jot","joy","jp","jpmorgan","jprs","juegos","juniper","kaufen","kddi","ke","kerryhotels","kerrylogistics","kerryproperties","kfh","kg","kh","ki","kia","kim","kinder","kindle","kitchen","kiwi","km","kn","koeln","komatsu","kosher","kp","kpmg","kpn","kr","krd","kred","kuokgroup","kw","ky","kyoto","kz","la","lacaixa","ladbrokes","lamborghini","lamer","lancaster","lancia","lancome","land","landrover","lanxess","lasalle","lat","latino","latrobe","law","lawyer","lb","lc","lds","lease","leclerc","lefrak","legal","lego","lexus","lgbt","li","liaison","lidl","life","lifeinsurance","lifestyle","lighting","like","lilly","limited","limo","lincoln","linde","link","lipsy","live","living","lixil","lk","loan","loans","locker","locus","loft","lol","london","lotte","lotto","love","lpl","lplfinancial","lr","ls","lt","ltd","ltda","lu","lundbeck","lupin","luxe","luxury","lv","ly","ma","macys","madrid","maif","maison","makeup","man","management","mango","market","marketing","markets","marriott","marshalls","maserati","mattel","mba","mc","mcd","mcdonalds","mckinsey","md","me","med","media","meet","melbourne","meme","memorial","men","menu","meo","metlife","mg","mh","miami","microsoft","mil","mini","mint","mit","mitsubishi","mk","ml","mlb","mls","mm","mma","mn","mo","mobi","mobile","mobily","moda","moe","moi","mom","monash","money","monster","montblanc","mopar","mormon","mortgage","moscow","moto","motorcycles","mov","movie","movistar","mp","mq","mr","ms","msd","mt","mtn","mtpc","mtr","mu","museum","mutual","mv","mw","mx","my","mz","na","nab","nadex","nagoya","name","nationwide","natura","navy","nba","nc","ne","nec","netbank","netflix","network","neustar","new","newholland","news","next","nextdirect","nexus","nf","nfl","ng","ngo","nhk","ni","nico","nike","nikon","ninja","nissan","nissay","nl","no","nokia","northwesternmutual","norton","now","nowruz","nowtv","np","nr","nra","nrw","ntt","nu","nyc","nz","obi","observer","off","office","okinawa","olayan","olayangroup","oldnavy","ollo","om","omega","one","ong","onl","online","onyourside","ooo","open","oracle","orange","organic","orientexpress","origins","osaka","otsuka","ott","ovh","pa","page","pamperedchef","panasonic","panerai","paris","pars","partners","parts","party","passagens","pay","pccw","pe","pet","pf","pfizer","pg","ph","pharmacy","philips","phone","photo","photography","photos","physio","piaget","pics","pictet","pictures","pid","pin","ping","pink","pioneer","pizza","pk","pl","place","play","playstation","plumbing","plus","pm","pn","pnc","pohl","poker","politie","porn","post","pr","pramerica","praxi","press","prime","pro","prod","productions","prof","progressive","promo","properties","property","protection","pru","prudential","ps","pt","pub","pw","pwc","py","qa","qpon","quebec","quest","qvc","racing","radio","raid","re","read","realestate","realtor","realty","recipes","red","redstone","redumbrella","rehab","reise","reisen","reit","reliance","ren","rent","rentals","repair","report","republican","rest","restaurant","review","reviews","rexroth","rich","richardli","ricoh","rightathome","ril","rio","rip","rmit","ro","rocher","rocks","rodeo","rogers","room","rs","rsvp","ru","ruhr","run","rw","rwe","ryukyu","sa","saarland","safe","safety","sakura","sale","salon","samsclub","samsung","sandvik","sandvikcoromant","sanofi","sap","sapo","sarl","sas","save","saxo","sb","sbi","sbs","sc","sca","scb","schaeffler","schmidt","scholarships","school","schule","schwarz","science","scjohnson","scor","scot","sd","se","seat","secure","security","seek","select","sener","services","ses","seven","sew","sex","sexy","sfr","sg","sh","shangrila","sharp","shaw","shell","shia","shiksha","shoes","shop","shopping","shouji","show","showtime","shriram","si","silk","sina","singles","site","sj","sk","ski","skin","sky","skype","sl","sling","sm","smart","smile","sn","sncf","so","soccer","social","softbank","software","sohu","solar","solutions","song","sony","soy","space","spiegel","spot","spreadbetting","sr","srl","srt","st","stada","staples","star","starhub","statebank","statefarm","statoil","stc","stcgroup","stockholm","storage","store","stream","studio","study","style","su","sucks","supplies","supply","support","surf","surgery","suzuki","sv","swatch","swiftcover","swiss","sx","sy","sydney","symantec","systems","sz","tab","taipei","talk","taobao","target","tatamotors","tatar","tattoo","tax","taxi","tc","tci","td","tdk","team","tech","technology","tel","telecity","telefonica","temasek","tennis","teva","tf","tg","th","thd","theater","theatre","tiaa","tickets","tienda","tiffany","tips","tires","tirol","tj","tjmaxx","tjx","tk","tkmaxx","tl","tm","tmall","tn","to","today","tokyo","tools","top","toray","toshiba","total","tours","town","toyota","toys","tr","trade","trading","training","travel","travelchannel","travelers","travelersinsurance","trust","trv","tt","tube","tui","tunes","tushu","tv","tvs","tw","tz","ua","ubank","ubs","uconnect","ug","unicom","university","uno","uol","ups","us","uy","uz","va","vacations","vana","vanguard","vc","ve","vegas","ventures","verisign","versicherung","vet","vg","vi","viajes","video","vig","viking","villas","vin","vip","virgin","visa","vision","vista","vistaprint","viva","vivo","vlaanderen","vn","vodka","volkswagen","volvo","vote","voting","voto","voyage","vu","vuelos","wales","walmart","walter","wang","wanggou","warman","watch","watches","weather","weatherchannel","webcam","weber","website","wed","wedding","weibo","weir","wf","whoswho","wien","wiki","williamhill","win","windows","wine","winners","wme","wolterskluwer","woodside","work","works","world","wow","ws","wtc","wtf","xbox","xerox","xfinity","xihuan","xin","xn--11b4c3d","xn--1ck2e1b","xn--1qqw23a","xn--30rr7y","xn--3bst00m","xn--3ds443g","xn--3e0b707e","xn--3oq18vl8pn36a","xn--3pxu8k","xn--42c2d9a","xn--45brj9c","xn--45q11c","xn--4gbrim","xn--54b7fta0cc","xn--55qw42g","xn--55qx5d","xn--5su34j936bgsg","xn--5tzm5g","xn--6frz82g","xn--6qq986b3xl","xn--80adxhks","xn--80ao21a","xn--80aqecdr1a","xn--80asehdb","xn--80aswg","xn--8y0a063a","xn--90a3ac","xn--90ae","xn--90ais","xn--9dbq2a","xn--9et52u","xn--9krt00a","xn--b4w605ferd","xn--bck1b9a5dre4c","xn--c1avg","xn--c2br7g","xn--cck2b3b","xn--cg4bki","xn--clchc0ea0b2g2a9gcd","xn--czr694b","xn--czrs0t","xn--czru2d","xn--d1acj3b","xn--d1alf","xn--e1a4c","xn--eckvdtc9d","xn--efvy88h","xn--estv75g","xn--fct429k","xn--fhbei","xn--fiq228c5hs","xn--fiq64b","xn--fiqs8s","xn--fiqz9s","xn--fjq720a","xn--flw351e","xn--fpcrj9c3d","xn--fzc2c9e2c","xn--fzys8d69uvgm","xn--g2xx48c","xn--gckr3f0f","xn--gecrj9c","xn--gk3at1e","xn--h2brj9c","xn--hxt814e","xn--i1b6b1a6a2e","xn--imr513n","xn--io0a7i","xn--j1aef","xn--j1amh","xn--j6w193g","xn--jlq61u9w7b","xn--jvr189m","xn--kcrx77d1x4a","xn--kprw13d","xn--kpry57d","xn--kpu716f","xn--kput3i","xn--l1acc","xn--lgbbat1ad8j","xn--mgb9awbf","xn--mgba3a3ejt","xn--mgba3a4f16a","xn--mgba7c0bbn0a","xn--mgbaam7a8h","xn--mgbab2bd","xn--mgbai9azgqp6j","xn--mgbayh7gpa","xn--mgbb9fbpob","xn--mgbbh1a71e","xn--mgbc0a9azcg","xn--mgbca7dzdo","xn--mgberp4a5d4ar","xn--mgbi4ecexp","xn--mgbpl2fh","xn--mgbt3dhd","xn--mgbtx2b","xn--mgbx4cd0ab","xn--mix891f","xn--mk1bu44c","xn--mxtq1m","xn--ngbc5azd","xn--ngbe9e0a","xn--node","xn--nqv7f","xn--nqv7fs00ema","xn--nyqy26a","xn--o3cw4h","xn--ogbpf8fl","xn--p1acf","xn--p1ai","xn--pbt977c","xn--pgbs0dh","xn--pssy2u","xn--q9jyb4c","xn--qcka1pmc","xn--qxam","xn--rhqv96g","xn--rovu88b","xn--s9brj9c","xn--ses554g","xn--t60b56a","xn--tckwe","xn--tiq49xqyj","xn--unup4y","xn--vermgensberater-ctb","xn--vermgensberatung-pwb","xn--vhquv","xn--vuq861b","xn--w4r85el8fhu5dnra","xn--w4rs40l","xn--wgbh1c","xn--wgbl6a","xn--xhq521b","xn--xkc2al3hye2a","xn--xkc2dl3a5ee0h","xn--y9a3aq","xn--yfro4i67o","xn--ygbi2ammx","xn--zfr164b","xperia","xxx","xyz","yachts","yahoo","yamaxun","yandex","ye","yodobashi","yoga","yokohama","you","youtube","yt","yun","za","zappos","zara","zero","zip","zippo","zm","zone","zuerich","zw"],a.htmlAttrs=["src=","data=","href=","cite=","formaction=","icon=","manifest=","poster=","codebase=","background=","profile=","usemap="]}),t=e(function(e,a){function t(e){var a=e.match(o);if(null===a)return!1;for(var t=r.length-1;t>=0;t--)if(r[t].test(e))return!1;var i=a[2];return!!i&&-1!==n.tlds.indexOf(i)}Object.defineProperty(a,"__esModule",{value:!0});var o=/^[a-z0-9!#$%&'*+\-\/=?^_`{|}~.]+@([a-z0-9%\-]+\.){1,}([a-z0-9\-]+)?$/i,r=[/^[!#$%&'*+\-\/=?^_`{|}~.]/,/[.]{2,}[a-z0-9!#$%&'*+\-\/=?^_`{|}~.]+@/i,/\.@/];a.default=t}),o=e(function(e,n){function t(e){if(!o.test(e))return!1;var n=e.split("."),t=Number(n[0]);if(isNaN(t)||t>255||t<0)return!1;var r=Number(n[1]);if(isNaN(r)||r>255||r<0)return!1;var i=Number(n[2]);if(isNaN(i)||i>255||i<0)return!1;var s=Number((n[3].match(/^\d+/)||[])[0]);if(isNaN(s)||s>255||s<0)return!1;var c=(n[3].match(/(^\d+)(:)(\d+)/)||[])[3];return!(c&&!a.isPort(c))}Object.defineProperty(n,"__esModule",{value:!0});var o=/^(\d{1,3}\.){3}\d{1,3}(:\d{1,5})?(\/([a-z0-9\-._~:\/\?#\[\]@!$&'\(\)\*\+,;=%]+)?)?$/i;n.default=t}),r=e(function(e,t){function o(e){var t=e.match(r);return null!==t&&("string"==typeof t[3]&&(-1!==n.tlds.indexOf(t[3].toLowerCase())&&!(t[5]&&!a.isPort(t[5]))))}Object.defineProperty(t,"__esModule",{value:!0});var r=/^(https?:\/\/|ftps?:\/\/)?([a-z0-9%\-]+\.){1,}([a-z0-9\-]+)?(:(\d{1,5}))?(\/([a-z0-9\-._~:\/\?#\[\]@!$&'\(\)\*\+,;=%]+)?)?$/i;t.default=o}),i=e(function(e,a){function n(e,a,t){return e.forEach(function(o,r){!(o.indexOf(".")>-1)||e[r-1]===a&&e[r+1]===t||e[r+1]!==a&&e[r+1]!==t||(e[r]=e[r]+e[r+1],"string"==typeof e[r+2]&&(e[r]=e[r]+e[r+2]),"string"==typeof e[r+3]&&(e[r]=e[r]+e[r+3]),"string"==typeof e[r+4]&&(e[r]=e[r]+e[r+4]),e.splice(r+1,4),n(e,a,t))}),e}function t(e){return e=n(e,"(",")"),e=n(e,"[","]"),e=n(e,'"','"'),e=n(e,"'","'")}Object.defineProperty(a,"__esModule",{value:!0}),a.fixSeparators=n,a.default=t}),s=e(function(e,a){function n(e){var a=e.replace(/([\s\(\)\[\]<>"'])/g,"\0$1\0").replace(/([?;:,.!]+)(?=(\0|$|\s))/g,"\0$1\0").split("\0");return i.default(a)}function t(e){return e.join("")}Object.defineProperty(a,"__esModule",{value:!0}),a.separate=n,a.deSeparate=t}),c=e(function(e,a){function n(e){return e=e.toLowerCase(),0===e.indexOf("http://")?"http://":0===e.indexOf("https://")?"https://":0===e.indexOf("ftp://")?"ftp://":0===e.indexOf("ftps://")?"ftps://":0===e.indexOf("file:///")?"file:///":0===e.indexOf("mailto:")&&"mailto:"}Object.defineProperty(a,"__esModule",{value:!0}),a.default=n}),l=e(function(e,a){function i(e,a){return e.map(function(i,s){var l=encodeURI(i);if(l.indexOf(".")<1&&!c.default(l))return i;var u=null,d=c.default(l)||"";return d&&(l=l.substr(d.length)),a.files&&"file:///"===d&&l.split(/\/|\\/).length-1&&(u={reason:"file",protocol:d,raw:i,encoded:l}),!u&&a.urls&&r.default(l)&&(u={reason:"url",protocol:d||("function"==typeof a.defaultProtocol?a.defaultProtocol(i):a.defaultProtocol),raw:i,encoded:l}),!u&&a.emails&&t.default(l)&&(u={reason:"email",protocol:"mailto:",raw:i,encoded:l}),!u&&a.ips&&o.default(l)&&(u={reason:"ip",protocol:d||("function"==typeof a.defaultProtocol?a.defaultProtocol(i):a.defaultProtocol),raw:i,encoded:l}),u&&("'"!==e[s-1]&&'"'!==e[s-1]||!~n.htmlAttrs.indexOf(e[s-2]))?u:i})}Object.defineProperty(a,"__esModule",{value:!0}),a.default=i}),u=e(function(e,a){function n(e,a){var n=o.separate(e),r=l.default(n,a);if(a.exclude)for(var i=0;ia.truncate&&(t=t.substring(0,a.truncate)+"..."),"object"==typeof a.truncate&&t.length>a.truncate[0]+a.truncate[1]&&(t=t.substr(0,a.truncate[0])+"..."+t.substr(t.length-a.truncate[1])),void 0===a.attributes&&(a.attributes=[]),'"+t+""}Object.defineProperty(a,"__esModule",{value:!0});var o=s;a.default=n}),d=e(function(e,n){Object.defineProperty(n,"__esModule",{value:!0});var i=function(e,n){return n=a.defaultOptions(n),u.default(e,n)};i.validate={ip:o.default,url:function(e){var a=c.default(e)||"";return e=e.substr(a.length),e=encodeURI(e),r.default(e)},email:t.default},n.default=i});return function(e){return e&&e.__esModule?e.default:e}(d)});export default anchorme; 2 | -------------------------------------------------------------------------------- /src/css/style.css: -------------------------------------------------------------------------------- 1 | @import url(../css/pixel_font.css); 2 | @import url(../css/styled_scrollbar.css); 3 | @import url(../css/context.css); 4 | 5 | html, 6 | body { 7 | font: 16px pixel-op, sans-serif; 8 | width: 100%; 9 | height: 100%; 10 | margin: 0; 11 | touch-action: none; 12 | position: fixed; 13 | } 14 | 15 | body { 16 | background-color: #d1d1d1; 17 | background-image: url(../img/unloaded.png); 18 | background-size: 16px; 19 | } 20 | 21 | html { 22 | user-select: none; 23 | } 24 | 25 | hr { 26 | border-color: rgba(0, 0, 0, 0.2); 27 | } 28 | 29 | .hide { 30 | display: none !important; 31 | } 32 | 33 | .selectable { 34 | user-select: text; 35 | } 36 | 37 | .centered { 38 | position: absolute; 39 | padding-top: 1px; 40 | /* fix captcha window not being pixel perfect */ 41 | top: 50%; 42 | left: 50%; 43 | transform: translate(-50%, -50%); 44 | } 45 | 46 | .centeredChilds { 47 | display: flex; 48 | justify-content: center; 49 | align-items: center; 50 | } 51 | 52 | /* css for improved tooltips */ 53 | #tooltip { 54 | position: absolute; 55 | z-index: 100; 56 | border: 5px #7e635c solid; 57 | border-image: url(../img/small_border.png) 5 repeat; 58 | border-image-outset: 1px; 59 | /* background-color: #5c0c91; */ 60 | box-shadow: 0px 0px 5px #000; 61 | background-color: #7e635c; 62 | color: #fff; 63 | text-shadow: -1px 0 #000, 0 1px #000, 1px 0 #000, 0 -1px #000; 64 | pointer-events: none; 65 | } 66 | 67 | /* .tooltip { 68 | pointer-events: none; 69 | position: absolute; 70 | top: 0; 71 | left: 0; 72 | opacity: 0.9; 73 | } */ 74 | 75 | .owopdropdown { 76 | pointer-events: none !important; 77 | padding: 0 !important; 78 | padding-top: 1px !important; 79 | top: 0; 80 | left: 50%; 81 | transform: translateX(-50%); 82 | border: none !important; 83 | background-color: rgba(0, 0, 0, 0) !important; 84 | transition: transform 0.5s ease-out; 85 | } 86 | 87 | button.winframe:active { 88 | border-image: inherit; 89 | } 90 | 91 | .whitetext, 92 | #xy-display, 93 | #chat, 94 | #dev-chat, 95 | #playercount-display, 96 | #topright-displays, 97 | #topleft-displays>*, 98 | .generic-display { 99 | color: #FFF; 100 | font: 16px pixel-op, sans-serif; 101 | text-shadow: -1px 0 #000, 0 1px #000, 1px 0 #000, 0 -1px #000; 102 | } 103 | 104 | img, 105 | #tool-select { 106 | image-rendering: pixelated; 107 | } 108 | 109 | #load-scr { 110 | position: absolute; 111 | height: 100%; 112 | width: 100%; 113 | text-align: center; 114 | font: 0/0 a; 115 | pointer-events: none; 116 | transition: transform 1.5s cubic-bezier(0.68, -0.55, 0.27, 1.55); 117 | background-image: url(../img/unloaded.png); 118 | box-shadow: 0 0 5px #000; 119 | } 120 | 121 | #load-scr:before { 122 | content: ' '; 123 | display: inline-block; 124 | vertical-align: middle; 125 | height: 100%; 126 | } 127 | 128 | #load-ul { 129 | display: inline-block; 130 | vertical-align: middle; 131 | list-style-type: none; 132 | padding: 0; 133 | margin: 0; 134 | max-height: 100vh; 135 | max-width: 60%; 136 | min-width: 224px; 137 | pointer-events: initial; 138 | transition: transform 1s; 139 | } 140 | 141 | .uk-notice.framed { 142 | max-height: 70vh; 143 | overflow-y: auto; 144 | } 145 | 146 | #noscript-msg, 147 | #status { 148 | font: 16px pixel-op; 149 | } 150 | 151 | #status-msg { 152 | vertical-align: super; 153 | text-shadow: -1px 0 #000, 0 1px #000, 1px 0 #000, 0 -1px #000, 0 0 2px #000; 154 | } 155 | 156 | #spinner { 157 | margin-right: 8px; 158 | } 159 | 160 | #viewport, 161 | #windows, 162 | #animations { 163 | position: absolute; 164 | } 165 | 166 | #windows { 167 | pointer-events: none; 168 | width: 100%; 169 | height: 100%; 170 | z-index: 6; 171 | } 172 | 173 | #windows>div, 174 | .winframe { 175 | /* Frame */ 176 | position: absolute; 177 | pointer-events: initial; 178 | background-color: #aba389; 179 | border: 11px #aba389 solid; 180 | border-width: 11px; 181 | border-image: url(../img/window_out.png) 11 repeat; 182 | border-image-outset: 1px; 183 | box-shadow: 0px 0px 5px #000; 184 | } 185 | 186 | #windows>div>span { 187 | /* Title */ 188 | display: block; 189 | pointer-events: none; 190 | margin-top: -7px; 191 | text-shadow: 1px 1px #4d313b; 192 | color: #7e635c; 193 | margin-bottom: 3px; 194 | min-width: 100%; 195 | text-align: center; 196 | } 197 | 198 | .windowCloseButton { 199 | /* Close button */ 200 | position: absolute; 201 | right: 0; 202 | top: -2px; 203 | width: 9px; 204 | height: 9px; 205 | padding: 0; 206 | background-image: url(../img/gui.png); 207 | background-position: -48px -32px; 208 | background-color: #ff7979; 209 | border: none; 210 | } 211 | 212 | button.windowCloseButton:active { 213 | background-image: url(../img/gui.png); 214 | background-position: -48px -41px; 215 | } 216 | 217 | .wincontainer { 218 | /* Item container of windows */ 219 | overflow: auto; 220 | min-width: 100%; 221 | /* width: 0; Older browsers fix */ 222 | height: 100%; 223 | margin: 0 -5px -5px -5px; 224 | background-color: #7e635c; 225 | border: 5px #7e635c solid; 226 | border-width: 5px; 227 | border-image: url(../img/window_in.png) 5 repeat; 228 | } 229 | 230 | #windows>div>div input { 231 | border: 6px #7e635c solid; 232 | border-image: url(../img/small_border.png) 6 repeat; 233 | border-image-outset: 1px; 234 | } 235 | 236 | #windows>div>div input:focus { 237 | outline: none; 238 | } 239 | 240 | #windows>div>div>* { 241 | box-sizing: border-box; 242 | } 243 | 244 | button { 245 | border: 6px #aba389 outset; 246 | border-image: url(../img/button.png) 6 repeat; 247 | background-color: #aba389; 248 | transition: filter 0.125s; 249 | } 250 | 251 | button:hover { 252 | filter: brightness(110%); 253 | transition: filter 0.125s; 254 | } 255 | 256 | button:active, button.pushed { 257 | border-style: inset; 258 | border-image: url(../img/button_pressed.png) 6 repeat; 259 | filter: brightness(90%); 260 | transition: none; 261 | } 262 | 263 | button:focus { 264 | outline: none; 265 | } 266 | 267 | #clusters>canvas { 268 | position: absolute; 269 | background-image: url(../img/unloaded.png); 270 | background-size: 8px; 271 | } 272 | 273 | #animations { 274 | top: 0; 275 | left: 0; 276 | } 277 | 278 | #palette { 279 | position: absolute; 280 | } 281 | 282 | #xy-display, 283 | .generic-display { 284 | padding-left: 2px; 285 | } 286 | 287 | #playercount-display { 288 | padding-right: 2px; 289 | } 290 | 291 | #palette, 292 | #topright-displays>*, 293 | #topleft-displays>* { 294 | pointer-events: none; 295 | transform: translateY(-100%); 296 | transition: transform 0.75s; 297 | } 298 | 299 | #topleft-displays>*, 300 | #topright-displays>* { 301 | pointer-events: none; 302 | transition: transform 0.75s; 303 | } 304 | 305 | #notice-display { 306 | pointer-events: all; 307 | cursor: pointer; 308 | z-index: 5; 309 | } 310 | 311 | #notice-display>* { 312 | pointer-events: none; 313 | } 314 | 315 | #topright-displays, 316 | #topleft-displays { 317 | position: absolute; 318 | pointer-events: none; 319 | } 320 | 321 | /* #xy-display, #palette { 322 | position: absolute; 323 | } 324 | #xy-display, #playercount-display, #palette, #topright-displays > * { 325 | pointer-events: none; 326 | transform: translateY(-100%); 327 | transition: transform 0.75s; 328 | } 329 | #topright-displays { 330 | position: absolute; 331 | pointer-events: none; 332 | } 333 | #xy-display { 334 | padding-left: 2px; 335 | left: -4px; 336 | top: -4px; 337 | } */ 338 | 339 | #topright-displays { 340 | right: -4px; 341 | top: -4px; 342 | } 343 | 344 | #topleft-displays { 345 | left: -4px; 346 | top: -4px; 347 | } 348 | 349 | #topright-displays>*, 350 | #topleft-displays>* { 351 | display: inline-block; 352 | min-height: 8px; 353 | } 354 | 355 | #topright-displays:not(.hideui) #dinfo-display[data-pm]:not([data-pm="1"]) { 356 | transform: initial; 357 | } 358 | 359 | #dinfo-display { 360 | position: relative; 361 | } 362 | 363 | #dinfo-display::before { 364 | content: '' attr(data-pm) 'x boost for ' attr(data-tmo) '!'; 365 | border-right: 1px dashed #00000077; 366 | margin-right: 1px; 367 | } 368 | 369 | #dinfo-hlp { 370 | pointer-events: all; 371 | background-color: #00000044; 372 | border-radius: 100%; 373 | padding: 0 4px; 374 | cursor: help; 375 | } 376 | 377 | #dinfo-hlp-box { 378 | display: none; 379 | position: absolute; 380 | top: 150%; 381 | left: -5px; 382 | width: 150%; 383 | box-sizing: border-box; 384 | z-index: 100; 385 | } 386 | 387 | #dinfo-hlp:hover~#dinfo-hlp-box { 388 | display: block; 389 | } 390 | 391 | #toole-container { 392 | overflow: hidden; 393 | } 394 | 395 | #playercount-display, 396 | #xy-display, 397 | #palette, 398 | .framed, 399 | #pbucket-display, 400 | #rank-display, 401 | .generic-display { 402 | border: 5px #aba389 solid; 403 | border-image: url(../img/small_border.png) 5 repeat; 404 | border-image-outset: 1px; 405 | background-color: #7e635c; 406 | box-shadow: 0px 0px 5px #000; 407 | } 408 | 409 | .generic-display:active { 410 | border: 5px #aba389 solid; 411 | border-image: url(../img/small_border.png) 5 repeat; 412 | border-image-outset: 1px; 413 | background-color: #7e635c; 414 | box-shadow: 0px 0px 5px #000; 415 | -webkit-filter: brightness(90%); 416 | filter: brightness(90%); 417 | } 418 | 419 | #toole-container>button>div { 420 | /* ugly */ 421 | position: fixed; 422 | margin: -18px -4px; 423 | width: 36px; 424 | height: 36px; 425 | } 426 | 427 | #toole-container>button { 428 | position: relative; 429 | display: inline-block; 430 | width: 40px; 431 | height: 40px; 432 | padding: 0; 433 | } 434 | 435 | #toole-container>button.selected { 436 | background-color: #aaa; 437 | } 438 | 439 | #tool-select>button>div { 440 | position: absolute; 441 | width: 36px; 442 | height: 36px; 443 | margin-left: 50%; 444 | transform: translate(-50%, -50%); 445 | } 446 | 447 | #palette { 448 | right: -5px; 449 | top: 50%; 450 | transform: translateY(-50%) translateX(200%); 451 | width: 45px; 452 | height: 40px; 453 | box-sizing: border-box; 454 | } 455 | 456 | #palette-bg { 457 | position: absolute; 458 | height: 100%; 459 | width: 44px; 460 | top: 0; 461 | right: 0; 462 | background-color: rgba(0, 0, 0, 0.3); 463 | transition: transform 0.75s; 464 | pointer-events: none; 465 | } 466 | 467 | #palette-opts { 468 | display: flex; 469 | flex-direction: column; 470 | justify-content: center; 471 | height: 100%; 472 | position:absolute; 473 | right:50px; 474 | box-sizing: border-box; 475 | pointer-events:all; 476 | } 477 | 478 | #palette-create { 479 | background-image: url(../img/plus.png); 480 | background-repeat: no-repeat; 481 | box-sizing: border-box; 482 | width: 24px; 483 | min-height: 24px; 484 | margin-bottom: 4px; 485 | cursor: pointer; 486 | } 487 | 488 | #palette-load { 489 | background-image: url(../img/load.png); 490 | background-repeat: no-repeat; 491 | box-sizing: border-box; 492 | width: 24px; 493 | min-height: 24px; 494 | cursor: pointer; 495 | } 496 | 497 | #palette-save { 498 | background-image: url(../img/save.png); 499 | background-repeat: no-repeat; 500 | box-sizing: border-box; 501 | width: 24px; 502 | min-height: 24px; 503 | margin-top:4px; 504 | cursor: pointer; 505 | } 506 | 507 | #picker-anchor { 508 | position:absolute; 509 | right:50px; 510 | top:-30px; 511 | } 512 | 513 | #color-picker { 514 | position:absolute; 515 | left:-100%; 516 | } 517 | 518 | #palette-colors { 519 | position: absolute; 520 | left: -1px; 521 | top: -9px; 522 | transition: transform 0.2s ease-out; 523 | } 524 | 525 | #palette-colors>div { 526 | width: 32px; 527 | height: 32px; 528 | margin: 8px 0; 529 | border: 1px solid rgba(0, 0, 0, 0.3); 530 | box-sizing: border-box; 531 | pointer-events: all; 532 | cursor: pointer; 533 | } 534 | 535 | #player-list, #ban-list { 536 | max-height: 300px; 537 | overflow-y: scroll; 538 | } 539 | 540 | #player-list>table, .ban-list-table { 541 | max-width: 90vw; 542 | border-collapse: collapse; 543 | border: 1px solid #000; 544 | color: #fff; 545 | text-shadow: -1px 0 #000, 0 1px #000, 1px 0 #000, 0 -1px #000; 546 | padding: 2px; 547 | } 548 | 549 | #player-list>table>tr:nth-child(odd), .ban-list-table tbody tr:nth-child(even) { 550 | background-color: rgba(0, 0, 0, 0.1); 551 | } 552 | 553 | #player-list>table>tr:nth-child(even), .ban-list-table tbody tr:nth-child(odd) { 554 | background-color: rgba(0, 0, 0, 0.3); 555 | } 556 | 557 | #player-list>table>tr:first-child, .ban-list-table > thead > tr { 558 | text-align: left; 559 | background-color: rgba(0, 0, 0, 0.5); 560 | } 561 | 562 | #player-list td, 563 | #player-list th, 564 | .ban-list-table td, 565 | .ban-list-table th { 566 | padding: 2px 6px; 567 | } 568 | 569 | .ban-list-ctrl { 570 | display: flex; 571 | justify-content: center; 572 | } 573 | 574 | .ban-list-ctrl:first-child { 575 | margin-bottom: 5px; 576 | } 577 | 578 | .ban-list-ctrl:last-child { 579 | margin-top: 5px; 580 | } 581 | 582 | .ban-list-emptyrow > td { 583 | text-align: center; 584 | padding: 5px; 585 | } 586 | 587 | #player-list>table>tr>td:nth-child(1), 588 | .ban-list-table td:nth-child(1) { 589 | border-right: 1px solid rgba(0, 0, 0, 0.5); 590 | } 591 | 592 | #player-list>table>tr>td:nth-child(2), 593 | .ban-list-table td { 594 | border-right: 1px solid rgba(0, 0, 0, 0.3); 595 | } 596 | 597 | .ban-list-table td { 598 | user-select: text; 599 | max-width: 300px; 600 | } 601 | 602 | .ban-list-table button { 603 | user-select: none; 604 | } 605 | 606 | .ban-list-table td:nth-child(3) { 607 | max-width: 30vw; 608 | line-break: anywhere; 609 | } 610 | 611 | #help-button { 612 | position: absolute; 613 | bottom: 0; 614 | left: 0; 615 | padding: 0; 616 | margin: 16px; 617 | transition: transform 0.75s; 618 | } 619 | 620 | #help-button>img { 621 | width: 64px; 622 | display: block; 623 | } 624 | 625 | #help { 626 | position: absolute; 627 | top: 50%; 628 | left: 50%; 629 | transform: translate(-50%, -50%); 630 | width: 80%; 631 | max-width: 800px; 632 | 633 | background-color: #aba389; 634 | border: 11px #aba389 solid; 635 | border-width: 11px; 636 | border-image: url(../img/window_out.png) 11 repeat; 637 | border-image-outset: 1px; 638 | box-shadow: 0px 0px 5px #000; 639 | max-height:96%; 640 | display:flex; 641 | flex-direction:column; 642 | } 643 | 644 | 645 | 646 | #help>.title { 647 | display: block; 648 | pointer-events: none; 649 | margin-top: -7px; 650 | text-shadow: 1px 1px #4d313b; 651 | color: #7e635c; 652 | margin-bottom: 3px; 653 | min-width: 100%; 654 | text-align: center; 655 | } 656 | 657 | #help>.content { 658 | overflow: auto; 659 | flex-grow: 1; 660 | max-height: 100%; 661 | min-width: 100%; 662 | /* width: 0; Older browsers fix */ 663 | height: 100%; 664 | margin: 0 -5px -5px -5px; 665 | background-color: #7e635c; 666 | border: 5px #7e635c solid; 667 | border-width: 5px; 668 | border-image: url(../img/window_in.png) 5 repeat; 669 | } 670 | 671 | #help>.content>.links { 672 | text-align: center; 673 | } 674 | 675 | #help>.content>.links>* { 676 | display: inline-block; 677 | vertical-align: middle; 678 | width: 76px; 679 | } 680 | 681 | #help>.content>.links>* img { 682 | width: 100%; 683 | } 684 | 685 | #help.hidden { 686 | display: none; 687 | } 688 | 689 | #chat { 690 | transform: translateY(100%); 691 | } 692 | 693 | #chat, 694 | #dev-chat { 695 | position: absolute; 696 | right: 0; 697 | bottom: 0; 698 | min-width: 20%; 699 | max-width: 450px; 700 | /* max-height: 40%; // causes problems on old browsers */ 701 | display: flex; 702 | font-family: pixel-op, monospace; 703 | flex-direction: column; 704 | background-color: transparent; 705 | pointer-events: none; 706 | overflow: hidden; 707 | transition: background-color 0.2s, box-shadow 0.2s, transform 0.75s; 708 | animation-fill-mode: forwards; 709 | } 710 | 711 | #dev-chat { 712 | left: 0; 713 | right: initial; 714 | } 715 | 716 | #chat.active, 717 | #dev-chat.active { 718 | background-color: rgba(0, 0, 0, 0.8); 719 | box-shadow: 0px 0px 5px #000; 720 | pointer-events: all; 721 | overflow-y: auto; 722 | } 723 | 724 | @keyframes fade { 725 | from { 726 | opacity: 1; 727 | } 728 | 729 | to { 730 | opacity: 0; 731 | } 732 | } 733 | 734 | #chat-messages>li { 735 | background-color: rgba(0, 0, 0, 0.8); 736 | animation-name: fade; 737 | animation-duration: 3s; 738 | animation-delay: 15s; 739 | animation-fill-mode: forwards; 740 | transition: background-color 0.2s; 741 | white-space: pre-wrap; 742 | } 743 | 744 | #chat-messages>li a:link { 745 | color: #82c9ff; 746 | } 747 | 748 | #chat-messages>li a:visited { 749 | color: #ab80f9; 750 | } 751 | 752 | #chat-messages>li a:hover { 753 | color: #76b0dc; 754 | } 755 | 756 | #chat-messages>li.playerMessage { 757 | color: #999; 758 | } 759 | 760 | #chat-messages>li.userMessage>.nick { 761 | color: #3ab2ff; 762 | } 763 | 764 | #chat-messages>li.modMessage { 765 | color: #86ff41; 766 | } 767 | 768 | #chat-messages>li.adminMessage, 769 | #chat-messages>li.serverError, 770 | #chat-messages>li.serverRaw { 771 | color: #ff4f4f; 772 | } 773 | 774 | #chat-messages>li.discord>.nick { 775 | color: #6cffe7; 776 | } 777 | 778 | #chat-messages>li.serverInfo { 779 | color: #ff41e4; 780 | } 781 | 782 | #chat-messages>li.whisper, 783 | #chat-messages>li>.whisper { 784 | color: #ffb735; 785 | } 786 | 787 | #chat-messages .emote { 788 | max-width: 1.375em; 789 | max-height: 1.375em; 790 | vertical-align: bottom; 791 | image-rendering: auto; 792 | } 793 | 794 | #chat-messages.active>li { 795 | background-color: initial; 796 | animation-duration: 0s; 797 | animation-direction: reverse; 798 | } 799 | 800 | #chat-messages, 801 | #dev-chat-messages { 802 | flex: 1; 803 | margin: 0; 804 | padding: 0; 805 | font-size: 16px; 806 | max-height: 40vh; 807 | word-wrap: break-word; 808 | overflow: inherit; 809 | vertical-align: bottom; 810 | } 811 | 812 | #chat-input { 813 | flex: 0 1 auto; 814 | height: 16px; 815 | color: #FFF; 816 | pointer-events: all; 817 | border: 1px solid #666; 818 | padding: 4px; 819 | background: rgba(0, 0, 0, 0.8); 820 | font-family: pixel-op, sans-serif; 821 | font-size: 16px; 822 | resize: none; 823 | overflow-y: scroll; 824 | display: none; 825 | } 826 | 827 | #chat-input:focus { 828 | outline: none; 829 | } 830 | 831 | #chat-input::placeholder { 832 | color: #BBB; 833 | } 834 | 835 | #captchawdow { 836 | margin: -4px; 837 | } 838 | 839 | .rainbow-container { 840 | position: relative; 841 | display: inline-block; 842 | } 843 | 844 | .rainbow { 845 | background: linear-gradient(to right, #db2a2a, #d16d15, #d4b413, #18fa14, #192abf, #760dd9, #db2a2a); 846 | -webkit-background-clip: text; 847 | background-clip: text; 848 | background-repeat: repeat-x; 849 | color: transparent; 850 | animation: rainbow_animation 6s linear infinite; 851 | background-size: 400% 100%; 852 | text-shadow: none; 853 | position: relative; 854 | z-index: 1; 855 | } 856 | 857 | @keyframes rainbow_animation { 858 | 0% { 859 | background-position: 0 0; 860 | } 861 | 862 | 100% { 863 | background-position: 132% 0; 864 | } 865 | } 866 | 867 | .rainbow-back { 868 | text-shadow: -1px 0 #000, 0 1px #000, 1px 0 #000, 0 -1px #000; 869 | color: #000; 870 | position: absolute; 871 | left: 0; 872 | z-index: 0; 873 | } 874 | 875 | #keybind-settings { 876 | display: flex; 877 | flex-direction: row; 878 | } 879 | 880 | #keybinddiv { 881 | flex-grow: 1; 882 | } 883 | 884 | #keybindopts { 885 | text-align: right; 886 | } 887 | 888 | .color-picker-frame{ 889 | border: 5px #7e635c solid; 890 | border-image: url(../img/small_border.png) 5 repeat; 891 | border-image-outset: 1px; 892 | box-shadow: 0px 0px 5px #000; 893 | padding:5px; 894 | position:absolute; 895 | background-color:#7e635c; 896 | display:flex; 897 | align-items:stretch; 898 | } 899 | 900 | .color-picker-container{ 901 | position:relative; 902 | width:200px; 903 | height:200px; 904 | display:flex; 905 | flex-direction:row; 906 | align-items: stretch; 907 | gap:5px; 908 | } 909 | 910 | .color-picker-canvas{ 911 | width:100%; 912 | height:100%; 913 | } 914 | 915 | .color-picker-slider{ 916 | width: 10px; 917 | height:100%; 918 | border-radius: 24px; 919 | flex-shrink: 0; 920 | } 921 | 922 | .draggableHandle{ 923 | width:6px; 924 | height:6px; 925 | border:2px solid #333; 926 | border-radius: 50%; 927 | position:absolute; 928 | cursor:grab; 929 | z-index:5; 930 | } 931 | 932 | .picker-dragging{ 933 | cursor:grabbing; 934 | } 935 | 936 | .palette-load{ 937 | display:flex; 938 | flex-direction:column; 939 | align-items:stretch; 940 | } 941 | .palette-load-top{ 942 | flex: 1; 943 | } 944 | .palette-load-bottom{ 945 | display:flex; 946 | flex-direction:column; 947 | } 948 | .palette-load-palette-container{ 949 | display:flex; 950 | flex-direction: column; 951 | max-width:400px; 952 | overflow-y:scroll; 953 | align-items: stretch; 954 | } 955 | .palette-button-row{ 956 | display:flex; 957 | flex-direction:row; 958 | gap:2px; 959 | } 960 | .palette-load-selection-container{ 961 | display:flex; 962 | flex-direction:column; 963 | } 964 | .palette-load-preview{ 965 | 966 | } 967 | .palette-load-button-contianer{ 968 | display:flex; 969 | flex-direction:row; 970 | } 971 | -------------------------------------------------------------------------------- /src/js/canvas_renderer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { protocol, EVENTS as e, options } from './conf.js'; 3 | import { eventSys, PublicAPI } from './global.js'; 4 | import { elements, misc } from './main.js'; 5 | import { player } from './local_player.js'; 6 | import { activeFx } from './Fx.js'; 7 | import { getTime } from './util/misc.js'; 8 | import { colorUtils as color } from './util/color.js'; 9 | import { Lerp } from './util/Lerp.js'; 10 | import { tools } from './tools.js'; 11 | 12 | export { centerCameraTo, moveCameraBy, moveCameraTo, isVisible }; 13 | 14 | /* oh boy, i'm going to get shit for making this private, aren't i? */ 15 | const cameraValues = { 16 | x: 0, 17 | y: 0, 18 | zoom: -1/*, 19 | lerpZoom: new Lerp(options.defaultZoom, options.defaultZoom, 200)*/ 20 | }; 21 | 22 | export const camera = { 23 | get x() { return cameraValues.x; }, 24 | get y() { return cameraValues.y; }, 25 | get zoom() { return cameraValues.zoom; }, 26 | /*get lerpZoom() { return cameraValues.lerpZoom.val; },*/ 27 | set zoom(z) { 28 | z = Math.min(options.zoomLimitMax, Math.max(options.zoomLimitMin, z)); 29 | if (z !== cameraValues.zoom) { 30 | var center = getCenterPixel(); 31 | cameraValues.zoom = z; 32 | centerCameraTo(center[0], center[1]); 33 | eventSys.emit(e.camera.zoom, z); 34 | } 35 | }, 36 | isVisible: isVisible, 37 | 38 | centerCameraTo: centerCameraTo, 39 | moveCameraBy: moveCameraBy, 40 | moveCameraTo: moveCameraTo, 41 | alignCamera: alignCamera, 42 | }; 43 | 44 | const rendererValues = { 45 | updateRequired: 3, 46 | animContext: null, 47 | gridShown: true, 48 | gridPattern: null, /* Rendered each time the zoom changes */ 49 | unloadedPattern: null, 50 | worldBackground: null, 51 | minGridZoom: options.minGridZoom, 52 | updatedClusters: [], /* Clusters to render in the next frame */ 53 | clusters: {}, 54 | visibleClusters: [], 55 | currentFontSize: -1 56 | }; 57 | 58 | PublicAPI.rendererValues = rendererValues; 59 | PublicAPI.Lerp = Lerp; 60 | 61 | export const renderer = { 62 | rendertype: { 63 | ALL: 0b11, 64 | FX: 0b01, 65 | WORLD: 0b10 66 | }, 67 | patterns: { 68 | get unloaded() { return rendererValues.unloadedPattern; } 69 | }, 70 | render: requestRender, 71 | showGrid: setGridVisibility, 72 | get gridShown() { return rendererValues.gridShown; }, 73 | updateCamera: onCameraMove, 74 | unloadFarClusters: unloadFarClusters, 75 | 76 | drawText: drawText, 77 | renderPlayer: renderPlayer, 78 | renderPlayerId: renderPlayerId, 79 | }; 80 | 81 | PublicAPI.camera = camera; 82 | PublicAPI.renderer = renderer; 83 | 84 | class BufView { 85 | constructor(u32data, x, y, w, h, realw) { 86 | this.data = u32data; 87 | if (options.chunkBugWorkaround) { 88 | this.changes = []; 89 | } 90 | this.offx = x; 91 | this.offy = y; 92 | this.realwidth = realw; 93 | this.width = w; 94 | this.height = h; 95 | } 96 | 97 | get(x, y) { 98 | return this.data[(this.offx + x) + (this.offy + y) * this.realwidth]; 99 | } 100 | 101 | set(x, y, data) { 102 | this.data[(this.offx + x) + (this.offy + y) * this.realwidth] = data; 103 | if (options.chunkBugWorkaround) { 104 | this.changes.push([0, x, y, data]); 105 | } 106 | } 107 | 108 | fill(data) { 109 | for (var i = 0; i < this.height; i++) { 110 | for (var j = 0; j < this.width; j++) { 111 | this.data[(this.offx + j) + (this.offy + i) * this.realwidth] = data; 112 | } 113 | } 114 | if (options.chunkBugWorkaround) { 115 | this.changes.push([1, 0, 0, data]); 116 | } 117 | } 118 | 119 | fillFromBuf(u32buf) { 120 | for (var i = 0; i < this.height; i++) { 121 | for (var j = 0; j < this.width; j++) { 122 | this.data[(this.offx + j) + (this.offy + i) * this.realwidth] = u32buf[j + i * this.width]; 123 | if (options.chunkBugWorkaround) { 124 | /* Terrible */ 125 | this.changes.push([0, j, i, u32buf[j + i * this.width]]); 126 | } 127 | } 128 | } 129 | } 130 | } 131 | 132 | class ChunkCluster { 133 | constructor(x, y) { 134 | this.removed = false; 135 | this.toUpdate = false; 136 | this.shown = false; /* is in document? */ 137 | this.x = x; 138 | this.y = y; 139 | this.canvas = document.createElement("canvas"); 140 | this.canvas.width = protocol.chunkSize * protocol.clusterChunkAmount; 141 | this.canvas.height = protocol.chunkSize * protocol.clusterChunkAmount; 142 | this.ctx = this.canvas.getContext("2d"); 143 | this.data = this.ctx.createImageData(this.canvas.width, this.canvas.height); 144 | this.u32data = new Uint32Array(this.data.data.buffer); 145 | this.chunks = []; 146 | if (options.chunkBugWorkaround) { 147 | this.currentColor = 0; 148 | } 149 | } 150 | 151 | render() { 152 | this.toUpdate = false; 153 | for (var i = this.chunks.length; i--;) { 154 | var c = this.chunks[i]; 155 | if (c.needsRedraw) { 156 | c.needsRedraw = false; 157 | if (options.chunkBugWorkaround) { 158 | var arr = c.view.changes; 159 | var s = protocol.chunkSize; 160 | for (var j = 0; j < arr.length; j++) { 161 | var current = arr[j]; 162 | if (this.currentColor !== current[3]) { 163 | this.currentColor = current[3]; 164 | this.ctx.fillStyle = color.toHTML(current[3]); 165 | } 166 | switch (current[0]) { 167 | case 0: 168 | this.ctx.fillRect(c.view.offx + current[1], c.view.offy + current[2], 1, 1); 169 | break; 170 | case 1: 171 | this.ctx.fillRect(c.view.offx, c.view.offy, s, s); 172 | break; 173 | } 174 | } 175 | c.view.changes = []; 176 | } else { 177 | this.ctx.putImageData(this.data, 0, 0, 178 | c.view.offx, c.view.offy, c.view.width, c.view.height); 179 | } 180 | } 181 | } 182 | } 183 | 184 | remove() { 185 | this.removed = true; 186 | if (this.shown) { 187 | var visiblecl = rendererValues.visibleClusters; 188 | visiblecl.splice(visiblecl.indexOf(this), 1); 189 | this.shown = false; 190 | } 191 | this.canvas.width = 0; 192 | this.u32data = this.data = null; 193 | delete rendererValues.clusters[`${this.x},${this.y}`]; 194 | for (var i = 0; i < this.chunks.length; i++) { 195 | this.chunks[i].view = null; 196 | this.chunks[i].remove(); 197 | } 198 | this.chunks = []; 199 | } 200 | 201 | addChunk(chunk) { 202 | /* WARNING: Should absMod if not power of two */ 203 | var x = chunk.x & (protocol.clusterChunkAmount - 1); 204 | var y = chunk.y & (protocol.clusterChunkAmount - 1); 205 | var s = protocol.chunkSize; 206 | var view = new BufView(this.u32data, x * s, y * s, s, s, protocol.clusterChunkAmount * s); 207 | if (chunk.tmpChunkBuf) { 208 | view.fillFromBuf(chunk.tmpChunkBuf); 209 | chunk.tmpChunkBuf = null; 210 | } 211 | chunk.view = view; 212 | this.chunks.push(chunk); 213 | chunk.needsRedraw = true; 214 | } 215 | 216 | delChunk(chunk) { 217 | chunk.view = null; 218 | /* There is no real need to clearRect the chunk area */ 219 | var i = this.chunks.indexOf(chunk); 220 | if (i !== -1) { 221 | this.chunks.splice(i, 1); 222 | } 223 | if (!this.chunks.length) { 224 | this.remove(); 225 | } 226 | } 227 | } 228 | 229 | /* Draws white text with a black border */ 230 | export function drawText(ctx, str, x, y, centered){ 231 | ctx.strokeStyle = "#000000", 232 | ctx.fillStyle = "#FFFFFF", 233 | ctx.lineWidth = 2.5, 234 | ctx.globalAlpha = 0.5; 235 | if(centered) { 236 | x -= ctx.measureText(str).width >> 1; 237 | } 238 | ctx.strokeText(str, x, y); 239 | ctx.globalAlpha = 1; 240 | ctx.fillText(str, x, y); 241 | } 242 | 243 | function isVisible(x, y, w, h) { 244 | if(document.visibilityState === "hidden") return; 245 | var cx = camera.x; 246 | var cy = camera.y; 247 | var czoom = camera.zoom; 248 | var cw = window.innerWidth; 249 | var ch = window.innerHeight; 250 | return x + w > cx && y + h > cy && 251 | x <= cx + cw / czoom && y <= cy + ch / czoom; 252 | } 253 | 254 | export function unloadFarClusters() { /* Slow? */ 255 | var camx = camera.x; 256 | var camy = camera.y; 257 | var zoom = camera.zoom; 258 | var camw = window.innerWidth / zoom | 0; 259 | var camh = window.innerHeight / zoom | 0; 260 | var ctrx = camx + camw / 2; 261 | var ctry = camy + camh / 2; 262 | var s = protocol.clusterChunkAmount * protocol.chunkSize; 263 | for (var c in rendererValues.clusters) { 264 | c = rendererValues.clusters[c]; 265 | if (!isVisible(c.x * s, c.y * s, s, s)) { 266 | var dx = Math.abs(ctrx / s - c.x) | 0; 267 | var dy = Math.abs(ctry / s - c.y) | 0; 268 | var dist = dx + dy; /* no sqrt please */ 269 | //console.log(dist); 270 | if (dist > options.unloadDistance) { 271 | c.remove(); 272 | } 273 | } 274 | } 275 | } 276 | 277 | 278 | function render(type) { 279 | var time = getTime(true); 280 | var camx = camera.x; 281 | var camy = camera.y; 282 | var zoom = camera.zoom; 283 | var needsRender = 0; /* If an animation didn't finish, render again */ 284 | 285 | if (type & renderer.rendertype.WORLD) { 286 | var uClusters = rendererValues.updatedClusters; 287 | for (var i = 0; i < uClusters.length; i++) { 288 | var c = uClusters[i]; 289 | c.render(); 290 | } 291 | rendererValues.updatedClusters = []; 292 | } 293 | 294 | if (type & renderer.rendertype.FX && misc.world !== null) { 295 | var ctx = rendererValues.animContext; 296 | var visible = rendererValues.visibleClusters; 297 | var clusterCanvasSize = protocol.chunkSize * protocol.clusterChunkAmount; 298 | var cwidth = window.innerWidth; 299 | var cheight = window.innerHeight; 300 | var background = rendererValues.worldBackground; 301 | var allChunksLoaded = misc.world.allChunksLoaded(); 302 | 303 | var bggx = -(camx * zoom) % (16 * zoom); 304 | var bggy = -(camy * zoom) % (16 * zoom); 305 | 306 | if (!allChunksLoaded) { 307 | if (rendererValues.unloadedPattern == null) { 308 | ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); 309 | } else { 310 | ctx.translate(bggx, bggy); 311 | ctx.fillStyle = rendererValues.unloadedPattern; 312 | ctx.fillRect(-bggx, -bggy, ctx.canvas.width, ctx.canvas.height); 313 | ctx.translate(-bggx, -bggy); 314 | } 315 | } 316 | 317 | ctx.lineWidth = 2.5 / 16 * zoom; 318 | 319 | ctx.scale(zoom, zoom); 320 | 321 | for (var i = 0; i < visible.length; i++) { 322 | var cluster = visible[i]; 323 | var gx = -(camx - cluster.x * clusterCanvasSize); 324 | var gy = -(camy - cluster.y * clusterCanvasSize); 325 | var clipx = gx < 0 ? -gx : 0; 326 | var clipy = gy < 0 ? -gy : 0; 327 | var x = gx < 0 ? 0 : gx; 328 | var y = gy < 0 ? 0 : gy; 329 | var clipw = clusterCanvasSize - clipx; 330 | var cliph = clusterCanvasSize - clipy; 331 | clipw = clipw + x < cwidth / zoom ? clipw : cwidth / zoom - x; 332 | cliph = cliph + y < cheight / zoom ? cliph : cheight / zoom - y; 333 | //clipw = (clipw + 1) | 0; /* Math.ceil */ 334 | //cliph = (cliph + 1) | 0; 335 | if (clipw > 0 && cliph > 0) { 336 | ctx.drawImage(cluster.canvas, clipx, clipy, clipw, cliph, x, y, clipw, cliph); 337 | } 338 | } 339 | 340 | ctx.scale(1 / zoom, 1 / zoom); /* probably faster than ctx.save(), ctx.restore() */ 341 | 342 | /*if (background != null) { 343 | var newscale = zoom / options.defaultZoom; 344 | var oldscale = options.defaultZoom / zoom; 345 | var gx = -(camx * zoom) % (background.width * newscale); 346 | var gy = -(camy * zoom) % (background.height * newscale); 347 | ctx.translate(gx, gy); 348 | 349 | ctx.fillStyle = background; 350 | ctx.globalCompositeOperation = "destination-over"; 351 | 352 | ctx.scale(newscale, newscale); 353 | ctx.fillRect(-gx / newscale, -gy / newscale, ctx.canvas.width * oldscale, ctx.canvas.height * oldscale); 354 | ctx.scale(oldscale, oldscale); 355 | 356 | ctx.translate(-gx, -gy); 357 | }*/ 358 | 359 | if (rendererValues.gridShown && rendererValues.gridPattern) { 360 | ctx.translate(bggx, bggy); 361 | ctx.fillStyle = rendererValues.gridPattern; 362 | /*if (!allChunksLoaded) { 363 | ctx.globalCompositeOperation = "source-atop"; 364 | }*/ 365 | ctx.fillRect(-bggx, -bggy, ctx.canvas.width, ctx.canvas.height); 366 | ctx.translate(-bggx, -bggy); 367 | } 368 | 369 | 370 | //ctx.globalCompositeOperation = "source-over"; 371 | 372 | for (var i = 0; i < activeFx.length; i++) { 373 | switch (activeFx[i].render(ctx, time)) { 374 | case 0: /* Anim not finished */ 375 | needsRender |= renderer.rendertype.FX; 376 | break; 377 | case 2: /* Obj deleted from array, prevent flickering */ 378 | --i; 379 | break; 380 | } 381 | } 382 | ctx.globalAlpha = 1; 383 | var players = misc.world.players; 384 | var fontsize = 10 / 16 * zoom | 0; 385 | if (rendererValues.currentFontSize != fontsize) { 386 | ctx.font = fontsize + "px sans-serif"; 387 | rendererValues.currentFontSize = fontsize; 388 | } 389 | 390 | if (options.showPlayers) { 391 | for (var p in players) { 392 | var player = players[p]; 393 | if (!renderPlayer(player, fontsize)) { 394 | needsRender |= renderer.rendertype.FX; 395 | } 396 | } 397 | } 398 | } 399 | 400 | 401 | requestRender(needsRender); 402 | } 403 | 404 | function renderPlayer(targetPlayer, fontsize) { 405 | var camx = camera.x * 16; 406 | var camy = camera.y * 16; 407 | var zoom = camera.zoom; 408 | var ctx = rendererValues.animContext; 409 | var cnvs = ctx.canvas; 410 | var tool = targetPlayer.tool; 411 | if (!tool) { 412 | /* Render the default tool if the selected one isn't defined */ 413 | tool = tools['cursor']; 414 | } 415 | var toolwidth = tool.cursor.width / 16 * zoom; 416 | var toolheight = tool.cursor.height / 16 * zoom; 417 | 418 | var x = targetPlayer.x; 419 | var y = targetPlayer.y; 420 | var cx = ((x - camx) - tool.offset[0]) * (zoom / 16) | 0; 421 | var cy = ((y - camy) - tool.offset[1]) * (zoom / 16) | 0; 422 | 423 | if(cx < -toolwidth || cy < -toolheight 424 | || cx > cnvs.width || cy > cnvs.height) { 425 | return true; 426 | } 427 | 428 | if (fontsize > 3) { 429 | renderPlayerId(ctx, fontsize, zoom, cx, cy + toolheight, targetPlayer.id, targetPlayer.clr); 430 | } 431 | 432 | ctx.drawImage(tool.cursor, cx, cy, toolwidth, toolheight); 433 | 434 | return x === targetPlayer.endX && y === targetPlayer.endY; 435 | } 436 | 437 | function renderPlayerId(ctx, fontsize, zoom, x, y, id, color) { 438 | var idstr = id; 439 | var textw = ctx.measureText(idstr).width + (zoom / 2); 440 | 441 | ctx.globalAlpha = 1; 442 | ctx.fillStyle = color; 443 | ctx.fillRect(x, y, textw, zoom); 444 | ctx.globalAlpha = 0.2; 445 | ctx.lineWidth = 3; 446 | ctx.strokeStyle = "#000000"; 447 | ctx.strokeRect(x, y, textw, zoom); 448 | ctx.globalAlpha = 1; 449 | drawText(ctx, idstr, x + zoom / 4, y + fontsize + zoom / 8); 450 | } 451 | 452 | function requestRender(type) { 453 | rendererValues.updateRequired |= type; 454 | } 455 | 456 | function setGridVisibility(enabled) { 457 | rendererValues.gridShown = enabled; 458 | requestRender(renderer.rendertype.FX); 459 | } 460 | 461 | function renderGrid(zoom) { 462 | var tmpcanvas = document.createElement("canvas"); 463 | var ctx = tmpcanvas.getContext("2d"); 464 | var size = tmpcanvas.width = tmpcanvas.height = Math.round(16 * zoom); 465 | ctx.setLineDash([1]); 466 | ctx.globalAlpha = 0.2; 467 | if (zoom >= 4) { 468 | var fadeMult = Math.min(1, zoom - 4); 469 | if (fadeMult < 1) { 470 | ctx.globalAlpha = 0.2 * fadeMult; 471 | } 472 | ctx.beginPath(); 473 | for (var i = 16; --i;) { 474 | ctx.moveTo(i * zoom + .5, 0); 475 | ctx.lineTo(i * zoom + .5, size); 476 | ctx.moveTo(0, i * zoom + .5); 477 | ctx.lineTo(size, i * zoom + .5); 478 | } 479 | ctx.stroke(); 480 | ctx.globalAlpha = Math.max(0.2, 1 * fadeMult); 481 | } 482 | ctx.beginPath(); 483 | ctx.moveTo(0, 0); 484 | ctx.lineTo(0, size); 485 | ctx.lineTo(size, size); 486 | ctx.stroke(); 487 | return ctx.createPattern(tmpcanvas, "repeat"); 488 | } 489 | 490 | function setGridZoom(zoom) { 491 | if (zoom >= rendererValues.minGridZoom) { 492 | rendererValues.gridPattern = renderGrid(zoom); 493 | } else { 494 | rendererValues.gridPattern = null; 495 | } 496 | } 497 | 498 | function updateVisible() { 499 | var clusters = rendererValues.clusters; 500 | var visiblecl = rendererValues.visibleClusters; 501 | for (var c in clusters) { 502 | c = clusters[c]; 503 | var size = protocol.chunkSize * protocol.clusterChunkAmount; 504 | var visible = isVisible(c.x * size, c.y * size, size, size); 505 | if (!visible && c.shown) { 506 | c.shown = false; 507 | visiblecl.splice(visiblecl.indexOf(c), 1); 508 | } else if (visible && !c.shown) { 509 | c.shown = true; 510 | visiblecl.push(c); 511 | requestRender(renderer.rendertype.WORLD); 512 | } 513 | } 514 | }; 515 | 516 | function onResize() { 517 | elements.animCanvas.width = window.innerWidth; 518 | elements.animCanvas.height = window.innerHeight; 519 | var ctx = rendererValues.animContext; 520 | ctx.imageSmoothingEnabled = false; 521 | ctx.webkitImageSmoothingEnabled = false; 522 | ctx.mozImageSmoothingEnabled = false; 523 | ctx.msImageSmoothingEnabled = false; 524 | ctx.oImageSmoothingEnabled = false; 525 | rendererValues.currentFontSize = -1; 526 | onCameraMove(); 527 | } 528 | 529 | function alignCamera() { 530 | var zoom = cameraValues.zoom; 531 | var alignedX = Math.round(cameraValues.x * zoom) / zoom; 532 | var alignedY = Math.round(cameraValues.y * zoom) / zoom; 533 | cameraValues.x = alignedX; 534 | cameraValues.y = alignedY; 535 | } 536 | 537 | function requestMissingChunks() { /* TODO: move this to World */ 538 | var x = camera.x / protocol.chunkSize - 2 | 0; 539 | var mx = camera.x / protocol.chunkSize + window.innerWidth / camera.zoom / protocol.chunkSize | 0; 540 | var cy = camera.y / protocol.chunkSize - 2 | 0; 541 | var my = camera.y / protocol.chunkSize + window.innerHeight / camera.zoom / protocol.chunkSize | 0; 542 | while (++x <= mx) { 543 | var y = cy; 544 | while (++y <= my) { 545 | misc.world.loadChunk(x, y); 546 | } 547 | } 548 | } 549 | 550 | function onCameraMove() { 551 | eventSys.emit(e.camera.moved, camera); 552 | alignCamera(); 553 | updateVisible(); 554 | if (misc.world !== null) { 555 | requestMissingChunks(); 556 | } 557 | requestRender(renderer.rendertype.FX); 558 | } 559 | 560 | function getCenterPixel() { 561 | var x = Math.round(cameraValues.x + window.innerWidth / camera.zoom / 2); 562 | var y = Math.round(cameraValues.y + window.innerHeight / camera.zoom / 2); 563 | return [x, y]; 564 | } 565 | 566 | function centerCameraTo(x, y) { 567 | if(typeof(x) == "number" && !isNaN(x)){ 568 | cameraValues.x = -(window.innerWidth / camera.zoom / 2) + x; 569 | } 570 | 571 | if(typeof(y) == "number" && !isNaN(y)){ 572 | cameraValues.y = -(window.innerHeight / camera.zoom / 2) + y; 573 | } 574 | 575 | onCameraMove(); 576 | } 577 | 578 | function moveCameraBy(x, y) { 579 | cameraValues.x += x; 580 | cameraValues.y += y; 581 | onCameraMove(); 582 | } 583 | 584 | function moveCameraTo(x, y) { 585 | cameraValues.x = x; 586 | cameraValues.y = y; 587 | onCameraMove(); 588 | } 589 | 590 | eventSys.on(e.net.world.teleported, (x, y) => { 591 | centerCameraTo(x, y); 592 | }); 593 | 594 | eventSys.on(e.camera.zoom, z => { 595 | setGridZoom(z); 596 | /*cameraValues.lerpZoom.val = z;*/ 597 | requestRender(renderer.rendertype.FX); 598 | }); 599 | 600 | eventSys.on(e.renderer.addChunk, chunk => { 601 | var clusterX = Math.floor(chunk.x / protocol.clusterChunkAmount); 602 | var clusterY = Math.floor(chunk.y / protocol.clusterChunkAmount); 603 | var key = `${clusterX},${clusterY}`; 604 | var clusters = rendererValues.clusters; 605 | var cluster = clusters[key]; 606 | if (!cluster) { 607 | cluster = clusters[key] = new ChunkCluster(clusterX, clusterY); 608 | updateVisible(); 609 | } 610 | cluster.addChunk(chunk); 611 | if (!cluster.toUpdate) { 612 | cluster.toUpdate = true; 613 | rendererValues.updatedClusters.push(cluster); 614 | } 615 | var size = protocol.chunkSize; 616 | if (cluster.toUpdate || isVisible(chunk.x * size, chunk.y * size, size, size)) { 617 | requestRender(renderer.rendertype.WORLD | renderer.rendertype.FX); 618 | } 619 | }); 620 | 621 | eventSys.on(e.renderer.rmChunk, chunk => { 622 | var clusterX = Math.floor(chunk.x / protocol.clusterChunkAmount); 623 | var clusterY = Math.floor(chunk.y / protocol.clusterChunkAmount); 624 | var key = `${clusterX},${clusterY}`; 625 | var clusters = rendererValues.clusters; 626 | var cluster = clusters[key]; 627 | if (cluster) { 628 | cluster.delChunk(chunk); 629 | if (!cluster.removed && !cluster.toUpdate) { 630 | cluster.toUpdate = true; 631 | rendererValues.updatedClusters.push(cluster); 632 | } 633 | } 634 | }); 635 | 636 | eventSys.on(e.renderer.updateChunk, chunk => { 637 | var clusterX = Math.floor(chunk.x / protocol.clusterChunkAmount); 638 | var clusterY = Math.floor(chunk.y / protocol.clusterChunkAmount); 639 | var key = `${clusterX},${clusterY}`; 640 | var cluster = rendererValues.clusters[key]; 641 | if (cluster && !cluster.toUpdate) { 642 | cluster.toUpdate = true; 643 | rendererValues.updatedClusters.push(cluster); 644 | } 645 | var size = protocol.chunkSize; 646 | if (isVisible(chunk.x * size, chunk.y * size, size, size)) { 647 | requestRender(renderer.rendertype.WORLD | renderer.rendertype.FX); 648 | } 649 | }); 650 | 651 | eventSys.on(e.misc.worldInitialized, () => { 652 | requestMissingChunks(); 653 | }); 654 | 655 | eventSys.once(e.init, () => { 656 | rendererValues.animContext = elements.animCanvas.getContext("2d", { alpha: false }); 657 | window.addEventListener("resize", onResize); 658 | onResize(); 659 | camera.zoom = options.defaultZoom; 660 | centerCameraTo(0, 0); 661 | 662 | const mkPatternFromUrl = (url, cb) => { 663 | var patImg = new Image(); 664 | patImg.onload = () => { 665 | var pat = rendererValues.animContext.createPattern(patImg, "repeat"); 666 | pat.width = patImg.width; 667 | pat.height = patImg.height; 668 | cb(pat); 669 | }; 670 | patImg.src = url; 671 | }; 672 | 673 | /* Create the pattern images */ 674 | mkPatternFromUrl(options.unloadedPatternUrl, pat => { 675 | rendererValues.unloadedPattern = pat; 676 | }); 677 | 678 | if (options.backgroundUrl != null) { 679 | mkPatternFromUrl(options.backgroundUrl, pat => { 680 | rendererValues.worldBackground = pat; 681 | }); 682 | } 683 | 684 | function frameLoop() { 685 | let type; 686 | if ((type = rendererValues.updateRequired) !== 0) { 687 | rendererValues.updateRequired = 0; 688 | render(type); 689 | } 690 | window.requestAnimationFrame(frameLoop); 691 | } 692 | eventSys.once(e.misc.toolsInitialized, frameLoop); 693 | }); 694 | --------------------------------------------------------------------------------