Fabric

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: 1, // 边框宽度
            left: 0, // 图片离画板左侧的距离
            top: 0, // 图片离画板顶部的距离
            scaleX: .5, // x轴缩放比例
            scaleY: .5, // y轴缩放比例
            minScaleLimit: 0.1, // 最小缩放限制
            lockScalingFlip: true, // 限制镜像翻转
        });
        // 监听缩放事件 设置最大缩放大小
        img.on('scaling', function (e) {
             const currentScale = img.scaleX;
             const maxScale = 1.9;

             if (currentScale > maxScale) {
                 const scaleRatio = maxScale / currentScale;
                 img.scaleX *= scaleRatio;
                 img.scaleY *= scaleRatio;
             }
         });

        // 添加唯一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即可。

同时可以通过设置图片对象的索引值更改显示层级,类似cssz-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>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Raccom

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值