Fabric
Fabric.js是一个非常好用的Javascript HTML5 canvas库,封装了canvas原生较为复杂的api,在canvas元素的顶部提供交互式对象模型,用于实现图片的变形旋转拖拉拽等功能。
在线demo: 官网链接
下载
npm install fabric --save
或
yarn add fabric
期间下载到canvas
会用时比较长,推荐使用npm
,用yarn
有时候会安装失败
初始化
如果无需设置图片边框 也可以将canvas.on
整个函数删除
<div class="section" :style="{width: `800px`, height: `500px`}" ref="canvasSectionRef"><canvas ref="canvasRef" width="800" height="500" id="fabricCanvas"></canvas>
</div>
// vue3
import {onMounted, onUnmounted, ref} from 'vue';
import {fabric} from 'fabric';const canvasSectionRef = ref(null) // canvas 父元素引用
const canvasRef = ref(null) // canvas 的引用
let canvas // 用于保存fabric canvas 对象, 不能用ref/*** @description 根据 id 在 canvas 对象中查找出对应的图片对象和其索引(索引同时也是显示层级 即 z-index),在callBack中做对应处理* @param id {string} 图片对象的id* @param callBack {(selectedObject: Object, zIndex: number) => any} 查找到对象后的回调* @returns {undefined}*/
const editImageState = (id, callBack) => {const objList = canvas.getObjects()const zIndex = objList.findIndex((obj) => {return obj.customId === id});const selectedObject = objList[zIndex]if (!selectedObject) returncallBack?.(selectedObject, zIndex)
}/*** @description* 初始化fabric canvas 对象 设置边框颜色 和 四个角控制点的样式* - 点击时显示蓝色虚线边框* - 锁定时点击为灰色虚线边框* - 默认没有选中为透明边框*/
const fabricInit = () => {let selectedObjects = {}; // 使用对象来存储不同图片的选中状态 存储方式为 `key - value` -> `id - boolean`canvas = new fabric.Canvas(canvasRef.value);// 自定义控制点样式fabric.Object.prototype.set({cornerStyle: 'square',cornerColor: '#4191fa',cornerSize: 10,transparentCorners: false,cornerStrokeColor: '#fff',cornerStrokeWidth: 2});// 点击时图片添加边框 不需要可以去除 canvas.on('mouse:down', function (options) {// 当前鼠标点击时获取的对象const targetObject = canvas.findTarget(options.e);if (targetObject && targetObject.type === 'image') {// 获取选中图片的id 用于设置边框样式const currentId = targetObject.customId;if (selectedObjects[currentId]) {// 当前点击的图片已经是选中状态,不执行任何操作} else {// 取消显示当前所有图片的边框 (先全部取消 再按id查找当前点击的图片设置为选中)for (const id in selectedObjects) {// 处理取消选中的逻辑if (selectedObjects.hasOwnProperty(id) && selectedObjects[id]) {editImageState(id, (imageItem) => {imageItem.set({stroke: 'transparent',strokeWidth: 2});})}}selectedObjects = {};// 将当前点击的图片设置为选中状态 显示边框selectedObjects[currentId] = true;editImageState(currentId, (imageItem) => {let color = imageItem.selectable ? '#4191fa' : '#ccc'imageItem.set({stroke: color, // 显示边框strokeWidth: 2 // 边框宽度为2});})}} else {// 点击的是画布空白处,清空选中对象for (const id in selectedObjects) {if (selectedObjects.hasOwnProperty(id) && selectedObjects[id]) {editImageState(id, (imageItem) => {imageItem.set({stroke: 'transparent', // 隐藏边框strokeWidth: 2 // 边框宽度为0});})}}selectedObjects = {};}// 处理完成重新渲染 否则不是马上生效canvas.renderAll();});
}onMounted(() => {fabricInit()
});onUnmounted(() => {canvas = null
})
添加图片
添加图片时需要添加唯一id,方便后续根据id对图片进行操作
// 生成唯一的 ID 用于图片升成
function generateUniqueId() {return 'id_' + +new Date();
}/*** @description 根据url地址在canvas对象中添加图片* @param url {string} 图片地址 在线 or 离线*/
const addImg = (url) => {fabric.Image.fromURL(url, function (img) {img.set({selectable: true, // 禁用选中效果 -> true为不禁用 | false为禁用hasControls: true, // 禁用控制点 -> true为不禁用 | false为禁用borderColor: 'transparent', // 边框颜色strokeDashArray: [5, 5],strokeWidth: 2});// 添加唯一idimg.customId = generateUniqueId();canvas.add(img);});
}
新增了addImg
这个函数后就可以在onMounted
中使用addImg(图片地址)
,在canvas中就可以看到效果。
锁定 / 解锁图片(不可选中不可移动)
/*** @description 将canvas对象中对应id的图片对象设置为禁用 并设置边框颜色灰色* @param id {string} 图片对象id*/
const lockImg = (id) => {editImageState(id, (imageItem) => {imageItem.set({selectable: false, // 禁用选中效果hasControls: false, // 禁用控制点stroke: '#ccc',});canvas.discardActiveObject()canvas.renderAll()})
}/*** @description 将canvas对象中对应id的图片对象设置取消禁用 并设置边框颜色蓝色* @param id {string} 图片对象id*/
const unLockImg = (id) => {editImageState(id, (imageItem) => {imageItem.set({selectable: true, // 禁用选中效果hasControls: true, // 禁用控制点stroke: '#4191fa'});canvas.renderAll()})
}
删除图片
/*** @description 将canvas对象中对应id的图片对象删除* @param id {string} 图片对象id*/
const deleteImg = (id) => {editImageState(id, (imageItem) => {canvas.remove(imageItem);canvas.renderAll()})
}
获取图片id
根据id进行修改删除图片的方法都有了,然后就是获取图片id的方法
const selectImgId = ref() // 保存点击时的图片id// 判断元素是否为Canvas元素或其子元素
const isCanvasElement = (element) => {return element instanceof HTMLCanvasElement || element.closest('canvas') !== null;
}/*** @description 查找canvas所有生成的对象(图片)中 点击位置上的对象* @param x {number} 鼠标点击的相对于canvas的 x 坐标* @param y {number} 鼠标点击的相对于canvas的 Y 坐标* @returns {Object|null} 点击处的对象 没有则返回null*/
const findClickedObject = (x, y) => {// 遍历Canvas上的所有对象,判断点击位置是否在对象范围内for (let i = canvas.getObjects().length - 1; i >= 0; i--) {const obj = canvas.item(i);if (obj.containsPoint({x: x, y: y})) {return obj;}}return null;
}const getImgId = (event) => {if (isCanvasElement(event.target)) {// 获取鼠标点击位置相对于 Canvas 元素的坐标const canvasRect = canvasSectionRef.value.getBoundingClientRect();const x = event.clientX - canvasRect.left;const y = event.clientY - canvasRect.top;// 查找点击位置上的对象const targetObject = findClickedObject(x, y);if (targetObject && targetObject.type === 'image') {// 选中的图片id保存起来selectImgId.value = targetObject.customId}}
}onMounted(() => {document.addEventListener('click', getImgId);
});
当点击鼠标左键时会获取到图片id并保存在selectImgId
变量中,后续需要设置锁定或删除,只需要调用对应的函数传入selectImgId
即可。
同时可以通过设置图片对象的索引值更改显示层级,类似css
的z-index
,这里不过多赘述。
完整示例代码
<template><div class="section" :style="{width: `800px`, height: `500px`}" ref="canvasSectionRef"><canvas ref="canvasRef" width="800" height="500" id="fabricCanvas"></canvas></div>
</template><script setup>
import {onMounted, onUnmounted, ref, watchEffect} from 'vue';
import {fabric} from 'fabric';const canvasSectionRef = ref(null) // canvas 父元素引用
const canvasRef = ref(null) // canvas 的引用
let canvas // 用于保存fabric canvas 对象, 不能用refconst selectImgId = ref() // 保存点击时的图片idwatchEffect(() => {console.log('selectImgId 更改了', selectImgId.value)
})/*** @description 根据 id 在 canvas 对象中查找出对应的图片对象和其索引(索引同时也是显示层级 即 z-index),在callBack中做对应处理* @param id {string} 图片对象的id* @param callBack {(selectedObject: Object, zIndex: number) => any} 查找到对象后的回调* @returns {undefined}*/
const editImageState = (id, callBack) => {const objList = canvas.getObjects()const zIndex = objList.findIndex((obj) => {return obj.customId === id});const selectedObject = objList[zIndex]if (!selectedObject) returncallBack?.(selectedObject, zIndex)
}// 判断元素是否为Canvas元素或其子元素
const isCanvasElement = (element) => {return element instanceof HTMLCanvasElement || element.closest('canvas') !== null;
}/*** @description 查找canvas所有生成的对象(图片)中 点击位置上的对象* @param x {number} 鼠标点击的相对于canvas的 x 坐标* @param y {number} 鼠标点击的相对于canvas的 Y 坐标* @returns {Object|null} 点击处的对象 没有则返回null*/
const findClickedObject = (x, y) => {// 遍历Canvas上的所有对象,判断点击位置是否在对象范围内for (let i = canvas.getObjects().length - 1; i >= 0; i--) {const obj = canvas.item(i);if (obj.containsPoint({x: x, y: y})) {return obj;}}return null;
}const getImgId = (event) => {if (isCanvasElement(event.target)) {// 获取鼠标点击位置相对于 Canvas 元素的坐标const canvasRect = canvasSectionRef.value.getBoundingClientRect();const x = event.clientX - canvasRect.left;const y = event.clientY - canvasRect.top;// 查找点击位置上的对象const targetObject = findClickedObject(x, y);if (targetObject && targetObject.type === 'image') {// 选中的图片id保存起来selectImgId.value = targetObject.customId}}
}/*** @description* 初始化fabric canvas 对象 设置边框颜色 和 四个角控制点的样式* - 点击时显示蓝色虚线边框* - 锁定时点击为灰色虚线边框* - 默认没有选中为透明边框*/
const fabricInit = () => {let selectedObjects = {}; // 使用对象来存储不同图片的选中状态 存储方式为 `key - value` -> `id - boolean`canvas = new fabric.Canvas(canvasRef.value);// 自定义控制点样式fabric.Object.prototype.set({cornerStyle: 'square',cornerColor: '#4191fa',cornerSize: 10,transparentCorners: false,cornerStrokeColor: '#fff',cornerStrokeWidth: 2});// 点击时图片添加边框 不需要可以去除canvas.on('mouse:down', function (options) {// 当前鼠标点击时获取的对象const targetObject = canvas.findTarget(options.e);if (targetObject && targetObject.type === 'image') {// 获取选中图片的id 用于设置边框样式const currentId = targetObject.customId;if (selectedObjects[currentId]) {// 当前点击的图片已经是选中状态,不执行任何操作} else {// 取消显示当前所有图片的边框 (先全部取消 再按id查找当前点击的图片设置为选中)for (const id in selectedObjects) {// 处理取消选中的逻辑if (selectedObjects.hasOwnProperty(id) && selectedObjects[id]) {editImageState(id, (imageItem) => {imageItem.set({stroke: 'transparent',strokeWidth: 2});})}}selectedObjects = {};// 将当前点击的图片设置为选中状态 显示边框selectedObjects[currentId] = true;editImageState(currentId, (imageItem) => {let color = imageItem.selectable ? '#4191fa' : '#ccc'imageItem.set({stroke: color, // 显示边框strokeWidth: 2 // 边框宽度为2});})}} else {// 点击的是画布空白处,清空选中对象for (const id in selectedObjects) {if (selectedObjects.hasOwnProperty(id) && selectedObjects[id]) {editImageState(id, (imageItem) => {imageItem.set({stroke: 'transparent', // 隐藏边框strokeWidth: 2 // 边框宽度为0});})}}selectedObjects = {};}// 处理完成重新渲染 否则不是马上生效canvas.renderAll();});
}// 生成唯一的 ID 用于图片升成
function generateUniqueId() {return 'id_' + +new Date();
}/*** @description 根据url地址在canvas对象中添加图片* @param url {string} 图片地址 在线 or 离线*/
const addImg = (url) => {fabric.Image.fromURL(url, function (img) {img.set({selectable: true, // 禁用选中效果 -> true为不禁用 | false为禁用hasControls: true, // 禁用控制点 -> true为不禁用 | false为禁用borderColor: 'transparent', // 边框颜色strokeDashArray: [5, 5],strokeWidth: 2});// 添加唯一idimg.customId = generateUniqueId();canvas.add(img);});
}onMounted(() => {fabricInit()addImg(new URL('../assets/图片地址.png', import.meta.url).href)document.addEventListener('click', getImgId);
});onUnmounted(() => {canvas = null
})
</script>