如何用Fabric绘制一个多边形?

21人浏览 / 0人评论 / 添加收藏

最近一个项目中,用到了绘制Canvas的Fabric库。有关Fabric的介绍,大家可以看我往期的文章介绍。今天给大家介绍的是如何用Fabric绘制一个多边行。

本文以绘制一个8边行为例进行实例展示。

整个项目代码用Vue3搭建。如何创建Vue3项目,大家也可以搜索我往期的文章介绍:《Vue开发之如何用Vue3创建项目》

好了,话不多说,直接上代码。

1、在src/views/fabric/目录下创建文件FabricPolygon.vue

代码如下:

<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的消息组件
// import testImage from '@/assets/images/test.png';
// import { testImage } from "@/assets/images/test.png";
// import testImage from '@/assets/images/test.png'

// 获取 canvas 元素引用
const canvasRef = ref(null)
// 多边形边数
const sidesCount = ref(8) // 默认为5边形
// 显示提示信息的函数,使用Element Plus
const showMessage = (message, type = 'info') => {
ElMessage({
message,
type,
duration: 3000,
showClose: true
});
}
onMounted(() => {
// 创建图片对象
const imgElement = new Image();
// imgElement.src = testImage; // 图片路径,注意使用公共路径
// imgElement.src = '@/assets/images/test.png'
// imgElement.src = './../../assets/images/test.png'
// imgElement.src = './vite.svg'
// imgElement.src = './test.png'
imgElement.src = './1.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; // 当前动态多边形
 
let blueColor = 'blue'
let redColor = 'red'
// let otherColor = 'rgb(201, 201, 201)'
let otherColor = blueColor
let linkLineColor = 'rgb(51, 164, 255)'
let fillColor = 'rgb(232, 241, 249)'
let endColor = 'rgb(227, 242, 202)'
// let endColor = blueColor
// let endStrokeColor = 'rgb(169, 224, 36)'
let endStrokeColor = blueColor
 
// 创建点的函数(用于可视化)
const createPoint = (x, y, isFirst = false) => {
const circle = new fabric.Circle({
radius: 5,
fill: 'white',
stroke: isFirst ? redColor : otherColor,
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: linkLineColor,
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: linkLineColor,
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: fillColor,
stroke: fillColor,
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: endColor,
stroke: endStrokeColor, // 线段颜色为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>


 

2、在App.vue中增加一个菜单入口:

| <router-link to="/fabricPolygon">FabricPolygon</router-link>

 

3、在router.js文件中,增加路由。

,
{
path: '/fabricPolygon',
name: 'FabricPolygon',
component: () => import('./../views/fabric/FabricPolygon.vue')
}

 

这样代码就写完了。运行查看效果如下:

全部评论