import Konva from "konva"; import { IconRotate } from "@humansignal/icons"; const EVENTS_NAME = "tr-konva"; // Copies of local methods from Konva's original implementation function getCenter(shape) { return { x: shape.x + (shape.width / 2) * Math.cos(shape.rotation) + (shape.height / 2) * Math.sin(-shape.rotation), y: shape.y + (shape.height / 2) * Math.cos(shape.rotation) + (shape.width / 2) * Math.sin(shape.rotation), }; } function rotateAroundPoint(shape, angleRad, point) { const x = point.x + (shape.x - point.x) * Math.cos(angleRad) - (shape.y - point.y) * Math.sin(angleRad); const y = point.y + (shape.x - point.x) * Math.sin(angleRad) + (shape.y - point.y) * Math.cos(angleRad); return { ...shape, rotation: shape.rotation + angleRad, x, y, }; } function rotateAroundCenter(shape, deltaRad) { const center = getCenter(shape); return rotateAroundPoint(shape, deltaRad, center); } function getSnap(snaps, newRotationRad, tol) { let snapped = newRotationRad; for (let i = 0; i < snaps.length; i++) { const angle = Konva.getAngle(snaps[i]); const absDiff = Math.abs(angle - newRotationRad) % (Math.PI * 2); const dif = Math.min(absDiff, Math.PI * 2 - absDiff); if (dif < tol) { snapped = angle; } } return snapped; } class LSTransformer extends Konva.Transformer { isMouseOver = false; isMouseDown = false; initialRotationDelta = 0; origin; constructor(props) { super(props); if (props.rotateEnabled) this.createRotateButton(); } // Here starts the configuration of the rotation tool createRotateButton() { const rotateList = this.refreshRotationList(); for (const obj in rotateList) { const rotateButton = new Konva.Circle({ radius: 20, name: `rotate-${obj}`, dragDistance: 0, draggable: true, x: rotateList[obj].x, y: rotateList[obj].y, }); this.add(rotateButton); rotateButton.moveToBottom(); // to not overlap other controls rotateButton.on("mousedown touchstart", this.handleMouseDown); rotateButton.on("mouseover", () => { if (!this.isMouseDown) { this.getStage().content.style.cursor = `url(${IconRotate}) 16 16, pointer`; } this.isMouseOver = true; }); rotateButton.on("mouseout", () => { this.isMouseOver = false; if (!this.isMouseDown) { this.getStage().content.style.cursor = ""; } }); rotateButton.on("dragstart", (e) => { const anchorNode = this.findOne(`.${this._movingAnchorName}`); anchorNode.stopDrag(); e.cancelBubble = true; }); rotateButton.on("dragend", (e) => { e.cancelBubble = true; }); } } handleMouseDown = (e) => { const stage = this.getStage(); const pp = stage?.getPointerPosition(); if (!stage || !pp) return; const shape = this._getNodeRect(); const origin = getCenter(shape); const dx = pp.x - origin.x; const dy = pp.y - origin.y; const azimuth = Math.PI / 2 - Math.atan2(-dy, dx); stage.content.style.cursor = `url(${IconRotate}) 16 16, pointer`; this.isMouseDown = true; this._movingAnchorName = e.target.name().split(" ")[0]; // we save angle between vector to current pointer and shape rotation // and we keep this angle the same during mousemove by changing shape rotation this.initialRotationDelta = azimuth - shape.rotation; this.origin = origin; if (window) { window.addEventListener("mousemove", this.handleMouseMove); window.addEventListener("touchmove", this.handleMouseMove); window.addEventListener("mouseup", this.handleMouseUp, true); window.addEventListener("touchend", this.handleMouseUp, true); } this._fire("transformstart", { evt: e, target: this.getNode() }); this._nodes.forEach((target) => { target._fire("transformstart", { evt: e, target }); }); }; handleMouseUp = (e) => { this.isMouseDown = false; this.origin = undefined; if (!this.isMouseOver) { this.getStage().content.style.cursor = ""; } if (window) { window.removeEventListener("mousemove", this.handleMouseMove); window.removeEventListener("touchmove", this.handleMouseMove); window.removeEventListener("mouseup", this.handleMouseUp, true); window.removeEventListener("touchend", this.handleMouseUp, true); } const node = this.getNode(); this._fire("transformend", { evt: e, target: node }); if (node) { this._nodes.forEach((target) => { target._fire("transformend", { evt: e, target }); }); } this._movingAnchorName = ""; }; handleMouseMove = (e) => { const stage = this.getStage(); if (!this.isMouseDown || !this.origin || !stage) return; // register coordinates outside the stage into the stage stage.setPointersPositions(e); const pp = stage.getPointerPosition(); const shape = this._getNodeRect(); if (!pp) return; const dx = pp.x - this.origin.x; const dy = pp.y - this.origin.y; // @todo why such signs? but they produce correct angles in every quadrant const azimuth = Math.PI / 2 - Math.atan2(-dy, dx); const newRotation = azimuth - this.initialRotationDelta; // in case we have rotation snap enabled const tol = Konva.getAngle(this.rotationSnapTolerance()); const snappedRot = getSnap(this.rotationSnaps(), newRotation, tol); const diff = snappedRot - shape.rotation; const rotated = rotateAroundCenter(shape, diff); this._fitNodesInto(rotated, e); }; refreshRotationList() { return { "top-left": { x: 0, y: 0, }, "top-right": { x: this.getWidth(), y: 0, }, "bottom-left": { x: 0, y: this.getHeight(), }, "bottom-right": { x: this.getWidth(), y: this.getHeight(), }, }; } // Here starts override methods from LSTransform get _outerBack() { return this.getStage()?.findOne(this.attrs.backSelector); } setNodes(nodes = []) { super.setNodes(nodes); if (this._outerBack) { this._proxyDrag(this._outerBack); } return this; } detach() { this._outerBack?.off(`.${EVENTS_NAME}`); super.detach(); } update() { this.refreshRotationList(); const { x, y, width, height } = this._getNodeRect(); const rotation = this.rotation(); const outerBack = this._outerBack; const rotateList = this.refreshRotationList(); for (const obj in rotateList) { const anchorNode = this.findOne(`.rotate-${obj}`); if (anchorNode) { anchorNode .setAttrs({ x: rotateList[obj].x, y: rotateList[obj].y, }) .getLayer() .batchDraw(); } } super.update(); if (outerBack) { const backAbsScale = this.getAbsoluteScale(); const trAbsScale = outerBack.getAbsoluteScale(); const scale = { x: backAbsScale.x / trAbsScale.x, y: backAbsScale.y / trAbsScale.y, }; outerBack .setAttrs({ x: (x - this.getStage().getAttr("x")) * scale.x, y: (y - this.getStage().getAttr("y")) * scale.y, width: width * scale.x, height: height * scale.y, rotation, }) .getLayer() .batchDraw(); } } } Konva.LSTransformer = LSTransformer; export default "LSTransformer";