const apiURL = new URL('http://localhost:8000'); const wsURL = new URL('ws://localhost:8000/ws/v1/pixels'); class ColorBar { #colorPicker; constructor() { this.#colorPicker = document.querySelector('#color-picker'); } get hex() { return this.#colorPicker.value.substring(1, 7); }; get decimal() { return parseInt(this.hex, 16); }; } class BattleField { WIDTH$U = 64; HEIGHT$U = 32; PIXEL_SIZE$PX = 96; BORDER_SIZE$PX = 4; BACKGROUND_COLOR$HEX = '#FFFFFFFF'; BORDER_COLOR$HEX = '#00000010'; #colorBar; #main; #canvas; #context; constructor() { this.#colorBar = new ColorBar(); this.#main = document.querySelector('#main'); this.#canvas = document.querySelector('#battlefield'); this.#context = this.#canvas.getContext('2d'); this.#canvas.width = this.WIDTH$U * this.PIXEL_SIZE$PX; this.#canvas.height = this.HEIGHT$U * this.PIXEL_SIZE$PX; this.#main.onpointerdown = (event) => { const movement = () => Math.sqrt(Math.pow(xd$px, 2) + Math.pow(yd$px, 2)) / window.innerWidth > .005; let x0$pct = event.pageX * 100 / window.innerWidth; let y0$pct = event.pageY * 100 / window.innerHeight; let x0$px = event.pageX; let y0$px = event.pageY; let xd$px = 0; let yd$px = 0; this.#main.onpointermove = (event) => { if (movement()) document.body.classList.add('grabbing'); this.x += event.pageX * 100 / window.innerWidth - x0$pct; this.y += event.pageY * 100 / window.innerHeight - y0$pct; x0$pct = event.pageX * 100 / window.innerWidth; y0$pct = event.pageY * 100 / window.innerHeight; xd$px += Math.abs(x0$px - event.pageX); yd$px += Math.abs(y0$px - event.pageY); }; this.#main.onpointerup = () => { this.#main.onpointermove = null; this.#main.onpointerup = null; document.body.classList.remove('grabbing'); if (movement()) return; const xhr = new XMLHttpRequest(); const url = new URL(apiURL); url.pathname = '/api/v1/pixels'; xhr.open('PUT', url); xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); xhr.send( JSON.stringify({ x: Math.floor((event.pageX - this.#canvas.offsetLeft) / this.scale / this.PIXEL_SIZE$PX + this.WIDTH$U / 2), y: Math.floor((event.pageY - this.#canvas.offsetTop) / this.scale / this.PIXEL_SIZE$PX + this.HEIGHT$U / 2), color: this.#colorBar.decimal, }) ); }; }; document.onwheel = (event) => { if (event.deltaY > 0) this.scale += .1; if (event.deltaY < 0) this.scale -= .1; }; for (let x = 0; x < this.WIDTH$U; x++) { for (let y = 0; y < this.HEIGHT$U; y++) { this.setPixel( x, y, this.BACKGROUND_COLOR$HEX, ); } } }; get x() { return Number(this.#canvas.style.left.match(/^(.*)%$/)[1]); }; set x(value) { // const min$pct = -100 * this.scale; // const max$pct = 100 * this.scale + 100; // if (value < min$pct) { // this.#canvas.style.left = `${min$pct}%`; // return; // } // if (value > max$pct) { // this.#canvas.style.left = `${max$pct}%`; // return; // } this.#canvas.style.left = `${value}%`; }; get y() { return Number(this.#canvas.style.top.match(/^(.*)%$/)[1]); }; set y(value) { // const min$pct = -100 * this.scale; // const max$pct = 100 * this.scale + 100; // if (value < min$pct) { // this.#canvas.style.top = `${min$pct}%`; // return; // } // if (value > max$pct) { // this.#canvas.style.top = `${max$pct}%`; // return; // } this.#canvas.style.top = `${value}%`; }; get scale() { return Number(this.#canvas.style.scale); }; set scale(value) { if (value >= .2 && value <= 1) { this.#canvas.style.scale = String(value); // TODO(костыль) // this.x = this.x; // this.y = this.y; } }; fill() { const xhr = new XMLHttpRequest(); const url = new URL(apiURL); url.pathname = '/api/v1/pixels'; xhr.onload = () => { if (xhr.status !== 200) return; const response = JSON.parse(xhr.response); for (const pixel of response.pixels) { this.setPixel( pixel.x, pixel.y, decimalToHex(pixel.color), ); } }; xhr.open('GET', url); xhr.send(); }; setPixel(x, y, color) { let leftPadding = x === 0 ? this.BORDER_SIZE$PX : 0; let topPadding = y === 0 ? this.BORDER_SIZE$PX : 0; this.#context.fillStyle = this.BACKGROUND_COLOR$HEX; this.#context.fillRect( x * this.PIXEL_SIZE$PX, y * this.PIXEL_SIZE$PX, this.PIXEL_SIZE$PX, this.PIXEL_SIZE$PX, ); this.#context.fillStyle = this.BORDER_COLOR$HEX; this.#context.fillRect( x * this.PIXEL_SIZE$PX, y * this.PIXEL_SIZE$PX, this.PIXEL_SIZE$PX, this.PIXEL_SIZE$PX, ); this.#context.fillStyle = color; this.#context.fillRect( x * this.PIXEL_SIZE$PX + leftPadding, y * this.PIXEL_SIZE$PX + topPadding, this.PIXEL_SIZE$PX - this.BORDER_SIZE$PX - leftPadding, this.PIXEL_SIZE$PX - this.BORDER_SIZE$PX - topPadding, ); }; } const decimalToHex = (decimal) => `#${decimal.toString(16)}`; window.addEventListener('DOMContentLoaded', () => { const battlefield = new BattleField(); battlefield.fill(); const wsConnect = () => { const ws = new WebSocket(wsURL); ws.onmessage = (event) => { const pixel = JSON.parse(event.data); battlefield.setPixel( pixel.x, pixel.y, decimalToHex(pixel.color), ); }; ws.onclose = (event) => { setTimeout(wsConnect, 5000); } ws.onerror = () => { ws.close(); }; }; wsConnect(); });