Fabric

Fabric

Fabric.js是一个非常好用的Javascript HTML5 canvas库,封装了canvas原生较为复杂的api,在canvas元素的顶部提供交互式对象模型,用于实现图片的变形旋转拖拉拽等功能。

在线demo: 官网链接

下载

shell 复制代码
npm install fabric --save

shell 复制代码
yarn add fabric

期间下载到canvas会用时比较长,推荐使用npm,用yarn有时候会安装失败

初始化

如果无需设置图片边框 也可以将canvas.on整个函数删除

html 复制代码
<div class="section" :style="{width: `800px`, height: `500px`}" ref="canvasSectionRef">
    <canvas ref="canvasRef" width="800" height="500" id="fabricCanvas"></canvas>
</div>
js 复制代码
// 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对图片进行操作

js 复制代码
// 生成唯一的 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中就可以看到效果。

锁定 / 解锁图片(不可选中不可移动)

js 复制代码
/**
 * @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()
    })
}

删除图片

js 复制代码
/**
 * @description 将canvas对象中对应id的图片对象删除
 * @param id {string} 图片对象id
 */
const deleteImg = (id) => {
    editImageState(id, (imageItem) => {
        canvas.remove(imageItem);
        canvas.renderAll()
    })
}

获取图片id

根据id进行修改删除图片的方法都有了,然后就是获取图片id的方法

js 复制代码
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即可。

同时可以通过设置图片对象的索引值更改显示层级,类似cssz-index,这里不过多赘述。

完整示例代码

js 复制代码
<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>
相关推荐
开心工作室_kaic17 分钟前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿36 分钟前
webWorker基本用法
前端·javascript·vue.js
cy玩具1 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
清灵xmf1 小时前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
小白学大数据2 小时前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
qq_390161772 小时前
防抖函数--应用场景及示例
前端·javascript
334554322 小时前
element动态表头合并表格
开发语言·javascript·ecmascript
John.liu_Test2 小时前
js下载excel示例demo
前端·javascript·excel
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事2 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro