1. 先新建一个指令文件,比如 directives/guideline.js:
import { fabric } from 'fabric';
export default {
inserted(el) {
// 创建 fabric.Canvas
const canvas = new fabric.Canvas(el);
let ctx = canvas.getSelectionContext(),
aligningLineOffset = 5,
aligningLineMargin = 4,
aligningLineWidth = 1,
aligningLineColor = '#3370ff',
viewportTransform,
zoom = 1;
const verticalLines = [], horizontalLines = [];
function drawVerticalLine(coords) {
drawLine(coords.x, coords.y1, coords.x, coords.y2);
}
function drawHorizontalLine(coords) {
drawLine(coords.x1, coords.y, coords.x2, coords.y);
}
function drawLine(x1, y1, x2, y2) {
ctx.save();
ctx.lineWidth = aligningLineWidth;
ctx.strokeStyle = aligningLineColor;
ctx.beginPath();
ctx.moveTo(x1 * zoom + viewportTransform[4], y1 * zoom + viewportTransform[5]);
ctx.lineTo(x2 * zoom + viewportTransform[4], y2 * zoom + viewportTransform[5]);
ctx.stroke();
ctx.restore();
}
function isInRange(value1, value2) {
return Math.abs(Math.round(value1) - Math.round(value2)) <= aligningLineMargin;
}
canvas.on('mouse:down', () => {
viewportTransform = canvas.viewportTransform;
zoom = canvas.getZoom();
});
canvas.on('object:moving', (e) => {
const activeObject = e.target;
if (!activeObject) return;
const center = activeObject.getCenterPoint();
const canvasCenter = {
x: canvas.getWidth() / 2,
y: canvas.getHeight() / 2
};
const objBounds = activeObject.getBoundingRect(true);
const objWidth = objBounds.width / zoom;
const objHeight = objBounds.height / zoom;
let hasCenterSnap = false;
let hasEdgeSnap = false;
verticalLines.length = 0;
horizontalLines.length = 0;
// 中心线吸附
if (isInRange(center.x, canvasCenter.x)) {
verticalLines.push({
x: canvasCenter.x,
y1: 0,
y2: canvas.getHeight()
});
activeObject.left = canvasCenter.x - objWidth / 2;
hasCenterSnap = true;
}
if (isInRange(center.y, canvasCenter.y)) {
horizontalLines.push({
y: canvasCenter.y,
x1: 0,
x2: canvas.getWidth()
});
activeObject.top = canvasCenter.y - objHeight / 2;
hasCenterSnap = true;
}
// 元素之间吸附
const objects = canvas.getObjects().filter(o => o !== activeObject);
for (let obj of objects) {
const oCenter = obj.getCenterPoint();
const oBounds = obj.getBoundingRect(true);
const oWidth = oBounds.width / zoom;
const oHeight = oBounds.height / zoom;
if (isInRange(obj.left, activeObject.left)) {
verticalLines.push({
x: obj.left,
y1: 0,
y2: canvas.getHeight()
});
activeObject.left = obj.left;
hasEdgeSnap = true;
}
const objRight = obj.left + oWidth;
const activeRight = activeObject.left + objWidth;
if (isInRange(objRight, activeRight)) {
verticalLines.push({
x: objRight,
y1: 0,
y2: canvas.getHeight()
});
activeObject.left = objRight - objWidth;
hasEdgeSnap = true;
}
if (isInRange(obj.top, activeObject.top)) {
horizontalLines.push({
y: obj.top,
x1: 0,
x2: canvas.getWidth()
});
activeObject.top = obj.top;
hasEdgeSnap = true;
}
const objBottom = obj.top + oHeight;
const activeBottom = activeObject.top + objHeight;
if (isInRange(objBottom, activeBottom)) {
horizontalLines.push({
y: objBottom,
x1: 0,
x2: canvas.getWidth()
});
activeObject.top = objBottom - objHeight;
hasEdgeSnap = true;
}
if (isInRange(oCenter.x, center.x)) {
verticalLines.push({
x: oCenter.x,
y1: 0,
y2: canvas.getHeight()
});
activeObject.left = oCenter.x - objWidth / 2;
hasEdgeSnap = true;
}
if (isInRange(oCenter.y, center.y)) {
horizontalLines.push({
y: oCenter.y,
x1: 0,
x2: canvas.getWidth()
});
activeObject.top = oCenter.y - objHeight / 2;
hasEdgeSnap = true;
}
}
if (hasCenterSnap || hasEdgeSnap) {
activeObject.setCoords();
}
});
canvas.on('before:render', () => {
if (canvas.contextTop) {
canvas.clearContext(canvas.contextTop);
}
});
canvas.on('after:render', () => {
verticalLines.forEach(drawVerticalLine);
horizontalLines.forEach(drawHorizontalLine);
});
canvas.on('mouse:up', () => {
verticalLines.length = 0;
horizontalLines.length = 0;
canvas.renderAll();
});
// 把 canvas 挂到元素上,外面可以通过 el.canvas 拿到
el.canvas = canvas;
},
unbind(el) {
// 销毁 canvas
if (el.canvas) {
el.canvas.dispose();
}
}
}
2. 然后在 main.js 里全局注册
import Vue from 'vue';
import guideline from './directives/guideline';
Vue.directive('guideline', guideline);
用法:
在任意组件里,直接用:
<canvas id="myCanvas" v-guideline width="500" height="500"></canvas>
效果:
- 自动初始化 Fabric Canvas
- 自动开启辅助线
- 支持旋转、居中、边缘吸附
- 松开自动清除
- 不用你管任何复杂逻辑