UNPKG

fabric_guideline

Version:

Fabric.js alignment guideline package for latest fabric v6 version

304 lines 13.2 kB
import * as fabric from "fabric"; import { Keys } from "./util"; export class AlignGuidelines { constructor({ canvas, aligningOptions, ignoreObjTypes, pickObjTypes, }) { this.aligningLineMargin = 4; this.aligningLineWidth = 0.75; this.aligningLineColor = "#F68066"; this.ignoreObjTypes = []; this.pickObjTypes = []; this.verticalLines = []; this.horizontalLines = []; this.activeObj = new fabric.Object(); this.canvas = canvas; this.ctx = canvas.getSelectionContext(); this.ignoreObjTypes = ignoreObjTypes || []; this.pickObjTypes = pickObjTypes || []; if (aligningOptions) { this.aligningLineMargin = aligningOptions.lineMargin || this.aligningLineMargin; this.aligningLineWidth = aligningOptions.lineWidth || this.aligningLineWidth; this.aligningLineColor = aligningOptions.lineColor || this.aligningLineColor; } } drawSign(x, y) { const ctx = this.ctx; ctx.lineWidth = 0.5; ctx.strokeStyle = this.aligningLineColor; ctx.beginPath(); const size = 2; ctx.moveTo(x - size, y - size); ctx.lineTo(x + size, y + size); ctx.moveTo(x + size, y - size); ctx.lineTo(x - size, y + size); ctx.stroke(); } drawLine(x1, y1, x2, y2) { const ctx = this.ctx; const point1 = fabric.util.transformPoint(new fabric.Point(x1, y1), this.canvas.viewportTransform); const point2 = fabric.util.transformPoint(new fabric.Point(x2, y2), this.canvas.viewportTransform); // use origin canvas api to draw guideline ctx.save(); ctx.lineWidth = this.aligningLineWidth; ctx.strokeStyle = this.aligningLineColor; ctx.beginPath(); ctx.moveTo(point1.x, point1.y); ctx.lineTo(point2.x, point2.y); ctx.stroke(); this.drawSign(point1.x, point1.y); this.drawSign(point2.x, point2.y); // 恢复这两玩意 // ctx.lineWidth = aligningLineWidth // ctx.strokeStyle = aligningLineColor ctx.restore(); } drawVerticalLine(coords) { const movingCoords = this.getObjDraggingObjCoords(this.activeObj); if (!Keys(movingCoords).some((key) => Math.abs(movingCoords[key].x - coords.x) < 0.0001)) return; this.drawLine(coords.x, Math.min(coords.y1, coords.y2), coords.x, Math.max(coords.y1, coords.y2)); } drawHorizontalLine(coords) { const movingCoords = this.getObjDraggingObjCoords(this.activeObj); if (!Keys(movingCoords).some((key) => Math.abs(movingCoords[key].y - coords.y) < 0.0001)) return; this.drawLine(Math.min(coords.x1, coords.x2), coords.y, Math.max(coords.x1, coords.x2), coords.y); } isInRange(value1, value2) { return Math.abs(Math.round(value1) - Math.round(value2)) <= this.aligningLineMargin / this.canvas.getZoom(); } watchMouseDown() { this.canvas.on("mouse:down", () => { this.clearLinesMeta(); this.viewportTransform = this.canvas.viewportTransform; }); } watchMouseUp() { this.canvas.on("mouse:up", () => { this.clearLinesMeta(); this.canvas.renderAll(); }); } watchMouseWheel() { this.canvas.on("mouse:wheel", () => { this.clearLinesMeta(); }); } clearLinesMeta() { this.verticalLines.length = this.horizontalLines.length = 0; } watchObjectMoving() { this.canvas.on("object:moving", (e) => { this.clearLinesMeta(); const activeObject = e.target; this.activeObj = activeObject; const canvasObjects = this.canvas.getObjects().filter((obj) => { if (this.ignoreObjTypes.length) { return !this.ignoreObjTypes.some((item) => obj[item.key] === item.value); } if (this.pickObjTypes.length) { return this.pickObjTypes.some((item) => obj[item.key] === item.value); } return true; }); // @ts-ignore const transform = this.canvas._currentTransform; if (!transform) return; this.traversAllObjects(activeObject, canvasObjects); }); } getObjDraggingObjCoords(activeObject) { const aCoords = activeObject.aCoords; const centerPoint = new fabric.Point((aCoords.tl.x + aCoords.br.x) / 2, (aCoords.tl.y + aCoords.br.y) / 2); const offsetX = centerPoint.x - activeObject.getCenterPoint().x; const offsetY = centerPoint.y - activeObject.getCenterPoint().y; return Keys(aCoords).reduce((acc, key) => { return { ...acc, [key]: { x: aCoords[key].x - offsetX, y: aCoords[key].y - offsetY, }, }; }, { c: activeObject.getCenterPoint(), }); } // 当对象被旋转时,需要忽略一些坐标,例如水平辅助线只取最上、下边的坐标(参考 figma) omitCoords(objCoords, type) { let newCoords; if (type === "vertical") { let l = ["tl", objCoords.tl]; let r = ["tl", objCoords.tl]; Keys(objCoords).forEach((key) => { if (objCoords[key].x < l[1].x) { l = [key, objCoords[key]]; } if (objCoords[key].x > r[1].x) { r = [key, objCoords[key]]; } }); newCoords = { [l[0]]: l[1], [r[0]]: r[1], c: objCoords.c, }; } else { let t = ["tl", objCoords.tl]; let b = ["tl", objCoords.tl]; Keys(objCoords).forEach((key) => { if (objCoords[key].y < t[1].y) { t = [key, objCoords[key]]; } if (objCoords[key].y > b[1].y) { b = [key, objCoords[key]]; } }); newCoords = { [t[0]]: t[1], [b[0]]: b[1], c: objCoords.c, }; } return newCoords; } getObjMaxWidthHeightByCoords(coords) { const objHeight = Math.max(Math.abs(coords.c.y - coords["tl"].y), Math.abs(coords.c.y - coords["tr"].y)) * 2; const objWidth = Math.max(Math.abs(coords.c.x - coords["tl"].x), Math.abs(coords.c.x - coords["tr"].x)) * 2; return { objHeight, objWidth }; } /** * fabric.Object.getCenterPoint will return the center point of the object calc by mouse moving & dragging distance. * calcCenterPointByACoords will return real center point of the object position. */ calcCenterPointByACoords(coords) { return new fabric.Point((coords.tl.x + coords.br.x) / 2, (coords.tl.y + coords.br.y) / 2); } traversAllObjects(activeObject, canvasObjects) { const objCoordsByMovingDistance = this.getObjDraggingObjCoords(activeObject); const snapXPoints = []; const snapYPoints = []; for (let i = canvasObjects.length; i--;) { if (canvasObjects[i] === activeObject) continue; const objCoords = { ...canvasObjects[i].aCoords, c: canvasObjects[i].getCenterPoint(), }; const { objHeight, objWidth } = this.getObjMaxWidthHeightByCoords(objCoords); Keys(objCoordsByMovingDistance).forEach((activeObjPoint) => { const newCoords = canvasObjects[i].angle !== 0 ? this.omitCoords(objCoords, "horizontal") : objCoords; function calcHorizontalLineCoords(objPoint, activeObjCoords) { let x1, x2; if (objPoint === "c") { x1 = Math.min(objCoords.c.x - objWidth / 2, activeObjCoords[activeObjPoint].x); x2 = Math.max(objCoords.c.x + objWidth / 2, activeObjCoords[activeObjPoint].x); } else { x1 = Math.min(objCoords[objPoint].x, activeObjCoords[activeObjPoint].x); x2 = Math.max(objCoords[objPoint].x, activeObjCoords[activeObjPoint].x); } return { x1, x2 }; } Keys(newCoords).forEach((objPoint) => { if (this.isInRange(objCoordsByMovingDistance[activeObjPoint].y, objCoords[objPoint].y)) { const y = objCoords[objPoint].y; let { x1, x2 } = calcHorizontalLineCoords(objPoint, objCoordsByMovingDistance); const offset = objCoordsByMovingDistance[activeObjPoint].y - y; snapYPoints.push(objCoordsByMovingDistance.c.y - offset); if (activeObject.aCoords) { let { x1, x2 } = calcHorizontalLineCoords(objPoint, { ...activeObject.aCoords, c: this.calcCenterPointByACoords(activeObject.aCoords), }); this.horizontalLines.push({ y, x1, x2 }); } else { this.horizontalLines.push({ y, x1, x2 }); } } }); }); Keys(objCoordsByMovingDistance).forEach((activeObjPoint) => { const newCoords = canvasObjects[i].angle !== 0 ? this.omitCoords(objCoords, "vertical") : objCoords; function calcVerticalLineCoords(objPoint, activeObjCoords) { let y1, y2; if (objPoint === "c") { y1 = Math.min(newCoords.c.y - objHeight / 2, activeObjCoords[activeObjPoint].y); y2 = Math.max(newCoords.c.y + objHeight / 2, activeObjCoords[activeObjPoint].y); } else { y1 = Math.min(objCoords[objPoint].y, activeObjCoords[activeObjPoint].y); y2 = Math.max(objCoords[objPoint].y, activeObjCoords[activeObjPoint].y); } return { y1, y2 }; } Keys(newCoords).forEach((objPoint) => { if (this.isInRange(objCoordsByMovingDistance[activeObjPoint].x, objCoords[objPoint].x)) { const x = objCoords[objPoint].x; let { y1, y2 } = calcVerticalLineCoords(objPoint, objCoordsByMovingDistance); const offset = objCoordsByMovingDistance[activeObjPoint].x - x; snapXPoints.push(objCoordsByMovingDistance.c.x - offset); if (activeObject.aCoords) { let { y1, y2 } = calcVerticalLineCoords(objPoint, { ...activeObject.aCoords, c: this.calcCenterPointByACoords(activeObject.aCoords), }); this.verticalLines.push({ x, y1, y2 }); } else { this.verticalLines.push({ x, y1, y2 }); } } }); }); this.snap({ activeObject, draggingObjCoords: objCoordsByMovingDistance, snapXPoints, snapYPoints, }); } } snap({ activeObject, snapXPoints, draggingObjCoords, snapYPoints, }) { const sortPoints = (list, originPoint) => { if (!list.length) return originPoint; return list .map((val) => ({ abs: Math.abs(originPoint - val), val, })) .sort((a, b) => a.abs - b.abs)[0].val; }; activeObject.setPositionByOrigin( // auto snap nearest object, record all the snap points, and then find the nearest one new fabric.Point(sortPoints(snapXPoints, draggingObjCoords.c.x), sortPoints(snapYPoints, draggingObjCoords.c.y)), "center", "center"); } clearGuideline() { this.canvas.clearContext(this.ctx); } watchRender() { this.canvas.on("before:render", () => { this.clearGuideline(); }); this.canvas.on("after:render", () => { for (let i = this.verticalLines.length; i--;) { this.drawVerticalLine(this.verticalLines[i]); } for (let i = this.horizontalLines.length; i--;) { this.drawHorizontalLine(this.horizontalLines[i]); } this.canvas.calcOffset(); }); } init() { this.watchObjectMoving(); this.watchRender(); this.watchMouseDown(); this.watchMouseUp(); this.watchMouseWheel(); } } //# sourceMappingURL=aligning.js.map