icydraw/src/client/canvas.ts

241 lines
6.7 KiB
TypeScript

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 = $('<canvas class="canvas">') as HTMLCanvasElement;
private _ctx = this._canvas.getContext('2d');
private _wrapper = $('<div class="canvas__wrapper">');
private _container = $('<div class="canvas__container">');
private _cursor = $('<div class="canvas__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);
}
}