fabric_guideline
Version:
Fabric.js alignment guideline package for latest fabric v6 version
304 lines • 13.2 kB
JavaScript
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