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) return
callBack?.(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
});
// 添加唯一id
img.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 对象, 不能用ref
const selectImgId = ref() // 保存点击时的图片id
watchEffect(() => {
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) return
callBack?.(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
});
// 添加唯一id
img.customId = generateUniqueId();
canvas.add(img);
});
}
onMounted(() => {
fabricInit()
addImg(new URL('../assets/图片地址.png', import.meta.url).href)
document.addEventListener('click', getImgId);
});
onUnmounted(() => {
canvas = null
})
</script>