UNPKG

use-konva-snapping

Version:

A lightweight React hook for adding snapping functionality to Konva.js elements. It allows objects to snap to stage borders, center, and other shapes, with customizable guidelines and snapping sensitivity.

272 lines (240 loc) 12.1 kB
import Konva from "konva"; export const useKonvaSnapping = (params) => { const defaultParams = { snapRange:params.snapRange??3, guidelineColor: params.guidelineColor ??"rgb(0, 161, 255)", guidelineDash: params.guidelineDash ?? true , showGuidelines:params.showGuidelines??true, guidelineThickness:params.guidelineThickness??1, snapToStageCenter:params.snapToStageCenter??true, snapToStageBorders:params.snapToStageBorders??true, snapToShapes:params.snapToShapes??true } const oppositeAnchors = { "top-left": "bottom-right", "top-right": "bottom-left", "bottom-right": "top-left", "bottom-left": "top-right", }; const getSnappingPoints = (e) => { const{snapToStageCenter,snapToStageBorders,snapToShapes} = defaultParams const stage = e.currentTarget.getStage(); const vertical = []; const horizontal = []; if(snapToStageCenter){ vertical.push(stage.attrs.width / 2) horizontal.push(stage.attrs.height / 2) } if(snapToStageBorders){ horizontal.push(0,stage.attrs.height) vertical.push(0,stage.attrs.width) } if(snapToShapes){ stage.children.forEach((layer) => { layer.children.forEach((obj) => { const box = obj.getClientRect(); if (obj.getType() === "Shape" && e.target !== obj && obj.getName()!=="guid-line" && !(obj instanceof Konva.Transformer)) { vertical.push(box.x, box.x + box.width, box.x + box.width / 2); horizontal.push(box.y, box.y + box.height, box.y + box.height / 2); } }); }); } return { vertical, horizontal }; }; const createLine = (Layer, isHorizontal, lineX, lineY) => { const{guidelineColor, showGuidelines, guidelineThickness, guidelineDash }=defaultParams if(!showGuidelines) return const points = isHorizontal ? [-6000, 0, 6000, 0] : [0, -6000, 0, 6000]; const line = new Konva.Line({ points, stroke: guidelineColor, strokeWidth: guidelineThickness, name: "guid-line", dash: guidelineDash?[4, 6]: [0, 0], }); Layer.add(line); line.absolutePosition({ x: lineX, y: lineY }); }; function dotProduct(v1, v2) { return v1.x * v2.x + v1.y * v2.y; } function vectorProject(a, b) { const dotAB = dotProduct(a, b); const dotBB = dotProduct(b, b); const scalar = dotAB / dotBB; return { x: scalar * b.x, y: scalar * b.y, }; } function calculateSlope(point1, point2) { const deltaX = point2.x - point1.x; // Check for a vertical line to avoid division by zero if (deltaX === 0) { throw new Error(`Slope is undefined for vertical lines (deltaX is zero). ${point2.x}, ${point1.x}`); } const deltaY = point2.y - point1.y; return deltaY / deltaX; } const handleDragging = (e) => { const Layer = e.target.parent; console.log(e.target) // Clear existing guidelines Layer.find(".guid-line").forEach((line) => line.destroy()); const { horizontal, vertical } = getSnappingPoints(e); let newPos = { x: e.target.absolutePosition().x, y: e.target.absolutePosition().y }; let guideLinesX = [] let guideLinesY = [] const {snapRange} = defaultParams // Snap vertically vertical.forEach((breakPoint) => { if (Math.abs(e.target.getClientRect().x - breakPoint) <= snapRange) { newPos.x = breakPoint + e.target.absolutePosition().x - e.target.getClientRect().x; guideLinesX.push(breakPoint) } if (Math.abs(e.target.getClientRect().x - breakPoint + e.target.getClientRect().width / 2) <= snapRange) { newPos.x = breakPoint + e.target.absolutePosition().x - e.target.getClientRect().x - e.target.getClientRect().width / 2; guideLinesX.push(breakPoint) } if (Math.abs(e.target.getClientRect().x - breakPoint + e.target.getClientRect().width) <= snapRange) { newPos.x = breakPoint + e.target.absolutePosition().x - e.target.getClientRect().x - e.target.getClientRect().width; guideLinesX.push(breakPoint) } }); e.target.absolutePosition(newPos) guideLinesX.forEach(line=>{ if (Math.round(e.target.getClientRect().x - line) ===0 || Math.round(e.target.getClientRect().x - line + e.target.getClientRect().width / 2)===0 || Math.round(e.target.getClientRect().x - line + e.target.getClientRect().width) ===0) { createLine(Layer, false, line, 0); } }) // Snap horizontally horizontal.forEach((breakPoint) => { if (Math.abs(e.target.getClientRect().y - breakPoint) <= snapRange) { newPos.y = breakPoint + e.target.absolutePosition().y - e.target.getClientRect().y; guideLinesY.push(breakPoint) } if (Math.abs(e.target.getClientRect().y - breakPoint + e.target.getClientRect().height) <= snapRange) { newPos.y = breakPoint + e.target.absolutePosition().y - e.target.getClientRect().y - e.target.getClientRect().height; guideLinesY.push(breakPoint) } if (Math.abs(e.target.getClientRect().y - breakPoint + e.target.getClientRect().height / 2) <= snapRange) { newPos.y = breakPoint + e.target.absolutePosition().y - e.target.getClientRect().y - e.target.getClientRect().height / 2; guideLinesY.push(breakPoint) } }); e.target.absolutePosition(newPos); guideLinesY.forEach(line=>{ if (Math.round(e.target.getClientRect().y - line) ===0 || Math.round(e.target.getClientRect().y - line + e.target.getClientRect().height)===0 || Math.round(e.target.getClientRect().y - line + e.target.getClientRect().height / 2) ===0) { createLine(Layer, true, 0, line); } }) }; const handleResizing = (e) => { const Layer = e.target.parent; const { snapRange } = defaultParams; let { horizontal, vertical } = getSnappingPoints(e); if(!e.currentTarget.keepRatio() || (e.currentTarget.keepRatio() && !!!oppositeAnchors[e.currentTarget._movingAnchorName])){ e.currentTarget.anchorDragBoundFunc((oldAbsPos, newAbsPos, event) => { console.log(e.target) Layer.find(".guid-line").forEach((line) => line.destroy()); let bounds = { x: newAbsPos.x, y: newAbsPos.y }; //console.log(Konva.Util.haveIntersection(e.target.getClientRect(),breakPoint)) if (e.currentTarget.getActiveAnchor() === 'rotater') return bounds for (let breakPoint of vertical) { if (Math.abs(newAbsPos.x - breakPoint) <= snapRange && Math.abs(oldAbsPos.x - breakPoint) <= snapRange + 1 ) { bounds.x = breakPoint; createLine(Layer, false, breakPoint, 0); break; } } for (let breakPoint of horizontal) { if (Math.abs(newAbsPos.y - breakPoint) <= snapRange && Math.abs(oldAbsPos.y - breakPoint) <= snapRange + 1 ) { bounds.y = breakPoint; createLine(Layer, true, 0,breakPoint); break; } } return bounds; }) }else{ e.currentTarget.anchorDragBoundFunc((oldAbsPos, newPos, event) => { Layer.find(".guid-line").forEach((line) => line.destroy()); const currentAnchorName = e.currentTarget._movingAnchorName; const oppositeAnchorName = oppositeAnchors[currentAnchorName]; const movingAnchor = e.currentTarget.findOne(`.${currentAnchorName}`); // Capture the anchor's starting absolute position: const anchorStartPosition = movingAnchor.getAbsolutePosition(); // Do nothing for the rotater anchor. if (currentAnchorName === "rotater") { return newPos; } const oppositeElement = e.currentTarget.findOne(`.${oppositeAnchorName}`); if (!oppositeElement) return newPos; const oppositePoint = oppositeElement.getAbsolutePosition(); const slope = calculateSlope(anchorStartPosition, oppositePoint); // Calculate the vector from the starting anchor position to the opposite anchor. const transformVector = { x: anchorStartPosition.x - oppositePoint.x, y: anchorStartPosition.y - oppositePoint.y, }; // Compute the movement delta from the starting position. const delta = { x: newPos.x - anchorStartPosition.x, y: newPos.y - anchorStartPosition.y, }; // Project the delta onto the transform vector. const projectedDelta = vectorProject(delta, transformVector); // Compute the candidate new position (before snapping). const nextPos = { x: anchorStartPosition.x + projectedDelta.x, y: anchorStartPosition.y + projectedDelta.y, }; for (let breakPoint of horizontal) { if (Math.abs(nextPos.y - breakPoint) <= snapRange) { nextPos.y = breakPoint; nextPos.x = anchorStartPosition.x + (breakPoint - anchorStartPosition.y) / slope; createLine(Layer, true, 0,breakPoint); break; } } for (let breakPoint of vertical) { if (Math.abs(nextPos.x - breakPoint) <= snapRange) { nextPos.x = breakPoint; nextPos.y= anchorStartPosition.y + slope * (breakPoint - anchorStartPosition.x) createLine(Layer, false,breakPoint, 0); break; } } return nextPos; }); } }; const handleResizeEnd = (e) =>{ const Layer = e.target.parent; e.currentTarget.anchorDragBoundFunc((oldAbsPos, newPos, event) => { return newPos }) console.log(e.currentTarget.anchorDragBoundFunc()) Layer.find(".guid-line").forEach((line) => line.destroy()); } const handleDragEnd = (e) =>{ const Layer = e.target.parent; Layer.find(".guid-line").forEach((line) => line.destroy()); } return { handleDragging,handleResizing,handleResizeEnd,handleDragEnd }; };