基于Vue3和Fabric.js实现交互式多边形绘制功能
作者:jnsytgsyqjgm
需求概述
1.基础画布设置
加载背景图片
创建与图片尺寸匹配的Fabric.js画布
2.多边形绘制功能
支持固定边数多边形绘制(默认5边形)
通过点击添加顶点
限制最大顶点数,达到最大数后只能闭合
3.视觉反馈系统
绘制顶点可视化(白色圆点,首个点为红边框)
点与点之间的连接线(蓝色)
实时多边形预览(半透明填充)
4.智能交互
起点闭合检测(鼠标靠近起点时自动吸附)
距离计算(20像素范围内检测接近)
绘制完成时的状态转换
技术实现详解
1. 基础架构
项目使用Vue 3的<script setup>语
import { ref, onMounted, reactive } from 'vue'
import { fabric } from 'fabric'
import { ElMessage } from 'element-plus'
2. 画布初始化
在组件挂载时,通过以下步骤初始化画布:
- 加载背景图片
- 创建Fabric画布,尺寸与图片一致
- 将图片设置为背景
onMounted(() => {
const imgElement = new Image();
imgElement.src = '/test.jpg'; // 图片路径,注意使用公共路径 src/public/test.jpg
imgElement.onload = function() {
const imgWidth = imgElement.width;
const imgHeight = imgElement.height;
const canvas = new fabric.Canvas(canvasRef.value, {
width: imgWidth,
height: imgHeight
});
// 设置背景图片
const bgImage = new fabric.Image(imgElement, {
scaleX: 1,
scaleY: 1,
selectable: false,
});
canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas));
// 后续代码...
};
});
3. 多边形绘制核心逻辑
多边形绘制实现了以下核心功能:
3.1 状态管理
let isDrawing = false; // 绘制状态 let points = []; // 多边形顶点 let pointObjects = []; // 顶点可视化对象 let lineObjects = []; // 线段对象 let finalPolygon = null; // 最终多边形 let activeLine = null; // 当前活动线 let activeShape = null; // 动态多边形
3.2 顶点可视化
使用Fabric.js的Circle对象实现顶点可视化,保证顶点精确定位:
const createPoint = (x, y, isFirst = false) => {
const circle = new fabric.Circle({
radius: 5,
fill: 'white', // 白色背景
stroke: isFirst ? 'red' : 'rgb(201, 201, 201)', // 第一个点红色边框,其余灰色
strokeWidth: 1,
selectable: false,
originX: 'center',
originY: 'center',
left: x,
top: y
});
canvas.add(circle);
return circle;
};
注意这里的实现细节:
- 使用originX/originY为'center'确保圆心精确定位在点击位置
- left/top直接使用点击坐标,不再额外减去半径值
3.3 线段绘制
连接顶点的线段使用Fabric.js的Line对象实现:
const createLine = (fromX, fromY, toX, toY) => {
const line = new fabric.Line([fromX, fromY, toX, toY], {
stroke: 'rgb(51, 164, 255)',
strokeWidth: 2,
selectable: false
});
canvas.add(line);
return line;
};
3.4 动态多边形生成
generatePolygon函数是实现实时预览效果的核心,该函数会在鼠标移动时被调用,创建从最后一个固定点到当前鼠标位置的动态线段和实时更新的多边形:
const generatePolygon = (mousePointer) => {
// 确保处于绘制状态且至少有一个点
if (!isDrawing || points.length < 1) return;
// 清除旧的活动线
if (activeLine) {
canvas.remove(activeLine);
activeLine = null;
}
// 清除旧的活动形状
if (activeShape) {
canvas.remove(activeShape);
activeShape = null;
}
// 创建动态线段 - 从最后一个点到当前鼠标位置
const lastPoint = points[points.length - 1];
activeLine = new fabric.Line(
[lastPoint.x, lastPoint.y, mousePointer.x, mousePointer.y],
{
stroke: 'rgb(51, 164, 255)',
strokeWidth: 2,
selectable: false
}
);
canvas.add(activeLine);
// 如果有至少2个点,创建动态多边形
if (points.length >= 2) {
// 创建包含所有点和当前鼠标位置的点数组
let polygonPoints = [...points];
// 如果鼠标接近第一个点,使用第一个点作为闭合点
if (isNearFirstPoint(mousePointer.x, mousePointer.y)) {
polygonPoints.push(firstPoint);
} else {
polygonPoints.push({ x: mousePointer.x, y: mousePointer.y });
}
// 创建动态多边形
activeShape = new fabric.Polygon(
polygonPoints.map(p => ({ x: p.x, y: p.y })),
{
fill: 'rgb(232, 241, 249)',
stroke: 'rgb(232, 241, 249)',
strokeWidth: 1,
selectable: false,
globalCompositeOperation: 'multiply' // 使用混合模式而不是透明度
}
);
// 添加到画布
canvas.add(activeShape);
// 确保背景图在最下层
activeShape.sendToBack();
bgImage.sendToBack();
}
canvas.renderAll();
};
动态多边形生成技术细节分析:
1. 清除历史状态: 每次鼠标移动时,先清除之前的活动线和活动多边形,避免重复渲染
2. 动态线段创建:
- 使用Fabric.js的Line对象创建从最后固定点到鼠标当前位置的线段
- 设置线段颜色为蓝色(rgb(51, 164, 255)),提供明显的视觉反馈
3. 多边形预览生成:
- 当已有至少2个点时,创建包含所有固定点和当前鼠标位置的临时点数组
- 通过isNearFirstPoint函数检测鼠标是否靠近起点,实现"吸附"效果
- 如果鼠标接近起点,使用起点坐标替代鼠标坐标,视觉上表示可以闭合
4. 多边形样式设置:
- 使用浅蓝色填充(rgb(232, 241, 249))和相同颜色的边框
- 使用globalCompositeOperation: 'multiply'实现半透明效果,使背景图仍然可见
- 多边形不可选择,确保不会干扰绘制流程
5. 层级管理:
- 使用activeShape.sendToBack()确保多边形在下层
- 使用bgImage.sendToBack()确保背景图始终在最底层
- 这种层级管理确保了可视化元素的正确叠加顺序
4. 事件处理
4.1 鼠标点击事件
鼠标点击事件处理是多边形绘制的核心逻辑,处理启动绘制、添加顶点和完成多边形等关键操作:
canvas.on('mouse:down', function(o) {
const pointer = canvas.getPointer(o.e);
// 检查是否点击在现有对象上
if (o.target && o.target.type === 'polygon' && !isDrawing) {
// 如果点击了现有多边形且不在绘制模式,只进行选择
return;
}
// 如果是第一次点击,开始绘制
if (!isDrawing) {
// 初始化绘制状态
isDrawing = true;
points = [];
pointObjects = [];
lineObjects = [];
// 记录第一个点
firstPoint = { x: pointer.x, y: pointer.y };
points.push(firstPoint);
// 创建第一个点的可视标记(红色表示起点)
const firstPointObject = createPoint(pointer.x, pointer.y, true);
pointObjects.push(firstPointObject);
showMessage(`开始绘制${sidesCount.value}边形,请继续点击确定顶点位置`, 'info');
} else {
console.log('当前点数:', points.length, '最大点数:', sidesCount.value);
// 检查是否点击了第一个点(闭合多边形)
if (isNearFirstPoint(pointer.x, pointer.y)) {
// 如果已经有至少3个点且点击接近第一个点,闭合多边形
if (points.length >= 5) {
completePolygon();
} else {
showMessage(`需要${sidesCount.value}个点才能闭合多边形。`, 'warning');
}
} else {
// 只有在未达到最大点数时才添加新点
if (points.length < sidesCount.value) {
// 继续添加点
const newPoint = { x: pointer.x, y: pointer.y };
points.push(newPoint);
// 创建点的可视标记
const pointObject = createPoint(pointer.x, pointer.y);
pointObjects.push(pointObject);
// 添加永久连接线
if (points.length >= 2) {
const lastIndex = points.length - 1;
const line = createLine(
points[lastIndex - 1].x, points[lastIndex - 1].y,
points[lastIndex].x, points[lastIndex].y
);
lineObjects.push(line);
}
// 如果刚好达到最大点数,提示用户
if (points.length === sidesCount.value) {
showMessage(`已达到最大点数${sidesCount.value},请点击第一个点完成绘制`, 'warning');
}
} else {
// 已达到最大点数,点击无效,只能点击起点完成绘制
showMessage(`已达到最大点数${sidesCount.value},请点击第一个点完成绘制`, 'warning');
}
// 强制更新画布
canvas.renderAll();
}
}
});
鼠标点击事件技术细节分析:
1. 事件上下文解析:
- 使用canvas.getPointer(o.e)获取鼠标在画布上的准确坐标
- 通过o.target检测点击是否落在现有对象上,防止在已有多边形上开始新绘制
2. 绘制状态初始化:
- 首次点击时,重置所有状态变量(isDrawing, points, pointObjects, lineObjects)
- 创建第一个点并用红色标记,区别于后续点
- 通过消息提示告知用户已开始绘制
3. 多种点击场景处理:
- 区分三种主要场景:首次点击、点击第一个点(闭合)、添加新点
- 使用状态变量isDrawing区分是否处于绘制模式
4. 闭合多边形逻辑:
- 使用isNearFirstPoint函数检测点击是否接近起点
- 当点击接近第一个点且已有足够的点(>=5)时,通过completePolygon()函数完成多边形
- 如果点不足,显示警告消息指导用户
5. 顶点数量管理:
- 通过sidesCount.value控制多边形的最大顶点数
- 实现严格的顶点数限制,达到最大数后只允许闭合操作
- 根据不同阶段提供相应的用户提示
6. 可视化反馈:
- 每个点都有可视化标记(白色圆形带有边框)
- 点与点之间有颜色鲜明的连接线
- 每次操作后立即更新画布(canvas.renderAll())
4.2 鼠标移动事件
实时更新动态线段和多边形形状:
canvas.on('mouse:move', function(o) {
if (!isDrawing) return;
const pointer = canvas.getPointer(o.e);
generatePolygon(pointer);
});
4.3 键盘事件
支持Esc键取消绘制:
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && isDrawing) {
// 清除所有临时元素
if (activeLine) {
canvas.remove(activeLine);
}
if (activeShape) {
canvas.remove(activeShape);
}
pointObjects.forEach(point => {
if (point) canvas.remove(point);
});
lineObjects.forEach(line => {
if (line) canvas.remove(line);
});
// 重置状态
points = [];
pointObjects = [];
lineObjects = [];
activeLine = null;
activeShape = null;
isDrawing = false;
firstPoint = null;
canvas.renderAll();
showMessage('已取消绘制', 'info');
}
});
5. 完成多边形绘制
当用户点击接近起点时,多边形绘制完成:
const completePolygon = () => {
if (points.length < 3) {
showMessage('至少需要3个点才能形成多边形', 'warning');
return;
}
// 清除动态元素
if (activeLine) {
canvas.remove(activeLine);
activeLine = null;
}
// 清除动态多边形
if (activeShape) {
canvas.remove(activeShape);
activeShape = null;
}
// 移除所有线段
lineObjects.forEach(line => {
if (line) canvas.remove(line);
});
// 创建最终的多边形
finalPolygon = new fabric.Polygon(points.map(p => ({ x: p.x, y: p.y })), {
fill: 'rgb(227, 242, 202)',
stroke: 'rgb(169, 224, 36)', // 线段颜色为rgb(169, 224, 36)
strokeWidth: 2,
selectable: true,
globalCompositeOperation: 'multiply' // 使用混合模式而不是透明度
});
// 移除所有临时点
pointObjects.forEach(point => {
if (point) canvas.remove(point);
});
// 添加多边形到画布
canvas.add(finalPolygon);
canvas.renderAll();
// 重置状态
points = [];
pointObjects = [];
lineObjects = [];
isDrawing = false;
firstPoint = null;
showMessage('多边形绘制完成', 'success');
};
6. 用户体验优化
6.1 消息提示
使用Element Plus的消息组件提供友好的用户反馈:
const isNearFirstPoint = (x, y) => {
if (!firstPoint) return false;
const distance = Math.sqrt(
Math.pow(x - firstPoint.x, 2) +
Math.pow(y - firstPoint.y, 2)
);
return distance < 20; // 20像素范围内视为接近
};
6.2 接近检测
实现智能的"吸附"功能,方便用户闭合多边形:
const isNearFirstPoint = (x, y) => {
if (!firstPoint) return false;
const distance = Math.sqrt(
Math.pow(x - firstPoint.x, 2) +
Math.pow(y - firstPoint.y, 2)
);
return distance < 20; // 20像素范围内视为接近
};
7. 完整代码
<template>
<div class="canvas-container">
<!-- 使用单独的canvas元素,图片将作为背景 -->
<canvas ref="canvasRef"></canvas>
</div>
</template>
<script setup>
import { ref, onMounted, reactive } from 'vue'
import { fabric } from 'fabric'
import { ElMessage } from 'element-plus' // 导入Element Plus的消息组件
// 获取 canvas 元素引用
const canvasRef = ref(null)
// 多边形边数
const sidesCount = ref(5) // 默认为5边形
// 显示提示信息的函数,使用Element Plus
const showMessage = (message, type = 'info') => {
ElMessage({
message,
type,
duration: 3000,
showClose: true
});
}
onMounted(() => {
// 创建图片对象
const imgElement = new Image();
imgElement.src = '/test.jpg'; // 图片路径,注意使用公共路径
imgElement.onload = function() {
// 获取图片宽高
const imgWidth = imgElement.width;
const imgHeight = imgElement.height;
// 初始化 Fabric 画布,尺寸与图片一致
const canvas = new fabric.Canvas(canvasRef.value, {
width: imgWidth,
height: imgHeight
});
// 创建背景图片
const bgImage = new fabric.Image(imgElement, {
scaleX: 1,
scaleY: 1,
selectable: false, // 设置背景图片不可选
});
// 将图片设置为背景
canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas));
// 定义变量
let isDrawing = false; // 是否正在绘制多边形
let points = []; // 存储多边形的点
let pointObjects = []; // 存储点的可视化对象
let lineObjects = []; // 存储永久线段对象
let finalPolygon = null; // 最终多边形
let firstPoint = null; // 第一个点(用于闭合多边形)
// 动态元素 - 随鼠标移动更新
let activeLine = null; // 当前鼠标位置的活动线
let activeShape = null; // 当前动态多边形
// 创建点的函数(用于可视化)
const createPoint = (x, y, isFirst = false) => {
const circle = new fabric.Circle({
radius: 5,
fill: 'white',
stroke: isFirst ? 'red' : 'rgb(201, 201, 201)',
strokeWidth: 1,
selectable: false,
originX: 'center',
originY: 'center',
left: x,
top: y
});
canvas.add(circle);
return circle;
};
// 创建连接线的函数
const createLine = (fromX, fromY, toX, toY) => {
const line = new fabric.Line([fromX, fromY, toX, toY], {
stroke: 'rgb(51, 164, 255)',
strokeWidth: 2,
selectable: false
});
canvas.add(line);
return line;
};
// 检查点是否接近第一个点
const isNearFirstPoint = (x, y) => {
if (!firstPoint) return false;
const distance = Math.sqrt(
Math.pow(x - firstPoint.x, 2) +
Math.pow(y - firstPoint.y, 2)
);
return distance < 20; // 20像素范围内视为接近
};
// 生成或更新动态多边形
const generatePolygon = (mousePointer) => {
// 确保有足够的点
if (!isDrawing || points.length < 1) return;
// 清除旧的活动线
if (activeLine) {
canvas.remove(activeLine);
activeLine = null;
}
// 清除旧的活动形状
if (activeShape) {
canvas.remove(activeShape);
activeShape = null;
}
// 创建动态线段 - 从最后一个点到当前鼠标位置
const lastPoint = points[points.length - 1];
activeLine = new fabric.Line(
[lastPoint.x, lastPoint.y, mousePointer.x, mousePointer.y],
{
stroke: 'rgb(51, 164, 255)',
strokeWidth: 2,
selectable: false
}
);
canvas.add(activeLine);
// 如果有至少2个点,创建动态多边形
if (points.length >= 2) {
// 创建包含所有点和当前鼠标位置的点数组
let polygonPoints = [...points];
// 如果鼠标接近第一个点,使用第一个点作为闭合点
if (isNearFirstPoint(mousePointer.x, mousePointer.y)) {
polygonPoints.push(firstPoint);
} else {
polygonPoints.push({ x: mousePointer.x, y: mousePointer.y });
}
// 创建动态多边形
activeShape = new fabric.Polygon(
polygonPoints.map(p => ({ x: p.x, y: p.y })),
{
fill: 'rgb(232, 241, 249)',
stroke: 'rgb(232, 241, 249)',
strokeWidth: 1,
selectable: false,
globalCompositeOperation: 'multiply' // 使用混合模式而不是透明度
}
);
// 添加到画布
canvas.add(activeShape);
// 确保背景图在最下层
activeShape.sendToBack();
bgImage.sendToBack();
}
canvas.renderAll();
};
// 完成多边形绘制
const completePolygon = () => {
if (points.length < 3) {
showMessage('至少需要3个点才能形成多边形', 'warning');
return;
}
// 清除动态元素
if (activeLine) {
canvas.remove(activeLine);
activeLine = null;
}
// 清除动态多边形
if (activeShape) {
canvas.remove(activeShape);
activeShape = null;
}
// 移除所有线段
lineObjects.forEach(line => {
if (line) canvas.remove(line);
});
// 创建最终的多边形
finalPolygon = new fabric.Polygon(points.map(p => ({ x: p.x, y: p.y })), {
fill: 'rgb(227, 242, 202)',
stroke: 'rgb(169, 224, 36)', // 线段颜色为rgb(169, 224, 36)
strokeWidth: 2,
selectable: true,
globalCompositeOperation: 'multiply' // 使用混合模式而不是透明度
});
// 移除所有临时点
pointObjects.forEach(point => {
if (point) canvas.remove(point);
});
// 添加多边形到画布
canvas.add(finalPolygon);
canvas.renderAll();
// 重置状态
points = [];
pointObjects = [];
lineObjects = [];
isDrawing = false;
firstPoint = null;
showMessage('多边形绘制完成', 'success');
};
// 监听鼠标按下事件
canvas.on('mouse:down', function(o) {
const pointer = canvas.getPointer(o.e);
// 检查是否点击在现有对象上
// 注意:这里可能导致问题,因为activeLine和activeShape也是对象
// 只有明确是polygon类型的最终多边形才应该阻止操作
if (o.target && o.target.type === 'polygon' && !isDrawing) {
// 如果点击了现有多边形且不在绘制模式,只进行选择
return;
}
// 如果是第一次点击,开始绘制
if (!isDrawing) {
// 初始化绘制状态
isDrawing = true;
points = [];
pointObjects = [];
lineObjects = [];
// 记录第一个点
firstPoint = { x: pointer.x, y: pointer.y };
points.push(firstPoint);
// 创建第一个点的可视标记(红色表示起点)
const firstPointObject = createPoint(pointer.x, pointer.y, true);
pointObjects.push(firstPointObject);
showMessage(`开始绘制${sidesCount.value}边形,请继续点击确定顶点位置`, 'info');
} else {
console.log('当前点数:', points.length, '最大点数:', sidesCount.value);
// 检查是否点击了第一个点(闭合多边形)
if (isNearFirstPoint(pointer.x, pointer.y)) {
// 如果已经有至少3个点且点击接近第一个点,闭合多边形
if (points.length >= 5) {
completePolygon();
} else {
showMessage(`需要${sidesCount.value}个点才能闭合多边形。`, 'warning');
}
} else {
// 只有在未达到最大点数时才添加新点
if (points.length < sidesCount.value) {
// 继续添加点
const newPoint = { x: pointer.x, y: pointer.y };
points.push(newPoint);
// 创建点的可视标记
const pointObject = createPoint(pointer.x, pointer.y);
pointObjects.push(pointObject);
// 添加永久连接线
if (points.length >= 2) {
const lastIndex = points.length - 1;
const line = createLine(
points[lastIndex - 1].x, points[lastIndex - 1].y,
points[lastIndex].x, points[lastIndex].y
);
lineObjects.push(line);
}
// if (points.length < sidesCount.value) {
// showMessage(`已添加${points.length}个点,继续点击添加更多点`, 'info');
// }
// 如果刚好达到最大点数,提示用户
if (points.length === sidesCount.value) {
showMessage(`已达到最大点数${sidesCount.value},请点击第一个点完成绘制`, 'warning');
}
} else {
// 已达到最大点数,点击无效,只能点击起点完成绘制
showMessage(`已达到最大点数${sidesCount.value},请点击第一个点完成绘制`, 'warning');
}
// 强制更新画布
canvas.renderAll();
}
}
});
// 监听鼠标移动事件
canvas.on('mouse:move', function(o) {
if (!isDrawing) return;
const pointer = canvas.getPointer(o.e);
// 生成动态多边形
generatePolygon(pointer);
});
// 取消绘制的快捷键(Esc键)
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && isDrawing) {
// 清除所有元素
if (activeLine) {
canvas.remove(activeLine);
}
if (activeShape) {
canvas.remove(activeShape);
}
pointObjects.forEach(point => {
if (point) canvas.remove(point);
});
lineObjects.forEach(line => {
if (line) canvas.remove(line);
});
// 重置状态
points = [];
pointObjects = [];
lineObjects = [];
activeLine = null;
activeShape = null;
isDrawing = false;
firstPoint = null;
canvas.renderAll();
showMessage('已取消绘制', 'info');
}
});
};
})
</script>
<style scoped>
.canvas-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 20px;
position: relative;
}
</style>
以上就是基于Vue3和Fabric.js实现交互式多边形绘制功能的详细内容,更多关于Vue3 Fabric.js交互式多边形绘制的资料请关注脚本之家其它相关文章!
