import { Point } from '@svgdotjs/svg.js' import { Matrix } from '@svgdotjs/svg.js' import { on, off, Box } from '@svgdotjs/svg.js' const getCoordsFromEvent = (ev) => { if (ev.changedTouches) { ev = ev.changedTouches[0] } return { x: ev.clientX, y: ev.clientY } } const maxBoxFromPoints = (points) => { let x = Infinity let y = Infinity let x2 = -Infinity let y2 = -Infinity for (let i = 0; i < points.length; i++) { const p = points[i] x = Math.min(x, p[0]) y = Math.min(y, p[1]) x2 = Math.max(x2, p[0]) y2 = Math.max(y2, p[1]) } return new Box(x, y, x2 - x, y2 - y) } function scaleBox(box, origin, scale) { const points = [ [box.x, box.y], [box.x + box.width, box.y], [box.x + box.width, box.y + box.height], [box.x, box.y + box.height], ] const newPoints = points.map(([x, y]) => { // Translate to origin const translatedX = x - origin[0] const translatedY = y - origin[1] // Scale const scaledX = translatedX * scale const scaledY = translatedY * scale // Translate back return [scaledX + origin[0], scaledY + origin[1]] }) return maxBoxFromPoints(newPoints) } export class ResizeHandler { constructor(el) { this.el = el el.remember('_ResizeHandler', this) this.lastCoordinates = null this.eventType = '' this.lastEvent = null this.handleResize = this.handleResize.bind(this) this.resize = this.resize.bind(this) this.endResize = this.endResize.bind(this) this.rotate = this.rotate.bind(this) this.movePoint = this.movePoint.bind(this) } active(value, options) { this.preserveAspectRatio = options.preserveAspectRatio ?? false this.aroundCenter = options.aroundCenter ?? false this.grid = options.grid ?? 0 this.degree = options.degree ?? 0 // remove all resize events this.el.off('.resize') if (!value) return this.el.on( [ 'lt.resize', 'rt.resize', 'rb.resize', 'lb.resize', 't.resize', 'r.resize', 'b.resize', 'l.resize', 'rot.resize', 'point.resize', ], this.handleResize ) // in case the options were changed mid-resize, // we have to replay the last event to see the immediate effect of the option change if (this.lastEvent) { if (this.eventType === 'rot') { this.rotate(this.lastEvent) } else if (this.eventType === 'point') { this.movePoint(this.lastEvent) } else { this.resize(this.lastEvent) } } } // This is called when a user clicks on one of the resize points handleResize(e) { this.eventType = e.type const { event, index, points } = e.detail const isMouse = !event.type.indexOf('mouse') // Check for left button if (isMouse && (event.which || event.buttons) !== 1) { return } // Fire beforedrag event if (this.el.dispatch('beforeresize', { event: e, handler: this }).defaultPrevented) { return } this.box = this.el.bbox() this.startPoint = this.el.point(getCoordsFromEvent(event)) this.index = index this.points = points.slice() // We consider the resize done, when a touch is canceled, too const eventMove = (isMouse ? 'mousemove' : 'touchmove') + '.resize' const eventEnd = (isMouse ? 'mouseup' : 'touchcancel.resize touchend') + '.resize' if (e.type === 'point') { on(window, eventMove, this.movePoint) } else if (e.type === 'rot') { on(window, eventMove, this.rotate) } else { on(window, eventMove, this.resize) } on(window, eventEnd, this.endResize) } resize(e) { this.lastEvent = e const endPoint = this.snapToGrid(this.el.point(getCoordsFromEvent(e))) let dx = endPoint.x - this.startPoint.x let dy = endPoint.y - this.startPoint.y if (this.preserveAspectRatio && this.aroundCenter) { dx *= 2 dy *= 2 } const x = this.box.x + dx const y = this.box.y + dy const x2 = this.box.x2 + dx const y2 = this.box.y2 + dy let box = new Box(this.box) if (this.eventType.includes('l')) { box.x = Math.min(x, this.box.x2) box.x2 = Math.max(x, this.box.x2) } if (this.eventType.includes('r')) { box.x = Math.min(x2, this.box.x) box.x2 = Math.max(x2, this.box.x) } if (this.eventType.includes('t')) { box.y = Math.min(y, this.box.y2) box.y2 = Math.max(y, this.box.y2) } if (this.eventType.includes('b')) { box.y = Math.min(y2, this.box.y) box.y2 = Math.max(y2, this.box.y) } box.width = box.x2 - box.x box.height = box.y2 - box.y // after figuring out the resulting box, // we have to check if the aspect ratio should be preserved // if so, we have to find the correct scaling factor and scale the box around a fixed point (usually the opposite of the handle) // in case aroundCenter is active, the fixed point is the center of the box if (this.preserveAspectRatio) { const scaleX = box.width / this.box.width const scaleY = box.height / this.box.height const order = ['lt', 't', 'rt', 'r', 'rb', 'b', 'lb', 'l'] const origin = (order.indexOf(this.eventType) + 4) % order.length const constantPoint = this.aroundCenter ? [this.box.cx, this.box.cy] : this.points[origin] let scale = this.eventType.includes('t') || this.eventType.includes('b') ? scaleY : scaleX scale = this.eventType.length === 2 ? Math.max(scaleX, scaleY) : scale box = scaleBox(this.box, constantPoint, scale) } if ( this.el.dispatch('resize', { box: new Box(box), angle: 0, eventType: this.eventType, event: e, handler: this, }).defaultPrevented ) { return } this.el.size(box.width, box.height).move(box.x, box.y) } movePoint(e) { this.lastEvent = e const { x, y } = this.snapToGrid(this.el.point(getCoordsFromEvent(e))) const pointArr = this.el.array().slice() pointArr[this.index] = [x, y] if ( this.el.dispatch('resize', { box: maxBoxFromPoints(pointArr), angle: 0, eventType: this.eventType, event: e, handler: this, }).defaultPrevented ) { return } this.el.plot(pointArr) } rotate(e) { this.lastEvent = e const startPoint = this.startPoint const endPoint = this.el.point(getCoordsFromEvent(e)) const { cx, cy } = this.box const dx1 = startPoint.x - cx const dy1 = startPoint.y - cy const dx2 = endPoint.x - cx const dy2 = endPoint.y - cy const c = Math.sqrt(dx1 * dx1 + dy1 * dy1) * Math.sqrt(dx2 * dx2 + dy2 * dy2) if (c === 0) { return } let angle = (Math.acos((dx1 * dx2 + dy1 * dy2) / c) / Math.PI) * 180 // catches 0 angle and NaN angle that are zero as well (but numerically instable) if (!angle) return if (endPoint.x < startPoint.x) { angle = -angle } const matrix = new Matrix(this.el) const { x: ox, y: oy } = new Point(cx, cy).transformO(matrix) const { rotate } = matrix.decompose() const resultAngle = this.snapToAngle(rotate + angle) - rotate if ( this.el.dispatch('resize', { box: this.box, angle: resultAngle, eventType: this.eventType, event: e, handler: this, }).defaultPrevented ) { return } this.el.transform(matrix.rotateO(resultAngle, ox, oy)) } endResize(ev) { // Unbind resize and end events to window if (this.eventType !== 'rot' && this.eventType !== 'point') { this.resize(ev) } this.lastEvent = null this.eventType = '' off(window, 'mousemove.resize touchmove.resize') off(window, 'mouseup.resize touchend.resize') } snapToGrid(point) { if (this.grid) { point.x = Math.round(point.x / this.grid) * this.grid point.y = Math.round(point.y / this.grid) * this.grid } return point } snapToAngle(angle) { if (this.degree) { angle = Math.round(angle / this.degree) * this.degree } return angle } }