UNPKG

js-draw

Version:

Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.

146 lines (145 loc) 6.25 kB
import { Mat33, Vec2 } from '@js-draw/math'; import Viewport from '../../Viewport.mjs'; import { ResizeMode } from './types.mjs'; export class DragTransformer { constructor(editor, selection) { this.editor = editor; this.selection = selection; } onDragStart(startPoint) { this.selection.setTransform(Mat33.identity); this.dragStartPoint = startPoint; } onDragUpdate(canvasPos) { const delta = this.editor.viewport.roundPoint(canvasPos.minus(this.dragStartPoint)); this.selection.setTransform(Mat33.translation(delta)); } onDragEnd() { return this.selection.finalizeTransform(); } } export class ResizeTransformer { constructor(editor, selection) { this.editor = editor; this.selection = selection; this.mode = ResizeMode.Both; } onDragStart(startPoint, mode) { this.selection.setTransform(Mat33.identity); this.mode = mode; this.dragStartPoint = startPoint; this.computeOriginAndScaleRate(); } computeOriginAndScaleRate() { // Store the index of the furthest corner from startPoint. We'll use that // to determine where the transform considers (0, 0) (where we scale from). const selectionRect = this.selection.preTransformRegion; const selectionBoxCorners = selectionRect.corners; let largestDistSquared = 0; for (let i = 0; i < selectionBoxCorners.length; i++) { const currentCorner = selectionBoxCorners[i]; const distSquaredToCurrent = this.dragStartPoint.minus(currentCorner).magnitudeSquared(); if (distSquaredToCurrent > largestDistSquared) { largestDistSquared = distSquaredToCurrent; this.transformOrigin = currentCorner; } } // Determine whether moving the mouse to the right increases or decreases the width. let widthScaleRate = 1; let heightScaleRate = 1; if (this.transformOrigin.x > selectionRect.center.x) { widthScaleRate = -1; } if (this.transformOrigin.y > selectionRect.center.y) { heightScaleRate = -1; } this.scaleRate = Vec2.of(widthScaleRate, heightScaleRate); } onDragUpdate(canvasPos) { const canvasDelta = canvasPos.minus(this.dragStartPoint); const origWidth = this.selection.preTransformRegion.width; const origHeight = this.selection.preTransformRegion.height; let scale = Vec2.of(1, 1); if (this.mode === ResizeMode.HorizontalOnly) { const newWidth = origWidth + canvasDelta.x * this.scaleRate.x; scale = Vec2.of(newWidth / origWidth, scale.y); } if (this.mode === ResizeMode.VerticalOnly) { const newHeight = origHeight + canvasDelta.y * this.scaleRate.y; scale = Vec2.of(scale.x, newHeight / origHeight); } if (this.mode === ResizeMode.Both) { const delta = Math.abs(canvasDelta.x) > Math.abs(canvasDelta.y) ? canvasDelta.x : canvasDelta.y; const newWidth = origWidth + delta; scale = Vec2.of(newWidth / origWidth, newWidth / origWidth); } // Round: If this isn't done, scaling can create numbers with long decimal representations. // long decimal representations => large file sizes. scale = scale.map((component) => Viewport.roundScaleRatio(component, 2)); if (scale.x !== 0 && scale.y !== 0) { const origin = this.editor.viewport.roundPoint(this.transformOrigin); this.selection.setTransform(Mat33.scaling2D(scale, origin)); } } onDragEnd() { return this.selection.finalizeTransform(); } } export class RotateTransformer { constructor(editor, selection) { this.editor = editor; this.selection = selection; this.startAngle = 0; this.targetRotation = 0; this.maximumDistFromStart = 0; } getAngle(canvasPoint) { const selectionCenter = this.selection.preTransformRegion.center; const offset = canvasPoint.minus(selectionCenter); return offset.angle(); } roundAngle(angle) { // Round angles to the nearest 16th of a turn const roundingFactor = 16 / 2 / Math.PI; return Math.round(angle * roundingFactor) / roundingFactor; } onDragStart(startPoint) { this.startPoint = startPoint; this.selection.setTransform(Mat33.identity); this.startAngle = this.getAngle(startPoint); this.targetRotation = 0; // Used to determine whether the user clicked or not. this.maximumDistFromStart = 0; this.startTime = performance.now(); } setRotationTo(angle) { // Transform in canvas space const canvasSelCenter = this.editor.viewport.roundPoint(this.selection.preTransformRegion.center); const unrounded = Mat33.zRotation(angle); const roundedRotationTransform = unrounded.mapEntries((entry) => Viewport.roundScaleRatio(entry)); const fullRoundedTransform = Mat33.translation(canvasSelCenter) .rightMul(roundedRotationTransform) .rightMul(Mat33.translation(canvasSelCenter.times(-1))); this.selection.setTransform(fullRoundedTransform); } onDragUpdate(canvasPos) { this.targetRotation = this.roundAngle(this.getAngle(canvasPos) - this.startAngle); this.setRotationTo(this.targetRotation); const distFromStart = canvasPos.minus(this.startPoint).magnitude(); if (distFromStart > this.maximumDistFromStart) { this.maximumDistFromStart = distFromStart; } } onDragEnd() { // Anything with motion less than this is considered a click const clickThresholdDist = 10; const clickThresholdTime = 0.4; // s const dragTimeSeconds = (performance.now() - this.startTime) / 1000; if (dragTimeSeconds < clickThresholdTime && this.maximumDistFromStart < clickThresholdDist && this.targetRotation === 0) { this.setRotationTo(-Math.PI / 2); } return this.selection.finalizeTransform(); } }