From 58baae133b6a7f250b722cd5b58bc00578eaee9e Mon Sep 17 00:00:00 2001 From: Terncode Date: Sat, 15 Feb 2025 15:13:54 +0100 Subject: [PATCH] Add coordinate prediction --- src/UI/components/menu.tsx | 18 +++- src/UI/components/overlay.tsx | 2 + src/index.ts | 2 +- src/lib/coordinates.ts | 135 ++++++++++++++++---------- src/lib/coordinatesPredictor.ts | 166 ++++++++++++++++++++++++++++++++ src/lib/store.ts | 9 +- 6 files changed, 275 insertions(+), 57 deletions(-) create mode 100644 src/lib/coordinatesPredictor.ts diff --git a/src/UI/components/menu.tsx b/src/UI/components/menu.tsx index 9f4c543..08e166e 100644 --- a/src/UI/components/menu.tsx +++ b/src/UI/components/menu.tsx @@ -1,4 +1,4 @@ -import { faCamera, faCaretDown, faCaretUp, faPieChart, faUpload } from "@fortawesome/free-solid-svg-icons"; +import { faCamera, faCaretDown, faCaretUp, faLocationCrosshairs, faPieChart, faUpload } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React from "react"; import styled from "styled-components"; @@ -83,6 +83,7 @@ interface Props { interface State { takingCanvasShot: boolean; canvasShotAvailable: boolean; + locationPrecision: boolean; } export class Menu extends React.Component { @@ -92,6 +93,7 @@ export class Menu extends React.Component { this.state = { takingCanvasShot: false, canvasShotAvailable: true, + locationPrecision: props.cords.highPrecision }; } @@ -116,11 +118,15 @@ export class Menu extends React.Component { Popup.alert(error.name); } }; - inputElement = (event: React.FormEvent) => { const percentage = parseInt((event.target as HTMLInputElement).value); this.props.onOpacityChange(percentage); }; + togglePrecision = () => { + const value = !this.state.locationPrecision; + this.setState({ locationPrecision: value }); + this.props.cords.toggleHigherPrecision(value); + }; screenshot = async () => { if (this.props.cords.uScale < 0) { //Popup.alert("Cannot take screenshot at sc") @@ -148,6 +154,14 @@ export class Menu extends React.Component { onClick={() => this.props.showChart()} title="Pixels graph"> + + + {this.props.opacity}% diff --git a/src/UI/components/overlay.tsx b/src/UI/components/overlay.tsx index 91480dc..74734e9 100644 --- a/src/UI/components/overlay.tsx +++ b/src/UI/components/overlay.tsx @@ -46,6 +46,7 @@ export class Overlay extends React.Component { this.ref.current!.style.top = this.ref.current!.style.left = `0px`; this.props.cords.on(CordType.Url, this.draw); + this.props.cords.on(CordType.UrlPredict, this.draw); this.props.cords.on(CordType.Div, this.updateIfMouseDown); window.addEventListener("mousemove", this.onMouseMove); window.addEventListener("touchmove", this.onTouchMove); @@ -60,6 +61,7 @@ export class Overlay extends React.Component { } componentWillUnmount() { this.props.cords.off(CordType.Url, this.draw); + this.props.cords.off(CordType.UrlPredict, this.draw); this.props.cords.off(CordType.Div, this.updateIfMouseDown); window.removeEventListener("mousemove", this.onMouseMove); window.removeEventListener("touchmove", this.onTouchMove); diff --git a/src/index.ts b/src/index.ts index 06aa91b..bd78594 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,7 @@ async function main() { await store.load(); const injector = new Injector(store); await injector.inject(); - const coordinates = new Coordinates(injector); + const coordinates = new Coordinates(injector, store); await coordinates.init(); createUI(store,storage, coordinates, palette); diff --git a/src/lib/coordinates.ts b/src/lib/coordinates.ts index 5a28d91..11c42ec 100644 --- a/src/lib/coordinates.ts +++ b/src/lib/coordinates.ts @@ -1,11 +1,14 @@ import { CHUNK_SIZE } from "../constants"; +import { CoordinatePredictor } from "./coordinatesPredictor"; import { Listener, BasicEventEmitter } from "./eventEmitter"; import { InjectEvents, Injector } from "./injector"; +import { Store } from "./store"; import { waitForDraw } from "./utils"; export enum CordType { Div, - Url + Url, + UrlPredict } export class Coordinates { @@ -20,66 +23,90 @@ export class Coordinates { private _uy = 0; private _uScale = 0; private down?: number[]; + private coordinatePredictor: CoordinatePredictor; + private _highPrecision = false; //div: HTMLDivElement; - constructor(injector: Injector) { + constructor(injector: Injector, private storage: Store) { injector.on(InjectEvents.UrlChange, url => { this.onUrlCordsUpdate(url); }); this.onUrlCordsUpdate(); - - window.addEventListener("touchstart", (event) => { - if (event.target instanceof HTMLCanvasElement && event.touches.length === 1) { - this.down = [event.touches[0].clientX, event.touches[0].clientY, this.ux, this.uy]; - } + this.coordinatePredictor = new CoordinatePredictor(this, (x, y, scale) => { + this._ux = Math.round(x); + this._uy = Math.round(y); + this._uScale = Math.round(scale); + this.emitter.emit(CordType.UrlPredict, x, y, scale); }); - window.addEventListener("touchmove", event => { - if (this.down && event.touches.length === 1) { - const mx = Math.round((this.down[0] - event.touches[0].clientX) / Math.pow(2, this._uScale)); - const my = Math.round((this.down[1] - event.touches[0].clientY) / Math.pow(2, this._uScale)); - const ux = this._ux; - const uy = this._uy; - this._ux = this.down[2] + mx, - this._uy = this.down[3] + my; - if (ux !== this._ux || uy !== this._uy) { - this.emitter.emit(CordType.Url, this._ux, this._uy, this._uScale); - } - } - }); - window.addEventListener("touchend", () => { - this.down = undefined; - }); - window.addEventListener("mousedown", event => { - if (event.target instanceof HTMLCanvasElement) { - this.down = [event.x, event.y, this.ux, this.uy]; - } - }); - window.addEventListener("mousemove", event => { - if (this.down) { - const mx = Math.round((this.down[0] - event.x) / Math.pow(2, this._uScale)); - const my = Math.round((this.down[1] - event.y) / Math.pow(2, this._uScale)); - const ux = this._ux; - const uy = this._uy; - this._ux = this.down[2] + mx, - this._uy = this.down[3] + my; - if (ux !== this._ux || uy !== this._uy) { - this.emitter.emit(CordType.Url, this._ux, this._uy, this._uScale); - } - } - }); - window.addEventListener("mouseup", () => { - this.down = undefined; - }); - - // window.addEventListener("wheel" , event => { - // const s = this._uScale; - // if (event.deltaY > 0) { - // this._uScale = s / 2; - // } else { - // this._uScale = s * 2; - // } - // }, true); } + toggleHigherPrecision(value: boolean) { + if (this._highPrecision === value) { + return; + } + if (value) { + this.coordinatePredictor.enable(); + window.removeEventListener("touchstart", this.touchStart); + window.removeEventListener("touchmove", this.touchMove); + window.removeEventListener("touchend", this.endMovement); + window.removeEventListener("mousedown", this.mouseDown); + window.removeEventListener("mousemove", this.mouseMove); + window.removeEventListener("mouseup", this.endMovement); + } else { + this.coordinatePredictor.disable(); + window.addEventListener("touchstart", this.touchStart); + window.addEventListener("touchmove", this.touchMove); + window.addEventListener("touchend", this.endMovement); + window.addEventListener("mousedown", this.mouseDown); + window.addEventListener("mousemove", this.mouseMove); + window.addEventListener("mouseup", this.endMovement); + } + this.storage.toggleHighPrecision(value); + this._highPrecision = value; + } + get highPrecision() { + return this._highPrecision; + } + + private touchStart = (event: TouchEvent) => { + if (event.target instanceof HTMLCanvasElement && event.touches.length === 1) { + this.down = [event.touches[0].clientX, event.touches[0].clientY, this.ux, this.uy]; + } + }; + private touchMove = (event: TouchEvent) => { + if (this.down && event.touches.length === 1) { + const mx = Math.round((this.down[0] - event.touches[0].clientX) / Math.pow(2, this._uScale)); + const my = Math.round((this.down[1] - event.touches[0].clientY) / Math.pow(2, this._uScale)); + const ux = this._ux; + const uy = this._uy; + this._ux = this.down[2] + mx, + this._uy = this.down[3] + my; + if (ux !== this._ux || uy !== this._uy) { + this.emitter.emit(CordType.Url, this._ux, this._uy, this._uScale); + } + } + }; + private mouseDown = (event: MouseEvent) => { + if (event.target instanceof HTMLCanvasElement) { + this.down = [event.x, event.y, this.ux, this.uy]; + } + }; + private mouseMove = (event: MouseEvent) => { + if (this.down) { + const mx = Math.round((this.down[0] - event.x) / Math.pow(2, this._uScale)); + const my = Math.round((this.down[1] - event.y) / Math.pow(2, this._uScale)); + const ux = this._ux; + const uy = this._uy; + this._ux = this.down[2] + mx, + this._uy = this.down[3] + my; + if (ux !== this._ux || uy !== this._uy) { + this.emitter.emit(CordType.Url, this._ux, this._uy, this._uScale); + } + } + }; + private endMovement = () => { + this.down = undefined; + }; + on(event: CordType, listener: Listener<[number, number, number]>) { this.emitter.on(event, listener); } @@ -105,6 +132,8 @@ export class Coordinates { } await waitForDraw(); } + const value = await this.storage.enabledHighPrecision(); + this.toggleHigherPrecision(value); } stop() { diff --git a/src/lib/coordinatesPredictor.ts b/src/lib/coordinatesPredictor.ts new file mode 100644 index 0000000..009054d --- /dev/null +++ b/src/lib/coordinatesPredictor.ts @@ -0,0 +1,166 @@ +import { CHUNK_SIZE } from "../constants"; +import { Coordinates, CordType } from "./coordinates"; + +export class CoordinatePredictor { + private frame?: number; + tileRef?: HTMLCanvasElement; + private lastX?: number; + private lastY?: number; + private lastScale?: number; + private cords: number[]; + + constructor( + private coordinates: Coordinates, + private predict: (x: number, y: number, scale: number) => void + ) { + coordinates.on(CordType.Url, (x, y, scale) => { + if (this.cords) { + this.cords[0] = x; + this.cords[1] = y; + this.cords[2] = scale; + } else { + this.cords = [x, y, scale]; + } + }); + + } + + private cordUpdate = (x: number, y: number, s: number) => { + this.cords = [x, y, s]; + }; + syncCords() { + const cords = this.coordinates; + this.cordUpdate(cords.ux, cords.uy, cords.uScale); + } + enable() { + if (this.frame) + return; + + this.syncCords(); + this.coordinates.on(CordType.Url, this.cordUpdate); + this.frame = requestAnimationFrame(this.tick); + } + disable() { + if (this.frame) { + this.coordinates.off(CordType.Url, this.cordUpdate); + cancelAnimationFrame(this.frame); + this.frame = undefined; + } + } + get enabled() { + return !!this.frame; + } + tick = () => { + if (!this.tileRef || !document.body.contains(this.tileRef)) { + this.tileRef = [...document.getElementsByTagName("canvas")] + .find(e => e.classList.contains("leaflet-tile")); + + if (this.tileRef) { + const rect = this.tileRef?.getBoundingClientRect(); + if (rect) { + this.syncCords(); + this.lastScale = this.getScale(rect.width); + this.lastX = rect?.left; + this.lastY = rect?.top; + } + this.nextFrame(); + return; + } + } else { + const rect = this.tileRef?.getBoundingClientRect(); + if (rect) { + if (this.lastX != null && this.lastY != null && this.lastScale != null) { + const scale = this.getScale(rect.width); + const x = rect?.left; + const y = rect?.top; + if (x !== this.lastX || y !== this.lastY || scale != this.lastScale) { + const mx = this.lastX - rect?.left; + const my = this.lastY - rect?.top; + //const ms = this.lastScale - this.lastScale; + // console.log(this.lastX - rect?.left, this.lastY - rect?.top, scale); + // console.log(x / this.lastScale); + // console.log(y / this.lastScale); + this.lastX = x; + this.lastY = y; + this.lastScale = scale; + this.cords[0] += (mx / (rect.width / CHUNK_SIZE)); + this.cords[1] += (my / (rect.width / CHUNK_SIZE)); + this.predict(this.cords[0], this.cords[1], this.cords[2]); + this.cords[2] = scale; + } + } + } + } + + + this.nextFrame(); + }; + + private nextFrame() { + if (this.frame) { + this.frame = requestAnimationFrame(this.tick); + } + } + + private getScale(width: number) { + const scale = width / CHUNK_SIZE; + return Math.log(scale) / Math.log(2); + } + + // private get centerCanvas() { + // const hww = window.innerWidth / 2; + // const hhw = window.innerHeight / 2; + // const canvasObject = [ + // ...document.getElementsByTagName("canvas")] + // .filter(c => c.classList.contains("leaflet-tile")) + // .map(c => { + // const bounds = c.getBoundingClientRect(); + // return { + // canvas: c, + // bounds: { + // width: bounds.width, + // height: bounds.height, + // top: bounds.top, + // bottom: bounds.bottom, + // left: bounds.left, + // right: bounds.right, + // }, + // }; + // }).find(r => hww > r.bounds.left && hww < r.bounds.right && hhw > r.bounds.top && hhw < r.bounds.bottom); + + // if (canvasObject) { + // const ratio = canvasObject.canvas.width / CHUNK_SIZE; + // if (ratio === 2) { // scale -1 + // const hw = canvasObject.bounds.left + canvasObject.bounds.width / 2; + // const hh = canvasObject.bounds.top + canvasObject.bounds.height / 2; + // const chunkW = CHUNK_SIZE / canvasObject.canvas.width; + // const chunkH = CHUNK_SIZE / canvasObject.canvas.height; + // canvasObject.bounds.width = chunkW; + // canvasObject.bounds.height = chunkH; + + // if (hww <= hw && hhw <= hh) { + // // top left + // } else if (hww > hw && hhw < hh) { + // // top right + // canvasObject.bounds.top += chunkW; + // } else if (hww > hw && hhw > hh) { + // // bottom left + // canvasObject.bounds.left += chunkH; + // } else { + // // bottom left right + // canvasObject.bounds.left += chunkW; + // canvasObject.bounds.top += chunkH; + // } + // canvasObject.bounds.bottom = canvasObject.bounds.top + chunkW; + // canvasObject.bounds.right = canvasObject.bounds.left + chunkH; + // return null; + // } + // canvasObject.bounds.width /= ratio; + // canvasObject.bounds.height /= ratio; + // canvasObject.bounds.right = canvasObject.bounds.left + canvasObject.bounds.width; + // canvasObject.bounds.bottom = canvasObject.bounds.bottom + canvasObject.bounds.height; + // } + + // return canvasObject; + // } +} \ No newline at end of file diff --git a/src/lib/store.ts b/src/lib/store.ts index d755302..951769d 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -38,10 +38,10 @@ export interface PixelLogEx extends PixelLog { } export class Store implements LoadUnload { + private readonly STORAGE_KEY_HIGH_PRECISION = "_high-precision"; private readonly STORAGE_KEY_MURAL = "_murals"; private readonly STORAGE_KEY_SELECTED = "_mural"; private readonly STORAGE_KEY_LOG_STORE = "records"; - private readonly STORAGE_KEY_PIXEL_LOG = "_pixel-log"; private _murals: MuralEx[] = []; private _selected?: MuralEx; private emitter = new BasicEventEmitter(); @@ -290,6 +290,13 @@ export class Store implements LoadUnload { this.emit(StoreEvents.MuralPhantomOverlay); } } + + async enabledHighPrecision() { + return (await this.storage.getItem(this.STORAGE_KEY_HIGH_PRECISION)) ?? false; + } + toggleHighPrecision(value: boolean) { + return this.storage.setItem(this.STORAGE_KEY_HIGH_PRECISION, value); + } get murals() { return this._murals; }