From 5c0d6e4a2f6d1029cfe435af184957686f784d04 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sun, 3 Apr 2022 15:25:29 +0300 Subject: [PATCH] new version --- src/client/canvas.ts | 147 +++++++++++++++++++++++++++++-------- src/client/scss/index.scss | 18 ++++- src/common/helper.ts | 10 +++ 3 files changed, 139 insertions(+), 36 deletions(-) diff --git a/src/client/canvas.ts b/src/client/canvas.ts index 2762001..c393445 100644 --- a/src/client/canvas.ts +++ b/src/client/canvas.ts @@ -1,5 +1,5 @@ import { convertHex } from '../common/convert'; -import { clamp } from '../common/helper'; +import { clamp, debounce } from '../common/helper'; import { Placement } from '../common/types/canvas'; import { IcyNetUser } from '../server/types/user'; import { Picker } from './picker'; @@ -15,6 +15,7 @@ export class ViewCanvas { private _wrapper = $('
'); private _container = $('
'); private _cursor = $('
'); + private _coods = $('
'); private _size = 1000; private _viewWidth = 0; @@ -22,7 +23,7 @@ export class ViewCanvas { private _posx = 0; private _posy = 0; - private _zoom = this._size; + private _zoom = 1; private _mousex = 0; private _mousey = 0; @@ -42,10 +43,8 @@ export class ViewCanvas { constructor() {} public moveCanvas(): void { - const pixelSize = this._size / this._zoom; - this._canvas.style.top = `${this._posy}px`; - this._canvas.style.left = `${this._posx}px`; - this._canvas.style.transform = `scale(${pixelSize})`; + this._canvas.style.transform = `scale(${this._zoom})`; + this._container.style.transform = `translate(${this._posx}px, ${this._posy}px)`; } public center(): void { @@ -60,13 +59,11 @@ export class ViewCanvas { public moveCursor(): void { // Zoom multiplier - const pixelSize = this._size / this._zoom; // Apparent size of the canvas after scaling it - const realSize = pixelSize * this._size; + const realSize = this._zoom * this._size; // The difference between the real canvas size and apparent size - const scale = this._size / 2 - realSize / 2; - const screenX = this._posx + scale; - const screenY = this._posy + scale; + const screenX = this._posx; + const screenY = this._posy; // Position of the on-screen cursor, snapped // Relative to top left of screen @@ -78,27 +75,34 @@ export class ViewCanvas { ); // Position of the cursor on the canvas - this._relcursorx = Math.floor((this._cursorx - screenX) / pixelSize); - this._relcursory = Math.floor((this._cursory - screenY) / pixelSize); + this._relcursorx = Math.floor((this._cursorx - screenX) / this._zoom); + this._relcursory = Math.floor((this._cursory - screenY) / this._zoom); - this._screencursorx = this._relcursorx * pixelSize + screenX; - this._screencursory = this._relcursory * pixelSize + screenY; + this._screencursorx = this._relcursorx * this._zoom + screenX; + this._screencursory = this._relcursory * this._zoom + screenY; - this._cursor.style.top = `${this._screencursory + pixelSize / 2}px`; - this._cursor.style.left = `${this._screencursorx + pixelSize / 2}px`; - this._cursor.style.transform = `scale(${pixelSize})`; + this._cursor.style.top = `${this._screencursory + this._zoom / 2}px`; + this._cursor.style.left = `${this._screencursorx + this._zoom / 2}px`; + this._cursor.style.transform = `scale(${this._zoom})`; + + this._coods.innerText = `(${this._relcursorx}, ${ + this._relcursory + }) ${this._zoom.toFixed(2)}x`; + + this._updateURL(); } public initialize(): void { this.picker.initialize(); + this._wrapper.append(this._coods); this._container.append(this._canvas); - this._container.append(this._cursor); + this._wrapper.append(this._cursor); this._wrapper.append(this._container); this._wrapper.append(this.picker.element); document.body.append(this._wrapper); - this._container.addEventListener('pointermove', (ev: MouseEvent) => { + this._wrapper.addEventListener('pointermove', (ev: MouseEvent) => { const currentX = this._mousex; const currentY = this._mousey; @@ -109,39 +113,69 @@ export class ViewCanvas { const offsetY = currentY - this._mousey; if (this._dragging) { - this._posx -= offsetX; - this._posy -= offsetY; + const realSize = this._zoom * this._size; + this._posx = clamp( + this._posx - offsetX, + this._cursorx - realSize, + this._cursorx, + ); + this._posy = clamp( + this._posy - offsetY, + this._cursory - realSize, + this._cursory, + ); this.moveCanvas(); this.moveCursor(); } }); - this._container.addEventListener('pointerdown', (ev: MouseEvent) => { + this._wrapper.addEventListener('mousedown', (ev: MouseEvent) => { this._mousex = ev.clientX; this._mousey = ev.clientY; this._dragging = true; }); - this._container.addEventListener('pointerup', (ev: MouseEvent) => { + this._wrapper.addEventListener('mouseup', (ev: MouseEvent) => { this._dragging = false; }); - this._container.addEventListener('pointerleave', (ev: MouseEvent) => { + this._wrapper.addEventListener('touchstart', (ev: TouchEvent) => { + const touch = ev.touches[0] || ev.changedTouches[0]; + this._mousex = touch.pageX; + this._mousey = touch.pageY; + this._dragging = true; + }); + + this._wrapper.addEventListener('touchend', (ev: TouchEvent) => { this._dragging = false; }); - this._container.addEventListener('wheel', (ev: WheelEvent) => { + this._wrapper.addEventListener('pointerleave', (ev: MouseEvent) => { + this._dragging = false; + }); + + this._wrapper.addEventListener('wheel', (ev: WheelEvent) => { ev.preventDefault(); this._mousex = ev.clientX; this._mousey = ev.clientY; - const delta = Math.ceil(ev.deltaY / 2) * 2; - this._zoom = clamp((this._zoom += delta), 8, this._size); + const scaleX = (ev.clientX - this._posx) / this._zoom; + const scaleY = (ev.clientY - this._posy) / this._zoom; + + ev.deltaY < 0 ? (this._zoom *= 1.2) : (this._zoom /= 1.2); + this._zoom = clamp(this._zoom, 1, 100); + + this._posx = ev.clientX - scaleX * this._zoom; + this._posy = ev.clientY - scaleY * this._zoom; + + const realSize = this._zoom * this._size; + this._posx = clamp(this._posx, this._cursorx - realSize, this._cursorx); + this._posy = clamp(this._posy, this._cursory - realSize, this._cursory); - this.moveCanvas(); this.moveCursor(); + this.moveCanvas(); }); this.picker.registerOnPlace((color) => { @@ -155,8 +189,6 @@ export class ViewCanvas { } }); - this.setView(); - window.addEventListener('resize', () => this.setView()); window.addEventListener('keyup', (ev: KeyboardEvent) => { const numeral = parseInt(ev.key, 10); @@ -198,7 +230,7 @@ export class ViewCanvas { public setView() { this._viewWidth = document.body.clientWidth; this._viewHeight = document.body.clientHeight; - this.center(); + this._fromURL(); } public fill(size: number, canvas: number[]) { @@ -219,6 +251,8 @@ export class ViewCanvas { } } this._ctx!.putImageData(data, 0, 0); + + this.setView(); } public setPixel(x: number, y: number, pixel: number) { @@ -235,4 +269,53 @@ export class ViewCanvas { this._user = user; this.picker.setLoggedIn(user); } + + private _updateURL = debounce(() => { + const urlelements = new URLSearchParams({ + px: this._relcursorx.toString(), + py: this._relcursory.toString(), + z: this._zoom.toFixed(2), + }); + window.history.replaceState( + null, + document.title, + `/?${urlelements.toString()}`, + ); + }, 500); + + private _fromURL(): void { + const search = window.location.search.substring(1); + if (!search?.length) { + this.center(); + return; + } + + const obj = new URLSearchParams(search); + const move = !!obj.get('px') || !!obj.get('py'); + + if (!move) { + return this.center(); + } + + if (obj.get('z')) { + this._zoom = clamp(parseFloat(obj.get('z')), 1, 100); + } + + if (obj.get('px')) { + this._relcursorx = clamp(parseInt(obj.get('px'), 10), 0, this._size); + } + + if (obj.get('py')) { + this._relcursory = clamp(parseInt(obj.get('py'), 10), 0, this._size); + } + + this._cursorx = this._viewWidth / 2; + this._cursory = this._viewHeight / 2; + + this._posx = -Math.ceil(this._relcursorx * this._zoom - this._cursorx) - 1; + this._posy = -Math.ceil(this._relcursory * this._zoom - this._cursory); + + this.moveCanvas(); + this.moveCursor(); + } } diff --git a/src/client/scss/index.scss b/src/client/scss/index.scss index 544543d..3b012a6 100644 --- a/src/client/scss/index.scss +++ b/src/client/scss/index.scss @@ -21,21 +21,31 @@ body { image-rendering: pixelated; /* Awesome future-browsers */ -ms-interpolation-mode: nearest-neighbor; /* IE */ - position: absolute; z-index: 1001; - transform-origin: center center; + transform-origin: left top; &__wrapper { width: 100%; height: 100%; display: flex; background-color: rgb(143, 143, 143); + overflow: hidden; } &__container { position: relative; - flex-grow: 1; - overflow: hidden; + width: 0; + height: 0; + } + + &__coordinates { + position: absolute; + top: 1rem; + left: 1rem; + background-color: rgba(221, 221, 221, 0.404); + padding: 0.5rem; + border-radius: 5px; + z-index: 1008; } &__cursor { diff --git a/src/common/helper.ts b/src/common/helper.ts index d427d70..537f0c0 100644 --- a/src/common/helper.ts +++ b/src/common/helper.ts @@ -1,3 +1,13 @@ export function clamp(x: number, min: number, max: number): number { return Math.min(Math.max(x, min), max); } + +export function debounce(func: Function, timeout = 300) { + let timer: any; + return (...args: any[]) => { + clearTimeout(timer); + timer = setTimeout(() => { + func.apply(this, args); + }, timeout); + }; +}