From e4f44c10e857729e0c357a24c898a841fa43646b Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Fri, 8 Aug 2025 13:15:21 +0200 Subject: [PATCH] resolve TypeScript errors and memory leaks Fixed all TypeScript compilation errors by defining a dedicated TransformOrigin type and adding proper null checks. Implemented comprehensive event handler cleanup to prevent memory leaks and improved cross-platform support with Meta key handling for macOS users. --- js/CanvasInteractions.js | 111 +++++++++++++++++++------------ src/CanvasInteractions.ts | 135 +++++++++++++++++++++++++------------- 2 files changed, 161 insertions(+), 85 deletions(-) diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 56156c7..7442b7a 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -3,16 +3,33 @@ import { snapToGrid, getSnapAdjustment } from "./utils/CommonUtils.js"; const log = createModuleLogger('CanvasInteractions'); export class CanvasInteractions { constructor(canvas) { + // Bound event handlers to enable proper removeEventListener and avoid leaks + this.onMouseDown = (e) => this.handleMouseDown(e); + this.onMouseMove = (e) => this.handleMouseMove(e); + this.onMouseUp = (e) => this.handleMouseUp(e); + this.onMouseEnter = (e) => { this.canvas.isMouseOver = true; this.handleMouseEnter(e); }; + this.onMouseLeave = (e) => { this.canvas.isMouseOver = false; this.handleMouseLeave(e); }; + this.onWheel = (e) => this.handleWheel(e); + this.onKeyDown = (e) => this.handleKeyDown(e); + this.onKeyUp = (e) => this.handleKeyUp(e); + this.onDragOver = (e) => this.handleDragOver(e); + this.onDragEnter = (e) => this.handleDragEnter(e); + this.onDragLeave = (e) => this.handleDragLeave(e); + this.onDrop = (e) => { this.handleDrop(e); }; + this.onContextMenu = (e) => this.handleContextMenu(e); + this.onBlur = () => this.handleBlur(); + this.onPaste = (e) => this.handlePasteEvent(e); this.canvas = canvas; this.interaction = { mode: 'none', panStart: { x: 0, y: 0 }, dragStart: { x: 0, y: 0 }, - transformOrigin: {}, + transformOrigin: null, resizeHandle: null, resizeAnchor: { x: 0, y: 0 }, canvasResizeStart: { x: 0, y: 0 }, isCtrlPressed: false, + isMetaPressed: false, isAltPressed: false, isShiftPressed: false, isSPressed: false, @@ -37,7 +54,6 @@ export class CanvasInteractions { e.stopPropagation(); } performZoomOperation(worldCoords, zoomFactor) { - const rect = this.canvas.canvas.getBoundingClientRect(); const mouseBufferX = (worldCoords.x - this.canvas.viewport.x) * this.canvas.viewport.zoom; const mouseBufferY = (worldCoords.y - this.canvas.viewport.y) * this.canvas.viewport.zoom; const newZoom = Math.max(0.1, Math.min(10, this.canvas.viewport.zoom * zoomFactor)); @@ -64,29 +80,39 @@ export class CanvasInteractions { } } setupEventListeners() { - this.canvas.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this)); - this.canvas.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this)); - this.canvas.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this)); - this.canvas.canvas.addEventListener('mouseleave', this.handleMouseLeave.bind(this)); - this.canvas.canvas.addEventListener('wheel', this.handleWheel.bind(this), { passive: false }); - this.canvas.canvas.addEventListener('keydown', this.handleKeyDown.bind(this)); - this.canvas.canvas.addEventListener('keyup', this.handleKeyUp.bind(this)); + this.canvas.canvas.addEventListener('mousedown', this.onMouseDown); + this.canvas.canvas.addEventListener('mousemove', this.onMouseMove); + this.canvas.canvas.addEventListener('mouseup', this.onMouseUp); + this.canvas.canvas.addEventListener('wheel', this.onWheel, { passive: false }); + this.canvas.canvas.addEventListener('keydown', this.onKeyDown); + this.canvas.canvas.addEventListener('keyup', this.onKeyUp); // Add a blur event listener to the window to reset key states - window.addEventListener('blur', this.handleBlur.bind(this)); - document.addEventListener('paste', this.handlePasteEvent.bind(this)); - this.canvas.canvas.addEventListener('mouseenter', (e) => { - this.canvas.isMouseOver = true; - this.handleMouseEnter(e); - }); - this.canvas.canvas.addEventListener('mouseleave', (e) => { - this.canvas.isMouseOver = false; - this.handleMouseLeave(e); - }); - this.canvas.canvas.addEventListener('dragover', this.handleDragOver.bind(this)); - this.canvas.canvas.addEventListener('dragenter', this.handleDragEnter.bind(this)); - this.canvas.canvas.addEventListener('dragleave', this.handleDragLeave.bind(this)); - this.canvas.canvas.addEventListener('drop', this.handleDrop.bind(this)); - this.canvas.canvas.addEventListener('contextmenu', this.handleContextMenu.bind(this)); + window.addEventListener('blur', this.onBlur); + document.addEventListener('paste', this.onPaste); + this.canvas.canvas.addEventListener('mouseenter', this.onMouseEnter); + this.canvas.canvas.addEventListener('mouseleave', this.onMouseLeave); + this.canvas.canvas.addEventListener('dragover', this.onDragOver); + this.canvas.canvas.addEventListener('dragenter', this.onDragEnter); + this.canvas.canvas.addEventListener('dragleave', this.onDragLeave); + this.canvas.canvas.addEventListener('drop', this.onDrop); + this.canvas.canvas.addEventListener('contextmenu', this.onContextMenu); + } + teardownEventListeners() { + this.canvas.canvas.removeEventListener('mousedown', this.onMouseDown); + this.canvas.canvas.removeEventListener('mousemove', this.onMouseMove); + this.canvas.canvas.removeEventListener('mouseup', this.onMouseUp); + this.canvas.canvas.removeEventListener('wheel', this.onWheel); + this.canvas.canvas.removeEventListener('keydown', this.onKeyDown); + this.canvas.canvas.removeEventListener('keyup', this.onKeyUp); + window.removeEventListener('blur', this.onBlur); + document.removeEventListener('paste', this.onPaste); + this.canvas.canvas.removeEventListener('mouseenter', this.onMouseEnter); + this.canvas.canvas.removeEventListener('mouseleave', this.onMouseLeave); + this.canvas.canvas.removeEventListener('dragover', this.onDragOver); + this.canvas.canvas.removeEventListener('dragenter', this.onDragEnter); + this.canvas.canvas.removeEventListener('dragleave', this.onDragLeave); + this.canvas.canvas.removeEventListener('drop', this.onDrop); + this.canvas.canvas.removeEventListener('contextmenu', this.onContextMenu); } /** * Sprawdza czy punkt znajduje się w obszarze któregokolwiek z zaznaczonych layerów @@ -163,7 +189,7 @@ export class CanvasInteractions { } return; } - if (e.button !== 0) { // Środkowy przycisk + if (e.button === 1) { // Środkowy przycisk this.startPanning(e); return; } @@ -179,7 +205,7 @@ export class CanvasInteractions { return; } // 4. Domyślna akcja na tle (lewy przycisk bez modyfikatorów) - this.startPanningOrClearSelection(e); + this.startPanning(e, true); // clearSelection = true } handleMouseMove(e) { const coords = this.getMouseCoordinates(e); @@ -376,7 +402,7 @@ export class CanvasInteractions { } } calculateGridBasedScaling(oldHeight, deltaY) { - const gridSize = 64; + const gridSize = 64; // Grid size - could be made configurable in the future const direction = deltaY > 0 ? -1 : 1; let targetHeight; if (direction > 0) { @@ -401,6 +427,8 @@ export class CanvasInteractions { handleKeyDown(e) { if (e.key === 'Control') this.interaction.isCtrlPressed = true; + if (e.key === 'Meta') + this.interaction.isMetaPressed = true; if (e.key === 'Shift') this.interaction.isShiftPressed = true; if (e.key === 'Alt') { @@ -485,6 +513,8 @@ export class CanvasInteractions { handleKeyUp(e) { if (e.key === 'Control') this.interaction.isCtrlPressed = false; + if (e.key === 'Meta') + this.interaction.isMetaPressed = false; if (e.key === 'Shift') this.interaction.isShiftPressed = false; if (e.key === 'Alt') @@ -504,6 +534,7 @@ export class CanvasInteractions { handleBlur() { log.debug('Window lost focus, resetting key states.'); this.interaction.isCtrlPressed = false; + this.interaction.isMetaPressed = false; this.interaction.isAltPressed = false; this.interaction.isShiftPressed = false; this.interaction.isSPressed = false; @@ -525,6 +556,11 @@ export class CanvasInteractions { } } updateCursor(worldCoords) { + // If actively rotating, show grabbing cursor + if (this.interaction.mode === 'rotating') { + this.canvas.canvas.style.cursor = 'grabbing'; + return; + } const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y); if (transformTarget) { const handleName = transformTarget.handle; @@ -572,7 +608,8 @@ export class CanvasInteractions { } prepareForDrag(layer, worldCoords) { // Zaktualizuj zaznaczenie, ale nie zapisuj stanu - if (this.interaction.isCtrlPressed) { + // Support both Ctrl (Windows/Linux) and Cmd (macOS) for multi-selection + if (this.interaction.isCtrlPressed || this.interaction.isMetaPressed) { const index = this.canvas.canvasSelection.selectedLayers.indexOf(layer); if (index === -1) { this.canvas.canvasSelection.updateSelection([...this.canvas.canvasSelection.selectedLayers, layer]); @@ -590,10 +627,9 @@ export class CanvasInteractions { this.interaction.mode = 'potential-drag'; this.interaction.dragStart = { ...worldCoords }; } - startPanningOrClearSelection(e) { - // Ta funkcja jest teraz wywoływana tylko gdy kliknięto na tło bez modyfikatorów. - // Domyślna akcja: wyczyść zaznaczenie i rozpocznij panoramowanie. - if (!this.interaction.isCtrlPressed) { + startPanning(e, clearSelection = true) { + // Unified panning method - can optionally clear selection + if (clearSelection && !this.interaction.isCtrlPressed) { this.canvas.canvasSelection.updateSelection([]); } this.interaction.mode = 'panning'; @@ -642,13 +678,6 @@ export class CanvasInteractions { this.canvas.render(); this.canvas.saveState(); } - startPanning(e) { - if (!this.interaction.isCtrlPressed) { - this.canvas.canvasSelection.updateSelection([]); - } - this.interaction.mode = 'panning'; - this.interaction.panStart = { x: e.clientX, y: e.clientY }; - } panViewport(e) { const dx = e.clientX - this.interaction.panStart.x; const dy = e.clientY - this.interaction.panStart.y; @@ -709,7 +738,7 @@ export class CanvasInteractions { mouseY = Math.abs(mouseY - snapToGrid(mouseY)) < snapThreshold ? snapToGrid(mouseY) : mouseY; } const o = this.interaction.transformOrigin; - if (o.rotation === undefined || o.width === undefined || o.height === undefined || o.centerX === undefined || o.centerY === undefined) + if (!o) return; const handle = this.interaction.resizeHandle; const anchor = this.interaction.resizeAnchor; @@ -856,7 +885,7 @@ export class CanvasInteractions { if (!layer) return; const o = this.interaction.transformOrigin; - if (o.rotation === undefined || o.centerX === undefined || o.centerY === undefined) + if (!o) return; const startAngle = Math.atan2(this.interaction.dragStart.y - o.centerY, this.interaction.dragStart.x - o.centerX); const currentAngle = Math.atan2(worldCoords.y - o.centerY, worldCoords.x - o.centerX); diff --git a/src/CanvasInteractions.ts b/src/CanvasInteractions.ts index 6fb273f..68fca65 100644 --- a/src/CanvasInteractions.ts +++ b/src/CanvasInteractions.ts @@ -10,15 +10,29 @@ interface MouseCoordinates { view: Point; } +interface TransformOrigin { + x: number; + y: number; + width: number; + height: number; + rotation: number; + centerX: number; + centerY: number; + originalWidth?: number; + originalHeight?: number; + cropBounds?: { x: number; y: number; width: number; height: number }; +} + interface InteractionState { mode: 'none' | 'panning' | 'dragging' | 'resizing' | 'rotating' | 'drawingMask' | 'resizingCanvas' | 'movingCanvas' | 'potential-drag' | 'drawingShape'; panStart: Point; dragStart: Point; - transformOrigin: Partial & { centerX?: number, centerY?: number }; + transformOrigin: TransformOrigin | null; resizeHandle: string | null; resizeAnchor: Point; canvasResizeStart: Point; isCtrlPressed: boolean; + isMetaPressed: boolean; isAltPressed: boolean; isShiftPressed: boolean; isSPressed: boolean; @@ -35,17 +49,35 @@ export class CanvasInteractions { public interaction: InteractionState; private originalLayerPositions: Map; + // Bound event handlers to enable proper removeEventListener and avoid leaks + private onMouseDown = (e: MouseEvent) => this.handleMouseDown(e); + private onMouseMove = (e: MouseEvent) => this.handleMouseMove(e); + private onMouseUp = (e: MouseEvent) => this.handleMouseUp(e); + private onMouseEnter = (e: MouseEvent) => { this.canvas.isMouseOver = true; this.handleMouseEnter(e); }; + private onMouseLeave = (e: MouseEvent) => { this.canvas.isMouseOver = false; this.handleMouseLeave(e); }; + private onWheel = (e: WheelEvent) => this.handleWheel(e); + private onKeyDown = (e: KeyboardEvent) => this.handleKeyDown(e); + private onKeyUp = (e: KeyboardEvent) => this.handleKeyUp(e); + private onDragOver = (e: DragEvent) => this.handleDragOver(e); + private onDragEnter = (e: DragEvent) => this.handleDragEnter(e); + private onDragLeave = (e: DragEvent) => this.handleDragLeave(e); + private onDrop = (e: DragEvent) => { this.handleDrop(e); }; + private onContextMenu = (e: MouseEvent) => this.handleContextMenu(e); + private onBlur = () => this.handleBlur(); + private onPaste = (e: ClipboardEvent) => this.handlePasteEvent(e); + constructor(canvas: Canvas) { this.canvas = canvas; this.interaction = { mode: 'none', panStart: { x: 0, y: 0 }, dragStart: { x: 0, y: 0 }, - transformOrigin: {}, + transformOrigin: null, resizeHandle: null, resizeAnchor: { x: 0, y: 0 }, canvasResizeStart: { x: 0, y: 0 }, isCtrlPressed: false, + isMetaPressed: false, isAltPressed: false, isShiftPressed: false, isSPressed: false, @@ -74,7 +106,6 @@ export class CanvasInteractions { } private performZoomOperation(worldCoords: Point, zoomFactor: number): void { - const rect = this.canvas.canvas.getBoundingClientRect(); const mouseBufferX = (worldCoords.x - this.canvas.viewport.x) * this.canvas.viewport.zoom; const mouseBufferY = (worldCoords.y - this.canvas.viewport.y) * this.canvas.viewport.zoom; @@ -106,34 +137,49 @@ export class CanvasInteractions { } setupEventListeners(): void { - this.canvas.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this) as EventListener); - this.canvas.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this) as EventListener); - this.canvas.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this) as EventListener); - this.canvas.canvas.addEventListener('mouseleave', this.handleMouseLeave.bind(this) as EventListener); - this.canvas.canvas.addEventListener('wheel', this.handleWheel.bind(this) as EventListener, { passive: false }); - this.canvas.canvas.addEventListener('keydown', this.handleKeyDown.bind(this) as EventListener); - this.canvas.canvas.addEventListener('keyup', this.handleKeyUp.bind(this) as EventListener); + this.canvas.canvas.addEventListener('mousedown', this.onMouseDown as EventListener); + this.canvas.canvas.addEventListener('mousemove', this.onMouseMove as EventListener); + this.canvas.canvas.addEventListener('mouseup', this.onMouseUp as EventListener); + this.canvas.canvas.addEventListener('wheel', this.onWheel as EventListener, { passive: false }); + this.canvas.canvas.addEventListener('keydown', this.onKeyDown as EventListener); + this.canvas.canvas.addEventListener('keyup', this.onKeyUp as EventListener); // Add a blur event listener to the window to reset key states - window.addEventListener('blur', this.handleBlur.bind(this)); + window.addEventListener('blur', this.onBlur); - document.addEventListener('paste', this.handlePasteEvent.bind(this)); + document.addEventListener('paste', this.onPaste as unknown as EventListener); - this.canvas.canvas.addEventListener('mouseenter', (e: MouseEvent) => { - this.canvas.isMouseOver = true; - this.handleMouseEnter(e); - }); - this.canvas.canvas.addEventListener('mouseleave', (e: MouseEvent) => { - this.canvas.isMouseOver = false; - this.handleMouseLeave(e); - }); + this.canvas.canvas.addEventListener('mouseenter', this.onMouseEnter as EventListener); + this.canvas.canvas.addEventListener('mouseleave', this.onMouseLeave as EventListener); - this.canvas.canvas.addEventListener('dragover', this.handleDragOver.bind(this) as EventListener); - this.canvas.canvas.addEventListener('dragenter', this.handleDragEnter.bind(this) as EventListener); - this.canvas.canvas.addEventListener('dragleave', this.handleDragLeave.bind(this) as EventListener); - this.canvas.canvas.addEventListener('drop', this.handleDrop.bind(this) as unknown as EventListener); + this.canvas.canvas.addEventListener('dragover', this.onDragOver as EventListener); + this.canvas.canvas.addEventListener('dragenter', this.onDragEnter as EventListener); + this.canvas.canvas.addEventListener('dragleave', this.onDragLeave as EventListener); + this.canvas.canvas.addEventListener('drop', this.onDrop as unknown as EventListener); - this.canvas.canvas.addEventListener('contextmenu', this.handleContextMenu.bind(this) as EventListener); + this.canvas.canvas.addEventListener('contextmenu', this.onContextMenu as EventListener); + } + + teardownEventListeners(): void { + this.canvas.canvas.removeEventListener('mousedown', this.onMouseDown as EventListener); + this.canvas.canvas.removeEventListener('mousemove', this.onMouseMove as EventListener); + this.canvas.canvas.removeEventListener('mouseup', this.onMouseUp as EventListener); + this.canvas.canvas.removeEventListener('wheel', this.onWheel as EventListener); + this.canvas.canvas.removeEventListener('keydown', this.onKeyDown as EventListener); + this.canvas.canvas.removeEventListener('keyup', this.onKeyUp as EventListener); + + window.removeEventListener('blur', this.onBlur); + document.removeEventListener('paste', this.onPaste as unknown as EventListener); + + this.canvas.canvas.removeEventListener('mouseenter', this.onMouseEnter as EventListener); + this.canvas.canvas.removeEventListener('mouseleave', this.onMouseLeave as EventListener); + + this.canvas.canvas.removeEventListener('dragover', this.onDragOver as EventListener); + this.canvas.canvas.removeEventListener('dragenter', this.onDragEnter as EventListener); + this.canvas.canvas.removeEventListener('dragleave', this.onDragLeave as EventListener); + this.canvas.canvas.removeEventListener('drop', this.onDrop as unknown as EventListener); + + this.canvas.canvas.removeEventListener('contextmenu', this.onContextMenu as EventListener); } /** @@ -222,7 +268,7 @@ export class CanvasInteractions { } return; } - if (e.button !== 0) { // Środkowy przycisk + if (e.button === 1) { // Środkowy przycisk this.startPanning(e); return; } @@ -241,7 +287,7 @@ export class CanvasInteractions { } // 4. Domyślna akcja na tle (lewy przycisk bez modyfikatorów) - this.startPanningOrClearSelection(e); + this.startPanning(e, true); // clearSelection = true } handleMouseMove(e: MouseEvent): void { @@ -462,7 +508,7 @@ export class CanvasInteractions { } private calculateGridBasedScaling(oldHeight: number, deltaY: number): number { - const gridSize = 64; + const gridSize = 64; // Grid size - could be made configurable in the future const direction = deltaY > 0 ? -1 : 1; let targetHeight; @@ -487,6 +533,7 @@ export class CanvasInteractions { handleKeyDown(e: KeyboardEvent): void { if (e.key === 'Control') this.interaction.isCtrlPressed = true; + if (e.key === 'Meta') this.interaction.isMetaPressed = true; if (e.key === 'Shift') this.interaction.isShiftPressed = true; if (e.key === 'Alt') { this.interaction.isAltPressed = true; @@ -571,6 +618,7 @@ export class CanvasInteractions { handleKeyUp(e: KeyboardEvent): void { if (e.key === 'Control') this.interaction.isCtrlPressed = false; + if (e.key === 'Meta') this.interaction.isMetaPressed = false; if (e.key === 'Shift') this.interaction.isShiftPressed = false; if (e.key === 'Alt') this.interaction.isAltPressed = false; if (e.key.toLowerCase() === 's') this.interaction.isSPressed = false; @@ -590,6 +638,7 @@ export class CanvasInteractions { handleBlur(): void { log.debug('Window lost focus, resetting key states.'); this.interaction.isCtrlPressed = false; + this.interaction.isMetaPressed = false; this.interaction.isAltPressed = false; this.interaction.isShiftPressed = false; this.interaction.isSPressed = false; @@ -615,6 +664,12 @@ export class CanvasInteractions { } updateCursor(worldCoords: Point): void { + // If actively rotating, show grabbing cursor + if (this.interaction.mode === 'rotating') { + this.canvas.canvas.style.cursor = 'grabbing'; + return; + } + const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y); if (transformTarget) { @@ -663,7 +718,8 @@ export class CanvasInteractions { prepareForDrag(layer: Layer, worldCoords: Point): void { // Zaktualizuj zaznaczenie, ale nie zapisuj stanu - if (this.interaction.isCtrlPressed) { + // Support both Ctrl (Windows/Linux) and Cmd (macOS) for multi-selection + if (this.interaction.isCtrlPressed || this.interaction.isMetaPressed) { const index = this.canvas.canvasSelection.selectedLayers.indexOf(layer); if (index === -1) { this.canvas.canvasSelection.updateSelection([...this.canvas.canvasSelection.selectedLayers, layer]); @@ -681,14 +737,13 @@ export class CanvasInteractions { this.interaction.dragStart = {...worldCoords}; } - startPanningOrClearSelection(e: MouseEvent): void { - // Ta funkcja jest teraz wywoływana tylko gdy kliknięto na tło bez modyfikatorów. - // Domyślna akcja: wyczyść zaznaczenie i rozpocznij panoramowanie. - if (!this.interaction.isCtrlPressed) { + startPanning(e: MouseEvent, clearSelection: boolean = true): void { + // Unified panning method - can optionally clear selection + if (clearSelection && !this.interaction.isCtrlPressed) { this.canvas.canvasSelection.updateSelection([]); } this.interaction.mode = 'panning'; - this.interaction.panStart = {x: e.clientX, y: e.clientY}; + this.interaction.panStart = { x: e.clientX, y: e.clientY }; } startCanvasResize(worldCoords: Point): void { @@ -743,14 +798,6 @@ export class CanvasInteractions { this.canvas.saveState(); } - startPanning(e: MouseEvent): void { - if (!this.interaction.isCtrlPressed) { - this.canvas.canvasSelection.updateSelection([]); - } - this.interaction.mode = 'panning'; - this.interaction.panStart = { x: e.clientX, y: e.clientY }; - } - panViewport(e: MouseEvent): void { const dx = e.clientX - this.interaction.panStart.x; const dy = e.clientY - this.interaction.panStart.y; @@ -818,7 +865,7 @@ export class CanvasInteractions { } const o = this.interaction.transformOrigin; - if (o.rotation === undefined || o.width === undefined || o.height === undefined || o.centerX === undefined || o.centerY === undefined) return; + if (!o) return; const handle = this.interaction.resizeHandle; const anchor = this.interaction.resizeAnchor; @@ -974,7 +1021,7 @@ export class CanvasInteractions { if (!layer) return; const o = this.interaction.transformOrigin; - if (o.rotation === undefined || o.centerX === undefined || o.centerY === undefined) return; + if (!o) return; const startAngle = Math.atan2(this.interaction.dragStart.y - o.centerY, this.interaction.dragStart.x - o.centerX); const currentAngle = Math.atan2(worldCoords.y - o.centerY, worldCoords.x - o.centerX); let angleDiff = (currentAngle - startAngle) * 180 / Math.PI;