/* eslint-disable @typescript-eslint/no-unused-vars */ // noinspection JSUnusedLocalSymbols import { Point, type Vector } from '@flatten-js/core'; import type { DrawController } from '../../../src/drawControllers/DrawController'; import { triggerReactUpdate } from '../../../src/state'; import { StateVariable } from '../../../src/helpers/undo-stack'; import { MOUSE_ZOOM_MULTIPLIER } from '../../../src/App.consts'; import { mapNumberRange } from '../../../src/helpers/map-number-range'; /** * Screen coordinate system: * 0, 0 X * +----------> * | * | * | * Y v * * * World coordinate system: * Y ^ * | * | * | * +----------> * 0, 0 X * * To convert between the 2 coordinate systems, you need the screenOffset and screenScale */ export class ScreenCanvasDrawController implements DrawController { private screenOffset: Point = new Point(0, 0); private screenScale = 1; private worldMouseLocation: Point; constructor( private context: CanvasRenderingContext2D | null, private canvasSize: Point ) { this.worldMouseLocation = this.targetToWorld(new Point(canvasSize.x / 2, canvasSize.y / 2)); this.setScreenOffset(new Point(0, 0)); // User expects mathematical coordinates, where y axis goes up, but canvas y axis goes down } public getCanvasSize() { return this.canvasSize; } public getScreenScale() { return this.screenScale; } public setScreenScale(newScreenScale: number) { this.screenScale = newScreenScale; triggerReactUpdate(StateVariable.screenZoom); } public getScreenOffset() { return this.screenOffset; } public setScreenOffset(newScreenOffset: Point) { this.screenOffset = newScreenOffset; triggerReactUpdate(StateVariable.screenOffset); } public setScreenMouseLocation(newScreenMouseLocation: Point): void { this.worldMouseLocation = this.targetToWorld(newScreenMouseLocation); triggerReactUpdate(StateVariable.screenMouseLocation); } public getWorldMouseLocation(): Point { return this.worldMouseLocation; } public getScreenMouseLocation(): Point { return this.worldToTarget(this.worldMouseLocation); } public panScreen(screenOffsetX: number, screenOffsetY: number) { this.screenOffset = new Point( this.screenOffset.x - screenOffsetX / this.screenScale, this.screenOffset.y - screenOffsetY / this.screenScale ); } /** * This function takes the deltaY from the mouse wheel event and zooms the screen in or out * The location of the mouse in world space is preserved * @param deltaY */ public zoomScreen(deltaY: number) { const worldMouseLocationBeforeZoom = this.getWorldMouseLocation(); const newScreenScale = this.getScreenScale() * (1 - MOUSE_ZOOM_MULTIPLIER * (deltaY / Math.abs(deltaY))); this.setScreenScale(newScreenScale); // now get the location of the cursor in world space again // It will have changed because the scale has changed, // but we can offset our world now to fix the zoom location in screen space, // because we know how much it changed laterally between the two spatial scales. const worldMouseLocationAfterZoom = this.getWorldMouseLocation(); // Adjust the screen offset to maintain the cursor position this.screenOffset = new Point( this.screenOffset.x + (worldMouseLocationBeforeZoom.x - worldMouseLocationAfterZoom.x), this.screenOffset.y + (worldMouseLocationBeforeZoom.y - worldMouseLocationAfterZoom.y) ); } /** * Convert coordinates from World Space --> Screen Space */ public worldToTarget(worldCoordinate: Point): Point { return new Point( mapNumberRange( worldCoordinate.x, this.screenOffset.x, this.screenOffset.x + this.canvasSize.x / this.screenScale, 0, this.canvasSize.x ), mapNumberRange( worldCoordinate.y, this.screenOffset.y + this.canvasSize.y / this.screenScale, // inverted since world origin is bottom left and screen origin is top left this.screenOffset.y, 0, this.canvasSize.y ) ); } public worldsToTargets(worldCoordinates: Point[]): Point[] { return worldCoordinates.map(this.worldToTarget.bind(this)); } /** * Convert coordinates from Screen Space --> World Space * (0, 0) (1920, 0) * * (0, 1080) (1920, 1080) * * convert to * * (0, 1080) (1920, 1080) * * (0, 0) (1920, 0) */ public targetToWorld(screenCoordinate: Point): Point { // map the screen coordinate to the world coordinate based on this.getScreenOffset() and the this.getScreenScale() return new Point( mapNumberRange( screenCoordinate.x, 0, this.canvasSize.x, this.screenOffset.x, this.screenOffset.x + this.canvasSize.x / this.screenScale ), mapNumberRange( screenCoordinate.y, 0, this.canvasSize.y, this.screenOffset.y, this.screenOffset.y + this.canvasSize.y / this.screenScale ) ); } public targetsToWorlds(screenCoordinates: Point[]): Point[] { return screenCoordinates.map(this.targetToWorld.bind(this)); } public setLineStyles( isHighlighted: boolean, isSelected: boolean, color: string, lineWidth: number, dash: number[] = [] ) {} public setFillStyles(fillColor: string) {} public clear() {} /** * Draws a line from startPoint to endPoint and auto converts to screen space first * @param worldStartPoint * @param worldEndPoint */ public drawLine(worldStartPoint: Point, worldEndPoint: Point): void {} /** * Needs to be public to draw UI that is zoom independent, like snap point indicators * @param screenStartPoint * @param screenEndPoint */ public drawLineScreen(screenStartPoint: Point, screenEndPoint: Point): void {} /** * Draw an arc (segment of a circle) or a circle if startAngle = 0 and endAngle = 2PI * @param centerPoint * @param radius * @param startAngle * @param endAngle * @param counterClockWise */ public drawArc( centerPoint: Point, radius: number, startAngle: number, endAngle: number, counterClockWise: boolean ) {} public drawArcScreen( screenCenterPoint: Point, screenRadius: number, startAngle: number, endAngle: number, counterClockWise: boolean ) {} /** * Draw some text at the base location * The direction vector points from the bottom of the first letter towards the bottom of the last letter indicate the rotation of the text * @param label * @param label * @param basePoint * @param options */ public drawText( label: string, basePoint: Point, options: Partial<{ textDirection?: Vector; textAlign: 'left' | 'center' | 'right'; textColor: string; fontSize: number; fontFamily: string; }> = {} ): void {} /** * Draw some text at the base location * The direction vector points from the bottom of the first letter towards the bottom of the last letter indicate the rotation of the text * @param label * @param basePoint * @param options */ public drawTextScreen( label: string, basePoint: Point, options: Partial<{ textDirection?: Vector; textAlign: 'left' | 'center' | 'right'; textColor: string; fontSize: number; fontFamily: string; }> = {} ): void {} /** * Draw an image to the canvas using world coordinates * @param imageElement * @param xMin * @param yMin * @param width * @param height * @param angle */ public drawImage( imageElement: HTMLImageElement, xMin: number, yMin: number, width: number, height: number, angle: number ): void {} public fillRect(xMin: number, yMin: number, width: number, height: number, color: string) {} /** * Fill rectangle with color, but interpret the provided coordinates as screen coordinates * @param xMin * @param yMin * @param width * @param height * @param color */ public fillRectScreen(xMin: number, yMin: number, width: number, height: number, color: string) {} /** * Fill polygon with color * @param points */ public fillPolygon(...points: Point[]) {} }