fabric
fabric
是一个基于canvas
2d 绘图工具库,如果你打算使用canvas
进行2d
绘图,推荐使用fabric
。
官方文档比较难看懂,推荐看此作者的文章:Fabric.js 从入门到____
进阶:更推荐看此仓库源码vue-fabric-editor
基础设置
源代码
vue
<template>
<fabric-canvas @init="onFabricCanvasInit"></fabric-canvas>
<div class="actions">
<button type="button" @click="addRect">矩形</button>
<button type="button" @click="useConstomContols">设置自定义控件</button>
<button type="button" @click="resetContols">恢复控件</button>
</div>
<div class="actions">
<button type="button" @click="lockObject">锁定元素</button>
<button type="button" @click="unlockObject">解锁元素</button>
</div>
</template>
<script setup>
import bgTransparent from '../../../images/bg-transparent.png';
import FabricCanvas from '../canvas.vue';
import FabricZoom from '../handles/zoom';
import FabricSelection from '../handles/selection';
import FabricTranslate from '../handles/translate';
import {
customDefaultContorls,
resetDetaultContols,
} from '../controls/default';
import useCornerRotateControls from '../controls/rotate';
import useDelControl from '../controls/del';
import {
useLockControl,
doLockActivedObjectControl,
doUnlockActivedObjectControl,
} from '../controls/lock';
let fabricCanvas = null;
let fabric = null;
async function onFabricCanvasInit(canvas, fabricModule) {
fabricCanvas = canvas;
fabric = fabricModule;
// 设置背景
canvas.setBackgroundColor(
{
source: bgTransparent,
repeat: 'repeat',
},
canvas.renderAll.bind(canvas)
);
// 设置缩放
const zoomInstance = new FabricZoom(canvas, { byPoint: true });
zoomInstance.onChange((zoom) => {
// console.log(zoom)
});
// 设置拖拽移动
const translateInstance = new FabricTranslate(canvas);
translateInstance.onChange((vpt) => {
// console.log(vpt)
});
// 选中元素回调
const selectionInstance = new FabricSelection(canvas);
selectionInstance.onSelect((objects) => {
// console.log(objects)
});
selectionInstance.onClear(() => {
// console.log('clear')
});
}
// 添加一个矩形
function addRect() {
// 创建一个长方形
const rect = new fabric.Rect({
top: 100, // 距离容器顶部 100px
left: 100, // 距离容器左侧 100px
width: 30, // 矩形宽度 30px
height: 30, // 矩形高度 30px
fill: 'blue', // 填充
});
fabricCanvas.add(rect); // 将矩形添加到 canvas 画布里
}
let removeRotateControlEventCache = null;
// 使用自定义控件
function useConstomContols() {
// 覆盖默认控件
customDefaultContorls();
// 新自定义旋转控件
const { removeRotateControlEvent } = useCornerRotateControls(fabricCanvas);
removeRotateControlEventCache = removeRotateControlEvent;
// 新增删除控件
useDelControl(fabricCanvas, (object) => {
// console.log(object);
});
// 新增锁定控件
useLockControl(() => {
unlockObject();
});
fabricCanvas.requestRenderAll();
}
// 恢复默认控件
function resetContols() {
resetDetaultContols();
removeRotateControlEventCache && removeRotateControlEventCache();
fabricCanvas.requestRenderAll();
}
function lockObject() {
const activeObject = fabricCanvas.getActiveObject();
doLockActivedObjectControl(activeObject);
fabricCanvas.requestRenderAll();
}
function unlockObject() {
const activeObject = fabricCanvas.getActiveObject();
doUnlockActivedObjectControl(activeObject);
fabricCanvas.requestRenderAll();
}
</script>
<style lang="less" scoped>
.actions {
margin-top: 10px;
> button {
padding: 0 10px;
border: 1px solid #eee;
margin-right: 10px;
}
}
</style>
js
export default class FabricZoom {
constructor(canvas, options = { byPoint: true }) {
this.events = [];
this.hotkeys = options.hotkeys || ['Ctrl'];
const byPoint = options.byPoint;
// 缩放,监听鼠标滚轮事件
canvas.on('mouse:wheel', (opt) => {
if (this.hotkeys.includes('Ctrl') && !opt.e.ctrlKey) {
return false;
}
opt.e.stopPropagation();
opt.e.preventDefault();
const delta = opt.e.deltaY; // 滚轮向上滚一下是 -100,向下滚一下是 100
let zoom = canvas.getZoom(); // 获取画布当前缩放值
// 控制缩放范围在 0.01~20 的区间内
zoom *= 0.999 ** delta;
if (zoom > 20) zoom = 20;
if (zoom < 0.01) zoom = 0.01;
// 设置画布缩放比例
// 关键点!!!
// 参数1:将画布的所放点设置成鼠标当前位置
// 参数2:传入缩放值
if (byPoint) {
canvas.zoomToPoint(
{
x: opt.e.offsetX, // 鼠标x轴坐标
y: opt.e.offsetY, // 鼠标y轴坐标
},
zoom // 最后要缩放的值
);
} else {
canvas.setZoom(zoom);
}
this.events.forEach((cb) => {
cb(zoom);
});
});
}
onChange(cb) {
this.events.push(cb);
}
}
js
import hotkeys from 'hotkeys-js';
export default class FabricTranslate {
constructor(canvas, options = {}) {
this.events = [];
this.dragMode = false;
this.canvas = canvas;
this.selection = canvas.selection;
this.hotkeys = options.hotkeys || ['Space'];
this.checkObjectMovable = options.checkObjectMovable;
this.initHotKey();
this.initDring();
}
initHotKey() {
const hotkeyEvent = (eventName, e) => {
e.preventDefault();
const matchKey = this.hotkeys.find((item) => item === e.code);
if (matchKey && e.type === 'keydown') {
if (!this.dragMode) {
this.startDring();
}
}
if (matchKey && e.type === 'keyup') {
this.endDring();
}
};
this.hotkeys.forEach((keyName) => {
hotkeys(keyName, { keyup: true }, (e) => {
hotkeyEvent(keyName, e);
});
});
}
initDring() {
const canvas = this.canvas;
// 平移
canvas.on('mouse:down', (opt) => {
const checkObjectMovable = this.checkObjectMovable;
// 鼠标按下时触发
const evt = opt.e;
const targetObject = opt.target;
const onDragState =
(!opt.transform || opt.transform.action === 'drag') && evt.button === 0;
// 传入checkObjectMovable且以空白处开始拖拽时,允许拖拽画布
const onDragWhitespace =
checkObjectMovable &&
(!targetObject || !checkObjectMovable(targetObject));
if (this.dragMode || (onDragState && onDragWhitespace)) {
canvas.setCursor('grab');
canvas.isDragging = true; // isDragging 是自定义的,开启移动状态
canvas.lastPosX = evt.clientX; // lastPosX 是自定义的
canvas.lastPosY = evt.clientY; // lastPosY 是自定义的
// 禁止选中
canvas.selection = false;
canvas.getObjects().forEach((obj) => {
obj.beforeCanvasMoveSelectable = obj.selectable;
obj.selectable = false;
});
canvas.requestRenderAll();
}
});
canvas.on('mouse:move', (opt) => {
// 鼠标移动时触发
if (canvas.isDragging) {
const evt = opt.e;
const vpt = canvas.viewportTransform; // 聚焦视图的转换
vpt[4] += evt.clientX - canvas.lastPosX;
vpt[5] += evt.clientY - canvas.lastPosY;
canvas.requestRenderAll(); // 重新渲染
canvas.lastPosX = evt.clientX;
canvas.lastPosY = evt.clientY;
this.events.forEach((cb) => {
cb(vpt);
});
}
});
canvas.on('mouse:up', (opt) => {
// 鼠标松开时触发
canvas.setViewportTransform(canvas.viewportTransform); // 设置此画布实例的视口转换
canvas.isDragging = false; //
canvas.setCursor('auto');
// 取消禁止选中
canvas.selection = this.selection;
canvas.getObjects().forEach((obj) => {
obj.selectable =
typeof obj.beforeCanvasMoveSelectable !== 'undefined'
? obj.beforeCanvasMoveSelectable
: obj.selectable;
delete obj.beforeCanvasMoveSelectable;
});
canvas.requestRenderAll();
});
}
startDring() {
this.dragMode = true;
this.canvas.defaultCursor = 'grab';
this.canvas.renderAll();
}
endDring() {
this.dragMode = false;
this.canvas.defaultCursor = 'default';
this.canvas.isDragging = false;
this.canvas.renderAll();
}
onChange(cb) {
this.events.push(cb);
}
}
js
export default class FabricSelection {
constructor(canvas) {
this.selectEvents = [];
this.clearEvents = [];
canvas.on('selection:created', (e) => onObjectsSelect(e));
canvas.on('selection:updated', (e) => onObjectsSelect(e));
canvas.on('selection:cleared', (e) => onObjectsSelect(e));
const onObjectsSelect = (e) => {
const actives = canvas.getActiveObjects();
if (actives && actives.length > 0) {
this.selectEvents.forEach((cb) => {
cb(actives);
});
} else {
this.clearEvents.forEach((cb) => {
cb();
});
}
};
}
onSelect(cb) {
this.selectEvents.push(cb);
}
onClear(cb) {
this.clearEvents.push(cb);
}
}
js
import { fabric } from 'fabric';
import verticalImg from '../../../images/controls/middlecontrol.svg';
import horizontalImg from '../../../images/controls/middlecontrolhoz.svg';
import edgeImg from '../../../images/controls/edgecontrol.svg';
import rotateImg from '../../../images/controls/rotateicon.svg';
// 缓存默认控件
const defaultControls = { ...fabric.Object.prototype.controls };
// 缓存默认对象设置
const defaultObjectConfig = {
transparentCorners: fabric.Object.prototype.transparentCorners,
borderColor: fabric.Object.prototype.borderColor,
cornerColor: fabric.Object.prototype.cornerColor,
borderScaleFactor: fabric.Object.prototype.borderScaleFactor,
cornerStyle: fabric.Object.prototype.cornerStyle,
cornerStrokeColor: fabric.Object.prototype.cornerStrokeColor,
borderOpacityWhenMoving: fabric.Object.prototype.borderOpacityWhenMoving,
};
// 缓存默认组设置
const defaultGroupConfig = {
lockMovementX: fabric.Group.prototype.lockMovementX,
lockMovementY: fabric.Group.prototype.lockMovementY,
lockRotation: fabric.Group.prototype.lockRotation,
lockScalingX: fabric.Group.prototype.lockScalingX,
lockScalingY: fabric.Group.prototype.lockScalingY,
selectable: fabric.Group.prototype.selectable,
hasControls: fabric.Group.prototype.hasControls,
};
// 覆盖默认中间横杠
function intervalControl() {
const verticalImgIcon = document.createElement('img');
verticalImgIcon.src = verticalImg;
const horizontalImgIcon = document.createElement('img');
horizontalImgIcon.src = horizontalImg;
function renderIcon(ctx, left, top, styleOverride, fabricObject) {
const wsize = 20;
const hsize = 20;
ctx.save();
ctx.translate(left, top);
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
ctx.drawImage(verticalImgIcon, -wsize / 2, -hsize / 2, wsize, hsize);
ctx.restore();
}
function renderIconHoz(ctx, left, top, styleOverride, fabricObject) {
const wsize = 20;
const hsize = 20;
ctx.save();
ctx.translate(left, top);
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
ctx.drawImage(horizontalImgIcon, -wsize / 2, -hsize / 2, wsize, hsize);
ctx.restore();
}
// 中间横杠:左
fabric.Object.prototype.controls.ml = new fabric.Control({
visible: true,
x: -0.5,
y: 0,
offsetX: -1,
cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
actionHandler: fabric.controlsUtils.scalingXOrSkewingY,
getActionName: fabric.controlsUtils.scaleOrSkewActionName,
render: renderIcon,
});
// 中间横杠:右
fabric.Object.prototype.controls.mr = new fabric.Control({
visible: true,
x: 0.5,
y: 0,
offsetX: 1,
cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
actionHandler: fabric.controlsUtils.scalingXOrSkewingY,
getActionName: fabric.controlsUtils.scaleOrSkewActionName,
render: renderIcon,
});
// 中间横杠:底
fabric.Object.prototype.controls.mb = new fabric.Control({
visible: true,
x: 0,
y: 0.5,
offsetY: 1,
cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
actionHandler: fabric.controlsUtils.scalingYOrSkewingX,
getActionName: fabric.controlsUtils.scaleOrSkewActionName,
render: renderIconHoz,
});
// 中间横杠:顶
fabric.Object.prototype.controls.mt = new fabric.Control({
visible: true,
x: 0,
y: -0.5,
offsetY: -1,
cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
actionHandler: fabric.controlsUtils.scalingYOrSkewingX,
getActionName: fabric.controlsUtils.scaleOrSkewActionName,
render: renderIconHoz,
});
}
// 覆盖顶点
function peakControl() {
const img = document.createElement('img');
img.src = edgeImg;
function renderIconEdge(ctx, left, top, styleOverride, fabricObject) {
const wsize = 20;
const hsize = 20;
ctx.save();
ctx.translate(left, top);
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
ctx.drawImage(img, -wsize / 2, -hsize / 2, wsize, hsize);
ctx.restore();
}
// 四角图标:上左
fabric.Object.prototype.controls.tl = new fabric.Control({
x: -0.5,
y: -0.5,
cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,
actionHandler: fabric.controlsUtils.scalingEqually,
render: renderIconEdge,
});
// 四角图标:下左
fabric.Object.prototype.controls.bl = new fabric.Control({
x: -0.5,
y: 0.5,
cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,
actionHandler: fabric.controlsUtils.scalingEqually,
render: renderIconEdge,
});
// 四角图标:上右
fabric.Object.prototype.controls.tr = new fabric.Control({
x: 0.5,
y: -0.5,
cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,
actionHandler: fabric.controlsUtils.scalingEqually,
render: renderIconEdge,
});
// 四角图标:下右
fabric.Object.prototype.controls.br = new fabric.Control({
x: 0.5,
y: 0.5,
cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,
actionHandler: fabric.controlsUtils.scalingEqually,
render: renderIconEdge,
});
}
// 覆盖旋转点
function rotationControl() {
const img = document.createElement('img');
img.src = rotateImg;
function renderIconRotate(ctx, left, top, styleOverride, fabricObject) {
const wsize = 40;
const hsize = 40;
ctx.save();
ctx.translate(left, top);
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
ctx.drawImage(img, -wsize / 2, -hsize / 2, wsize, hsize);
ctx.restore();
}
// 旋转图标
fabric.Object.prototype.controls.mtr = new fabric.Control({
x: 0,
y: 0.5,
cursorStyleHandler: fabric.controlsUtils.rotationStyleHandler,
actionHandler: fabric.controlsUtils.rotationWithSnapping,
offsetY: 30,
withConnecton: false,
actionName: 'rotate',
render: renderIconRotate,
});
}
// 自定义控件
export function customDefaultContorls(canvas) {
// 顶点图标
peakControl(canvas);
// 中间横杠图标
intervalControl(canvas);
// 旋转图标
rotationControl(canvas);
// 选中样式
fabric.Object.prototype.set({
transparentCorners: false,
borderColor: '#5E7FFF',
cornerColor: '#FFF',
borderScaleFactor: 1.5,
cornerStyle: 'circle',
cornerStrokeColor: '#0E98FC',
borderOpacityWhenMoving: 0.7,
});
// 组样式
fabric.Group.prototype.set({
lockMovementX: false,
lockMovementY: false,
lockRotation: false,
lockScalingX: false,
lockScalingY: false,
selectable: true,
hasControls: true,
});
}
// 恢复默认控件
export function resetDetaultContols() {
fabric.Object.prototype.controls = {
...defaultControls,
};
fabric.Object.prototype.set(defaultObjectConfig);
fabric.Group.prototype.set(defaultGroupConfig);
}
js
import { fabric } from 'fabric';
// 定义旋转光标样式,根据转动角度设定光标旋转
function rotateIcon(angle) {
return `url("data:image/svg+xml,%3Csvg height='18' width='18' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'%3E%3Cg fill='none' transform='rotate(${angle} 16 16)'%3E%3Cpath d='M22.4484 0L32 9.57891L22.4484 19.1478V13.1032C17.6121 13.8563 13.7935 17.6618 13.0479 22.4914H19.2141L9.60201 32.01L0 22.4813H6.54912C7.36524 14.1073 14.0453 7.44023 22.4484 6.61688V0Z' fill='white'/%3E%3Cpath d='M24.0605 3.89587L29.7229 9.57896L24.0605 15.252V11.3562C17.0479 11.4365 11.3753 17.0895 11.3048 24.0879H15.3048L9.60201 29.7308L3.90932 24.0879H8.0806C8.14106 15.3223 15.2645 8.22345 24.0605 8.14313V3.89587Z' fill='black'/%3E%3C/g%3E%3C/svg%3E ") 12 12,crosshair`;
}
export function useCornerRotateControls(canvas) {
// 添加旋转控制响应区域
fabric.Object.prototype.controls.mtr = new fabric.Control({
x: -0.5,
y: -0.5,
offsetY: -10,
offsetX: -10,
rotate: 20,
actionName: 'rotate',
actionHandler: fabric.controlsUtils.rotationWithSnapping,
render: () => {},
});
// ↖左上
fabric.Object.prototype.controls.mtr2 = new fabric.Control({
x: 0.5,
y: -0.5,
offsetY: -10,
offsetX: 10,
rotate: 20,
actionName: 'rotate',
actionHandler: fabric.controlsUtils.rotationWithSnapping,
render: () => {},
}); // ↗右上
fabric.Object.prototype.controls.mtr3 = new fabric.Control({
x: 0.5,
y: 0.5,
offsetY: 10,
offsetX: 10,
rotate: 20,
actionName: 'rotate',
actionHandler: fabric.controlsUtils.rotationWithSnapping,
render: () => {},
}); // ↘右下
fabric.Object.prototype.controls.mtr4 = new fabric.Control({
x: -0.5,
y: 0.5,
offsetY: 10,
offsetX: -10,
rotate: 20,
actionName: 'rotate',
actionHandler: fabric.controlsUtils.rotationWithSnapping,
render: () => {},
}); // ↙左下
const onCanvasRender = () => {
if (canvas.getActiveObject()) {
fabric.Object.prototype.controls.mtr.cursorStyle = rotateIcon(
Number(canvas.getActiveObject().angle.toFixed(2))
);
fabric.Object.prototype.controls.mtr2.cursorStyle = rotateIcon(
Number(canvas.getActiveObject().angle.toFixed(2)) + 90
);
fabric.Object.prototype.controls.mtr3.cursorStyle = rotateIcon(
Number(canvas.getActiveObject().angle.toFixed(2)) + 180
);
fabric.Object.prototype.controls.mtr4.cursorStyle = rotateIcon(
Number(canvas.getActiveObject().angle.toFixed(2)) + 270
);
}
};
const onObjectRotating = (event) => {
const body = canvas.lowerCanvasEl.nextSibling;
switch (event.transform.corner) {
case 'mtr':
body.style.cursor = rotateIcon(
Number(canvas.getActiveObject().angle.toFixed(2))
);
break;
case 'mtr2':
body.style.cursor = rotateIcon(
Number(canvas.getActiveObject().angle.toFixed(2)) + 90
);
break;
case 'mtr3':
body.style.cursor = rotateIcon(
Number(canvas.getActiveObject().angle.toFixed(2)) + 180
);
break;
case 'mtr4':
body.style.cursor = rotateIcon(
Number(canvas.getActiveObject().angle.toFixed(2)) + 270
);
break;
default:
break;
} // 设置四角旋转光标
};
// 渲染时,执行
canvas.on('after:render', onCanvasRender);
// 旋转时,实时更新旋转控制图标
canvas.on('object:rotating', onObjectRotating);
function removeRotateControlEvent() {
canvas.off('after:render', onCanvasRender);
canvas.off('object:rotating', onObjectRotating);
}
return {
removeRotateControlEvent,
};
}
export default useCornerRotateControls;
js
import { fabric } from 'fabric';
import deleteIcon from '../../../images/controls/canvasObjectDeleteIcon.svg';
// 删除按钮
export function useDelControl(canvas, onObjectRemove) {
const delImg = document.createElement('img');
delImg.src = deleteIcon;
function renderDelIcon(ctx, left, top, styleOverride, fabricObject) {
const size = this.cornerSize;
ctx.save();
ctx.translate(left, top);
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
ctx.drawImage(delImg, -size / 2, -size / 2, size, size);
ctx.restore();
}
// 删除选中元素
function deleteObject() {
const activeObject = canvas.getActiveObjects();
if (activeObject) {
activeObject.map((item) => canvas.remove(item));
canvas.requestRenderAll();
canvas.discardActiveObject();
}
onObjectRemove && onObjectRemove(activeObject);
}
// 删除图标
fabric.Object.prototype.controls.del = new fabric.Control({
x: 0.5,
y: -0.5,
offsetY: -16,
offsetX: 16,
cursorStyle: 'pointer',
mouseUpHandler: deleteObject,
render: renderDelIcon,
cornerSize: 24,
});
}
export default useDelControl;
js
import { fabric } from 'fabric';
import lockCanvasImg from '../../../images/controls/lockCanvasObject.svg';
const LOCK_CONTROL_KEY = 'lock';
/**
* 锁定元素
*/
export function doLockActivedObjectControl(activeObject) {
if (!activeObject) {
return false;
}
const oldControlVisibleCache = {};
const oldBorderColor = activeObject.borderColor;
for (let key in activeObject.controls) {
oldControlVisibleCache[key] = activeObject.controls[key].visible;
if (key === LOCK_CONTROL_KEY) {
activeObject.setControlVisible(LOCK_CONTROL_KEY, true);
} else {
activeObject.setControlVisible(key, false);
}
}
activeObject.oldControlVisibleCache = oldControlVisibleCache;
activeObject.oldBorderColor = oldBorderColor;
activeObject.borderColor = '#FF3232';
}
/**
* 解锁元素
*/
export function doUnlockActivedObjectControl(activeObject) {
if (!activeObject) {
return false;
}
const oldControlVisibleCache = activeObject.oldControlVisibleCache || {};
for (let key in activeObject.controls) {
if (key === LOCK_CONTROL_KEY) {
activeObject.setControlVisible(LOCK_CONTROL_KEY, false);
} else {
activeObject.setControlVisible(key, oldControlVisibleCache[key] || true);
}
}
activeObject.borderColor = activeObject.oldBorderColor || '#FF3232';
}
export function useLockControl(unlockCallback) {
const img = document.createElement('img');
img.src = lockCanvasImg;
function renderIconLock(ctx, left, top, styleOverride, fabricObject) {
const wsize = 24;
const hsize = 24;
ctx.save();
ctx.translate(left, top);
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
ctx.drawImage(img, -wsize / 2, -hsize, wsize, hsize);
ctx.restore();
}
// 解锁元素
function unlockActivedObject() {
unlockCallback && unlockCallback();
}
// 锁定图标,只有被锁定时才显示,点击后解锁
fabric.Object.prototype.controls[LOCK_CONTROL_KEY] = new fabric.Control({
x: 0.5,
y: 0.5,
offsetY: 16,
touchSizeX: 36,
touchSizeY: 36,
visible: false,
sizeX: 36,
sizeY: 36,
mouseUpHandler: unlockActivedObject,
cursorStyle: 'pointer',
actionName: 'ulock',
render: renderIconLock,
});
}
export default {
useLockControl,
doLockActivedObjectControl,
doUnlockActivedObjectControl,
};
vue
<template>
<div ref="wrapEl" class="canvas-box">
<canvas :id="canvasId"></canvas>
</div>
</template>
<script setup>
import { useId } from 'vue';
// import { fabric } from 'fabric';
import { onMounted, ref } from 'vue';
const canvasId = `fabric-canvas-${useId()}`;
const emit = defineEmits(['init']);
const wrapEl = ref(null);
let fabricCanvas;
const initFabric = () => {
import('fabric').then((module) => {
// use code
const { fabric } = module.default;
const wrapHeight = wrapEl.value.offsetHeight;
const wrapWidth = wrapEl.value.offsetWidth;
fabricCanvas = new fabric.Canvas(canvasId, {
width: wrapWidth,
height: wrapHeight,
});
emit('init', fabricCanvas, fabric);
});
};
onMounted(initFabric);
</script>
<style>
.canvas-box {
width: 100%;
height: 450px;
border: 1px solid #eaeaea;
background-color: #fafafa;
}
</style>
应用
一、常用处理函数
源代码
vue
<template>
<fabric-canvas @init="onFabricCanvasInit"></fabric-canvas>
<div class="actions">
<button type="button" @click="screenshot">截图</button>
<button type="button" @click="centerSelectedObject">居中选中元素</button>
<button type="button" @click="zoomToFitObjects(fabricCanvas)">
展示所有元素
</button>
</div>
</template>
<script setup>
import bgTransparent from '../../../images/bg-transparent.png';
import FabricCanvas from '../canvas.vue';
import FabricZoom from '../handles/zoom';
import FabricSelection from '../handles/selection';
import FabricTranslate from '../handles/translate';
import { setCenterFromObject, zoomToFitObjects } from '../handles/utils';
let fabricCanvas = null;
let fabric = null;
let selectedObject = null;
async function onFabricCanvasInit(canvas, fabricModule) {
fabricCanvas = canvas;
fabric = fabricModule;
// 设置背景
canvas.setBackgroundColor(
{
source: bgTransparent,
repeat: 'repeat',
},
canvas.renderAll.bind(canvas)
);
// 设置缩放
const zoomInstance = new FabricZoom(canvas, { byPoint: true });
zoomInstance.onChange((zoom) => {
// console.log(zoom)
});
// 设置拖拽移动
const translateInstance = new FabricTranslate(canvas);
translateInstance.onChange((vpt) => {
// console.log(vpt)
});
// 选中元素回调
const selectionInstance = new FabricSelection(canvas);
selectionInstance.onSelect((objects) => {
selectedObject = objects[0];
});
selectionInstance.onClear(() => {
selectedObject = null;
// console.log('clear')
});
// 创建一个长方形
const rect = new fabric.Rect({
top: 100, // 距离容器顶部 100px
left: 100, // 距离容器左侧 100px
width: 30, // 矩形宽度 30px
height: 30, // 矩形高度 30px
fill: 'blue', // 填充
});
fabricCanvas.add(rect); // 将矩形添加到 canvas 画布里
// 创建一个长方形
const rect2 = new fabric.Rect({
top: 500, // 距离容器顶部 100px
left: 500, // 距离容器左侧 100px
width: 100, // 矩形宽度 30px
height: 100, // 矩形高度 30px
fill: 'purple', // 填充
});
fabricCanvas.add(rect2); // 将矩形添加到 canvas 画布里
}
function screenshot() {
const originalTransform = fabricCanvas.viewportTransform;
const imageData = fabricCanvas.toDataURL({
format: 'png',
});
fabricCanvas.viewportTransform = originalTransform;
// 下载
const a = document.createElement('a');
a.href = imageData;
a.download = `fabric-screenshot-${Date.now()}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
function centerSelectedObject() {
if (!selectedObject) {
return false;
}
setCenterFromObject(selectedObject, fabricCanvas);
}
</script>
<style lang="less" scoped>
.canvas-box {
margin-top: 10px;
}
.actions {
margin-top: 10px;
> button {
padding: 0 10px;
border: 1px solid #eee;
margin-right: 10px;
}
}
</style>
js
// import { fabric } from 'fabric-with-erasing'
/**
* 设置画布中心到指定对象中心点上
* @param {Object} obj 指定的对象
*/
export function setCenterFromObject(obj, canvas) {
const objCenter = obj.getCenterPoint();
const viewportTransform = canvas.viewportTransform;
if (
canvas.width === undefined ||
canvas.height === undefined ||
!viewportTransform
)
return;
viewportTransform[4] = canvas.width / 2 - objCenter.x * viewportTransform[0];
viewportTransform[5] = canvas.height / 2 - objCenter.y * viewportTransform[3];
canvas.setViewportTransform(viewportTransform);
canvas.renderAll();
}
/**
* @method zoomToFitObjects
* @description 缩放到显示所有对象
* @param {Object} canvas
*/
export function zoomToFitObjects(canvas) {
if (canvas.getObjects().length < 1) {
return;
}
const x1 = Math.min(...canvas.getObjects().map((obj) => obj.left));
const y1 = Math.min(...canvas.getObjects().map((obj) => obj.top));
const x2 = Math.max(
...canvas.getObjects().map((obj) => obj.left + obj.getScaledWidth())
);
const y2 = Math.max(
...canvas.getObjects().map((obj) => obj.top + obj.getScaledHeight())
);
const height = y2 - y1;
const width = x2 - x1;
const zoom1 = canvas.getHeight() / height;
const zoom2 = canvas.getWidth() / width;
const zoom = zoom1 > zoom2 ? zoom2 : zoom1;
const viewportTransform = [
zoom,
0,
0,
zoom,
(0 - x1) * zoom,
(0 - y1) * zoom,
];
canvas.setViewportTransform(viewportTransform);
}
二、在画布中进行框选创建矩形
源代码
vue
<template>
<fabric-canvas
style="margin-top: 10px"
@init="onFabricCanvasInit"
></fabric-canvas>
</template>
<script setup>
import FabricCanvas from '../canvas.vue';
import bgTransparent from '../../../images/bg-transparent.png';
import useCornerRotateControls from '../controls/rotate';
import useDelControl from '../controls/del';
import useConfirmControl from '../controls/confirm';
import useMouseCreateRect from '../handles/useMouseCreateRect';
import useResizeMask from '../handles/useResizeMask';
import { customDefaultContorls } from '../controls/default';
let fabricCanvas = null;
let fabric = null;
// 使用自定义控件
function useConstomContols(delCallback) {
// 覆盖默认控件
customDefaultContorls();
// 新自定义旋转控件
useCornerRotateControls(fabricCanvas);
// 新增删除控件
useDelControl(fabricCanvas, delCallback);
fabricCanvas.requestRenderAll();
}
async function onFabricCanvasInit(canvas, fabricModule) {
fabricCanvas = canvas;
fabric = fabricModule;
let onCreateRectTypeChange;
// 设置背景
canvas.setBackgroundColor(
{
source: bgTransparent,
repeat: 'repeat',
},
canvas.renderAll.bind(canvas)
);
// 使用自定义控件
useConstomContols(() => {
// 被删除后可以再次生成
setTimeout(() => {
onCreateRectTypeChange && onCreateRectTypeChange('rect');
}, 500);
});
// 即将使用自定义确认控件
const { setControlToObject, removeControlFromObject } = useConfirmControl();
// 可以使用鼠标框选生成矩形
const { typeChange } = useMouseCreateRect(fabricCanvas, (rect) => {
// 为元素设置遮罩层,遮罩层随元素变化
useResizeMask(fabricCanvas, rect);
// 添加确认控件设置
setControlToObject(rect, {
onCancel: () => {
fabricCanvas.remove(rect);
setTimeout(() => {
// 被删除后可以再次生成
typeChange('rect');
}, 500);
},
onConfirm: () => {
fabricCanvas.discardActiveObject();
},
});
// 只能绘制一次,恢复为点选模式
setTimeout(() => {
// 被删除后可以再次生成
typeChange('default');
}, 500);
});
onCreateRectTypeChange = typeChange;
fabricCanvas.renderAll();
}
</script>
js
import { fabric } from 'fabric';
export default function useConfirmControl() {
// 删除按钮样式
const deleteIcon = `data:image/svg+xml,${encodeURIComponent(
'<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1692954890354" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13326" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M617.92 516.096l272 272-101.824 101.824-272-272-272 272-101.856-101.824 272-272-275.008-275.04L241.056 139.2l275.04 275.04 275.04-275.04 101.824 101.824-275.04 275.04z" fill="#F85F4A" p-id="13327"></path></svg>'
)}`;
const deleteImg = document.createElement('img');
deleteImg.src = deleteIcon;
// 确定按钮样式
const collectIcon = `data:image/svg+xml,${encodeURIComponent(
'<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1692954639646" class="icon" viewBox="0 0 1092 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13291" xmlns:xlink="http://www.w3.org/1999/xlink" width="213.28125" height="200"><path d="M445.781333 756.667733L204.8 515.003733l71.953067-72.021333L470.357333 637.1328 835.242667 273.066667l71.953066 71.8848-410.8288 409.873066a33.928533 33.928533 0 0 1-3.6864 3.208534 34.133333 34.133333 0 0 1-46.967466-1.365334z" fill="#00D775" p-id="13292"></path></svg>'
)}`;
const collectImg = document.createElement('img');
collectImg.src = collectIcon;
// 渲染图标
function renderIcon(icon, size) {
return function renderIcon(ctx, left, top, styleOverride, fabricObject) {
ctx.save();
ctx.translate(left, top);
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
ctx.drawImage(icon, -size / 2, -size / 2, size, size);
ctx.restore();
};
}
// 圆角矩形
function createRadiusRect(
ctx,
x,
y,
width,
height,
cornerSize,
bgColor,
borderColor
) {
ctx.beginPath();
ctx.moveTo(x + cornerSize, y);
ctx.arcTo(x + width, y, x + width, y + cornerSize, cornerSize);
ctx.lineTo(x + width, y + height - cornerSize);
ctx.arcTo(
x + width,
y + height,
x + width - cornerSize,
y + height,
cornerSize
);
ctx.lineTo(x + cornerSize, y + height);
ctx.arcTo(x, y + height, x, y + height - cornerSize, cornerSize);
ctx.lineTo(x, y + cornerSize);
ctx.arcTo(x, y, x + cornerSize, y, cornerSize);
ctx.closePath();
ctx.fillStyle = bgColor;
// ctx.strokeStyle = borderColor
ctx.fill();
// ctx.stroke()
}
// 根据矩形状态更新按钮区域的位置
function checkEmptyPosition(object, emptyPositionChange) {
const checkPosition = () => {
const bottom = object.canvas.height - object.top - object.height;
if (bottom > object.top) {
emptyPositionChange('bottom');
} else {
emptyPositionChange('top');
}
};
// 更新
const updateEvents = [
'scaled',
'selected',
'modified',
'scaling',
'moving',
'rotating',
'skewing',
];
updateEvents.forEach((key) => {
object.on(key, checkPosition);
});
// 移除
// const removeEvents = ['removed', 'deselected']
// removeEvents.forEach((key) => {
// object.on(key, removeMask)
// })
}
// 为对象设置自定义控件
function setControlToObject(object, options) {
const bg = new fabric.Control({
x: 0.5,
y: -0.5,
offsetY: -25,
offsetX: -31,
cursorStyle: 'default', // 鼠标移到控件时的指针样式
render: function (ctx, left, top, styleOverride, fabricObject) {
ctx.save();
const width = 62;
const height = 23;
ctx.translate(left, top);
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
createRadiusRect(
ctx,
-width / 2,
-height / 2,
width,
height,
3,
'rgba(0,0,0,0.5)',
'rgba(0,0,0,0.5)'
);
ctx.restore();
},
cornerSize: 24,
});
const btnCancel = new fabric.Control({
x: 0.5,
y: -0.5,
offsetY: -25,
offsetX: -46,
cursorStyle: 'pointer',
mouseUpHandler: options.onCancel,
render: renderIcon(deleteImg, 16),
cornerSize: 24,
});
const btnConfirm = new fabric.Control({
x: 0.5,
y: -0.5,
offsetY: -25,
offsetX: -16,
cursorStyle: 'pointer',
mouseUpHandler: options.onConfirm,
render: renderIcon(collectImg, 24),
cornerSize: 24,
});
if (object.controls.deleteControl) {
object.controls.deleteControl.visible = false;
}
object.set({
controls: {
confirmBg: bg,
confirmBtnCancel: btnCancel,
confirmBtnConfirm: btnConfirm,
},
});
// 自动更换控件的位置
checkEmptyPosition(object, (position) => {
if (position === 'bottom') {
object.controls.confirmBg.y = 0.5;
object.controls.confirmBg.offsetY = 25;
object.controls.confirmBtnCancel.y = 0.5;
object.controls.confirmBtnCancel.offsetY = 25;
object.controls.confirmBtnConfirm.y = 0.5;
object.controls.confirmBtnConfirm.offsetY = 25;
} else {
object.controls.confirmBg.y = -0.5;
object.controls.confirmBg.offsetY = -25;
object.controls.confirmBtnCancel.y = -0.5;
object.controls.confirmBtnCancel.offsetY = -25;
object.controls.confirmBtnConfirm.y = -0.5;
object.controls.confirmBtnConfirm.offsetY = -25;
}
});
}
function removeControlFromObject(object) {
delete object.controls.confirmBg;
delete object.controls.confirmBtnCancel;
delete object.controls.confirmBtnConfirm;
if (object.controls.deleteControl) {
object.controls.deleteControl.visible = true;
}
}
return {
setControlToObject,
removeControlFromObject,
};
}
js
import { fabric } from 'fabric';
import useClipMask from './useClipMask';
export function getRectByPoints(startPoint, endPoint) {
// 矩形参数计算(前面总结的4条公式)
const top = Math.min(startPoint.y, endPoint.y);
const left = Math.min(startPoint.x, endPoint.x);
const width = Math.abs(startPoint.x - endPoint.x);
const height = Math.abs(startPoint.y - endPoint.y);
return {
top,
left,
width,
height,
};
}
// 创建矩形
export function createRect(position) {
// 矩形对象
return new fabric.Rect({
...position,
fill: 'transparent', // 填充色:透明
stroke: '#3E63F1', // 边框颜色:主题色
shadow: '1px 1px 2px rgba(255, 255, 255, 0.5)', // 阴影
inverted: true,
absolutePositioned: true,
});
}
export default function (canvas, onRectInsert) {
// 当前操作模式(默认 || 创建矩形)
let currentType = 'rect';
// 画布操作类型切换
function typeChange(opt) {
currentType = opt;
switch (opt) {
case 'default': // 默认框选模式
canvas.selection = true; // 允许框选
canvas.selectionColor = 'rgba(100, 100, 255, 0.3)'; // 选框填充色:半透明的蓝色
canvas.selectionBorderColor = 'rgba(255, 255, 255, 0.3)'; // 选框边框颜色:半透明灰色
canvas.skipTargetFind = false; // 允许选中
break;
case 'rect': // 创建矩形模式
canvas.selectionColor = 'transparent'; // 选框填充色:透明
canvas.selectionBorderColor = 'rgba(0, 0, 0, 0.2)'; // 选框边框颜色:透明度很低的黑色(看上去是灰色)
canvas.skipTargetFind = true; // 禁止选中
break;
}
}
typeChange('rect');
// 起始和结束坐标
let startPoint = null;
let endPoint = null;
let pressing = false;
canvas.on('mouse:down', canvasMouseDown);
canvas.on('mouse:move', canvasMouseMove);
canvas.on('mouse:up', canvasMouseUp);
// 鼠标在画布上按下
function canvasMouseDown(e) {
pressing = true;
// 鼠标左键按下时,将当前坐标 赋值给 startPoint。{x: xxx, y: xxx} 的格式
startPoint = e.absolutePointer;
}
// 鼠标在画布上移动
let mask = null;
function canvasMouseMove(e) {
if (pressing && currentType === 'rect') {
const currentPoint = e.absolutePointer;
const position = getRectByPoints(startPoint, currentPoint);
const rect = createRect(position);
if (!mask) {
mask = useClipMask(canvas);
}
mask.updateMask(rect);
}
}
// 鼠标在画布上松开
function canvasMouseUp(e) {
pressing = false;
// 清除绘制时的遮罩层
if (mask) {
mask.removeMask();
mask = null;
}
// 绘制矩形的模式下,才执行下面的代码
if (currentType === 'rect') {
// 松开鼠标左键时,将当前坐标 赋值给 endPoint
endPoint = e.absolutePointer;
const canCreate =
endPoint.x !== startPoint.x || endPoint.y !== startPoint.y;
if (!canCreate) {
return false;
}
// 如果点击和松开鼠标,都是在同一个坐标点,不会生成矩形
if (JSON.stringify(startPoint) !== JSON.stringify(endPoint)) {
// 调用 创建矩形 的方法
const position = getRectByPoints(startPoint, endPoint);
const rect = createRect(position);
// 返回
onRectInsert && onRectInsert(rect);
// 将矩形添加到画布上
canvas.add(rect);
// 设置聚焦
canvas.setActiveObject(rect);
}
// 清空绘制数据
startPoint = null;
endPoint = null;
}
}
return {
typeChange,
};
}
js
import useClipMask from './useClipMask';
export default function (canvas, object) {
// 自定义遮罩层,用于裁剪时显示黑色半透明背景
const { updateMask, removeMask } = useClipMask(canvas);
// 更新
const updateEvents = [
'scaled',
'selected',
'modified',
'scaling',
'moving',
'rotating',
'skewing',
];
updateEvents.forEach((key) => {
object.on(key, () => {
updateMask(object);
});
});
// 移除
const removeEvents = ['removed', 'deselected'];
removeEvents.forEach((key) => {
object.on(key, removeMask);
});
}
三、实现文本的垂直居中
源代码
vue
<template>
<fabric-canvas @init="onFabricCanvasInit"></fabric-canvas>
<div class="actions">
<button type="button" @click="verticalAlign('top')">垂直-顶部</button>
<button type="button" @click="verticalAlign('middle')">垂直-居中</button>
<button type="button" @click="verticalAlign('bottom')">垂直-底部</button>
<button type="button" @click="textAlign('left')">水平-左</button>
<button type="button" @click="textAlign('center')">水平-居中</button>
<button type="button" @click="textAlign('right')">水平-右</button>
</div>
</template>
<script setup>
import bgTransparent from '../../../images/bg-transparent.png';
import FabricCanvas from '../canvas.vue';
let fabricCanvas = null;
let fabric = null;
async function onFabricCanvasInit(canvas, fabricModule) {
fabricCanvas = canvas;
fabric = fabricModule;
// 设置背景
canvas.setBackgroundColor(
{
source: bgTransparent,
repeat: 'repeat',
},
canvas.renderAll.bind(canvas)
);
const width = 150;
const height = 100;
// 文本
const textBox = new fabric.Text('hello world', {
fontSize: 14,
splitByGrapheme: true,
});
// 容器
const rect = new fabric.Rect({
width: width,
height: height,
fill: 'transparent',
});
// 建组
const group = new fabric.Group([rect, textBox], {
top: 100,
left: 100,
width: width,
height: height,
});
textBox.set('width', width);
fabricCanvas.add(group);
}
const groupKeys = [
'instance',
'top',
'left',
'width',
'height',
'scaleX',
'scaleY',
];
// 更新属性
function updateObjectProperty(config, object) {
if (object) {
Object.keys(config).forEach((key) => {
const value = config[key];
object.set(key, value);
});
}
}
function updateTextProperty(config, object) {
// 组内的容器和文本
const rectObject = object.item(0);
const textObject = object.item(1);
// 应始终具有宽高
if (!config.width) {
config.width = object.get('scaleX') * object.get('width');
}
if (!config.height) {
config.height = object.get('scaleY') * object.get('height');
}
// 取组的属性进行设置
const groupConfig = {};
groupKeys.forEach((key) => {
if (typeof config[key] !== 'undefined') {
groupConfig[key] = config[key];
}
});
// 取文本的属性进行设置
const textConfig = {};
for (const key in config) {
if (!groupConfig[key]) {
textConfig[key] = config[key];
}
}
// 特殊处理属性:
if (typeof config.visible !== 'undefined') {
groupConfig.visible = config.visible;
textConfig.visible = config.visible;
}
updateObjectProperty(groupConfig, rectObject);
updateObjectProperty(textConfig, textObject);
// 更新文本在组中的位置
const updateTextVerticalAlign = (type) => {
if (type === 'top') {
updateObjectProperty(
{
top: -object.height / 2,
},
textObject
);
}
if (type === 'middle') {
updateObjectProperty(
{
top: -textObject.height / 2,
},
textObject
);
}
if (type === 'bottom') {
updateObjectProperty(
{
top: object.height / 2 - textObject.height,
},
textObject
);
}
};
// 根据组的大小,更新组内矩形、文本大小位置
if (typeof groupConfig.width !== 'undefined') {
updateObjectProperty(
{
scaleX: 1,
width: object.width,
left: -object.width / 2,
},
rectObject
);
updateObjectProperty(
{
scaleX: 1,
width: object.width,
left: -object.width / 2,
},
textObject
);
}
if (typeof groupConfig.height !== 'undefined') {
updateObjectProperty(
{
scaleY: 1,
height: object.height,
top: -object.height / 2,
},
rectObject
);
updateTextVerticalAlign(textObject.verticalAlign);
}
}
function verticalAlign(type) {
const activeObjects = fabricCanvas.getActiveObjects();
const group = activeObjects[0];
if (!group) {
return false;
}
updateTextProperty(
{
verticalAlign: type,
},
group
);
fabricCanvas.renderAll();
}
function textAlign(type) {
const activeObjects = fabricCanvas.getActiveObjects();
const group = activeObjects[0];
if (!group) {
return false;
}
updateTextProperty(
{
textAlign: type,
},
group
);
fabricCanvas.renderAll();
}
</script>
<style lang="less" scoped>
.canvas-box {
margin-top: 10px;
}
.actions {
margin-top: 10px;
> button {
padding: 0 10px;
border: 1px solid #eee;
margin-right: 10px;
}
}
</style>