');
+ 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);
+ };
+}