import { convertHex } from '../common/convert'; import { clamp } from '../common/helper'; import { Placement } from '../common/types/canvas'; import { IcyNetUser } from '../server/types/user'; import { Picker } from './picker'; import { $ } from './utils/dom'; export class ViewCanvas { public picker = new Picker(); private _user?: IcyNetUser; private _fn?: (placement: Placement) => void; private _canvas = $('') as HTMLCanvasElement; private _ctx = this._canvas.getContext('2d'); private _wrapper = $('
'); private _container = $('
'); private _cursor = $('
'); private _size = 1000; private _viewWidth = 0; private _viewHeight = 0; private _posx = 0; private _posy = 0; private _zoom = this._size; private _mousex = 0; private _mousey = 0; private _relmousex = 0; private _relmousey = 0; private _cursorx = 0; private _cursory = 0; private _relcursorx = 0; private _relcursory = 0; private _dragging = false; constructor() {} public moveCanvas(): void { const zoomLength = this._size / this._zoom; // Snap the canvas coordinates to pixels, using the zoom level as the size of one pixel const snapx = Math.floor(this._posx / zoomLength) * zoomLength; const snapy = Math.floor(this._posy / zoomLength) * zoomLength; this._canvas.style.top = `${snapy}px`; this._canvas.style.left = `${snapx}px`; this._canvas.style.transform = `scale(${zoomLength})`; } public center(): void { const offsetWidth = this._viewWidth - this._size; const offsetHeight = this._viewHeight - this._size; this._posx = offsetWidth / 2; this._posy = offsetHeight / 2; this.moveCanvas(); this.moveCursor(); } public moveCursor(): void { // Zoom multiplier const pixelSize = this._size / this._zoom; // Apparent size of the canvas after scaling it const realSize = pixelSize * 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; // Position of the on-screen cursor, snapped // Relative to top left of screen this._cursorx = Math.ceil( clamp(this._viewWidth / 2, screenX, screenX + realSize) / pixelSize, ) * pixelSize; this._cursory = Math.ceil( clamp(this._viewHeight / 2, screenY, screenY + realSize) / pixelSize, ) * pixelSize; // Position of the cursor on the canvas this._relcursorx = Math.floor((this._cursorx - screenX) / pixelSize); this._relcursory = Math.floor((this._cursory - screenY) / pixelSize); this._cursor.style.top = `${this._cursory - pixelSize / 2}px`; this._cursor.style.left = `${this._cursorx - pixelSize / 2}px`; this._cursor.style.transform = `scale(${pixelSize})`; } public initialize(): void { this.picker.initialize(); this._container.append(this._canvas); this._container.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) => { const currentX = this._mousex; const currentY = this._mousey; this._mousex = ev.clientX; this._mousey = ev.clientY; const offsetX = currentX - this._mousex; const offsetY = currentY - this._mousey; if (this._dragging) { this._posx -= offsetX; this._posy -= offsetY; } if (this._dragging) { this.moveCanvas(); this.moveCursor(); } }); this._container.addEventListener('pointerdown', (ev: MouseEvent) => { this._mousex = ev.clientX; this._mousey = ev.clientY; this._dragging = true; }); this._container.addEventListener('pointerup', (ev: MouseEvent) => { this._dragging = false; }); this._container.addEventListener('pointerleave', (ev: MouseEvent) => { this._dragging = false; }); this._container.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); this.moveCanvas(); this.moveCursor(); }); this.picker.registerOnPlace((color) => { if (this._fn) { this._fn({ x: this._relcursorx, y: this._relcursory, c: color, t: Date.now(), }); } }); this.setView(); window.addEventListener('resize', () => this.setView()); window.addEventListener('keyup', (ev: KeyboardEvent) => { const numeral = parseInt(ev.key, 10); if (ev.key && !Number.isNaN(numeral)) { this.picker.pickPalette(numeral === 0 ? 9 : numeral - 1); return; } if ( !['ArrowLeft', 'ArrowRight', 'ArrowDown', 'ArrowUp', ' '].includes( ev.key, ) ) { return; } const pixelSize = this._size / this._zoom; if (ev.key === 'ArrowLeft') { this._posx += pixelSize; } else if (ev.key === 'ArrowRight') { this._posx -= pixelSize; } if (ev.key === 'ArrowUp') { this._posy += pixelSize; } else if (ev.key === 'ArrowDown') { this._posy -= pixelSize; } if (ev.key === ' ') { this.picker.place(); return; } this.moveCanvas(); this.moveCursor(); }); } public setView() { this._viewWidth = document.body.clientWidth; this._viewHeight = document.body.clientHeight; this.center(); } public fill(size: number, canvas: number[]) { this._size = size; this._canvas.width = this._size; this._canvas.height = this._size; const data = this._ctx!.getImageData(0, 0, this._size, this._size); for (let row = 0; row < this._size; row++) { for (let col = 0; col < this._size; col++) { const index = col + row * this._size; const pixel = canvas[index]; const { r, g, b } = convertHex(pixel); data.data[4 * index] = r; data.data[4 * index + 1] = g; data.data[4 * index + 2] = b; data.data[4 * index + 3] = 255; } } this._ctx!.putImageData(data, 0, 0); } public setPixel(x: number, y: number, pixel: number) { const { r, g, b } = convertHex(pixel); this._ctx!.fillStyle = `rgb(${r},${g},${b})`; this._ctx!.fillRect(x, y, 1, 1); } public registerOnPlace(fn: (placement: Placement) => void): void { this._fn = fn; } public setUser(user: IcyNetUser): void { this._user = user; this.picker.setLoggedIn(user); } }