基于Vue3和Fabric.js實(shí)現(xiàn)交互式多邊形繪制功能
需求概述
1.基礎(chǔ)畫(huà)布設(shè)置
加載背景圖片
創(chuàng)建與圖片尺寸匹配的Fabric.js畫(huà)布
2.多邊形繪制功能
支持固定邊數(shù)多邊形繪制(默認(rèn)5邊形)
通過(guò)點(diǎn)擊添加頂點(diǎn)
限制最大頂點(diǎn)數(shù),達(dá)到最大數(shù)后只能閉合
3.視覺(jué)反饋系統(tǒng)
繪制頂點(diǎn)可視化(白色圓點(diǎn),首個(gè)點(diǎn)為紅邊框)
點(diǎn)與點(diǎn)之間的連接線(xiàn)(藍(lán)色)
實(shí)時(shí)多邊形預(yù)覽(半透明填充)
4.智能交互
起點(diǎn)閉合檢測(cè)(鼠標(biāo)靠近起點(diǎn)時(shí)自動(dòng)吸附)
距離計(jì)算(20像素范圍內(nèi)檢測(cè)接近)
繪制完成時(shí)的狀態(tài)轉(zhuǎn)換
技術(shù)實(shí)現(xiàn)詳解
1. 基礎(chǔ)架構(gòu)
項(xiàng)目使用Vue 3的<script setup>語(yǔ)
import { ref, onMounted, reactive } from 'vue'
import { fabric } from 'fabric'
import { ElMessage } from 'element-plus'
2. 畫(huà)布初始化
在組件掛載時(shí),通過(guò)以下步驟初始化畫(huà)布:
- 加載背景圖片
- 創(chuàng)建Fabric畫(huà)布,尺寸與圖片一致
- 將圖片設(shè)置為背景
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
});
// 設(shè)置背景圖片
const bgImage = new fabric.Image(imgElement, {
scaleX: 1,
scaleY: 1,
selectable: false,
});
canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas));
// 后續(xù)代碼...
};
});
3. 多邊形繪制核心邏輯
多邊形繪制實(shí)現(xiàn)了以下核心功能:
3.1 狀態(tài)管理
let isDrawing = false; // 繪制狀態(tài) let points = []; // 多邊形頂點(diǎn) let pointObjects = []; // 頂點(diǎn)可視化對(duì)象 let lineObjects = []; // 線(xiàn)段對(duì)象 let finalPolygon = null; // 最終多邊形 let activeLine = null; // 當(dāng)前活動(dòng)線(xiàn) let activeShape = null; // 動(dòng)態(tài)多邊形
3.2 頂點(diǎn)可視化
使用Fabric.js的Circle對(duì)象實(shí)現(xiàn)頂點(diǎn)可視化,保證頂點(diǎn)精確定位:
const createPoint = (x, y, isFirst = false) => {
const circle = new fabric.Circle({
radius: 5,
fill: 'white', // 白色背景
stroke: isFirst ? 'red' : 'rgb(201, 201, 201)', // 第一個(gè)點(diǎn)紅色邊框,其余灰色
strokeWidth: 1,
selectable: false,
originX: 'center',
originY: 'center',
left: x,
top: y
});
canvas.add(circle);
return circle;
};
注意這里的實(shí)現(xiàn)細(xì)節(jié):
- 使用originX/originY為'center'確保圓心精確定位在點(diǎn)擊位置
- left/top直接使用點(diǎn)擊坐標(biāo),不再額外減去半徑值
3.3 線(xiàn)段繪制
連接頂點(diǎn)的線(xiàn)段使用Fabric.js的Line對(duì)象實(shí)現(xiàn):
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 動(dòng)態(tài)多邊形生成
generatePolygon函數(shù)是實(shí)現(xiàn)實(shí)時(shí)預(yù)覽效果的核心,該函數(shù)會(huì)在鼠標(biāo)移動(dòng)時(shí)被調(diào)用,創(chuàng)建從最后一個(gè)固定點(diǎn)到當(dāng)前鼠標(biāo)位置的動(dòng)態(tài)線(xiàn)段和實(shí)時(shí)更新的多邊形:
const generatePolygon = (mousePointer) => {
// 確保處于繪制狀態(tài)且至少有一個(gè)點(diǎn)
if (!isDrawing || points.length < 1) return;
// 清除舊的活動(dòng)線(xiàn)
if (activeLine) {
canvas.remove(activeLine);
activeLine = null;
}
// 清除舊的活動(dòng)形狀
if (activeShape) {
canvas.remove(activeShape);
activeShape = null;
}
// 創(chuàng)建動(dòng)態(tài)線(xiàn)段 - 從最后一個(gè)點(diǎn)到當(dāng)前鼠標(biāo)位置
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個(gè)點(diǎn),創(chuàng)建動(dòng)態(tài)多邊形
if (points.length >= 2) {
// 創(chuàng)建包含所有點(diǎn)和當(dāng)前鼠標(biāo)位置的點(diǎn)數(shù)組
let polygonPoints = [...points];
// 如果鼠標(biāo)接近第一個(gè)點(diǎn),使用第一個(gè)點(diǎn)作為閉合點(diǎn)
if (isNearFirstPoint(mousePointer.x, mousePointer.y)) {
polygonPoints.push(firstPoint);
} else {
polygonPoints.push({ x: mousePointer.x, y: mousePointer.y });
}
// 創(chuàng)建動(dòng)態(tài)多邊形
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' // 使用混合模式而不是透明度
}
);
// 添加到畫(huà)布
canvas.add(activeShape);
// 確保背景圖在最下層
activeShape.sendToBack();
bgImage.sendToBack();
}
canvas.renderAll();
};
動(dòng)態(tài)多邊形生成技術(shù)細(xì)節(jié)分析:
1. 清除歷史狀態(tài): 每次鼠標(biāo)移動(dòng)時(shí),先清除之前的活動(dòng)線(xiàn)和活動(dòng)多邊形,避免重復(fù)渲染
2. 動(dòng)態(tài)線(xiàn)段創(chuàng)建:
- 使用Fabric.js的Line對(duì)象創(chuàng)建從最后固定點(diǎn)到鼠標(biāo)當(dāng)前位置的線(xiàn)段
- 設(shè)置線(xiàn)段顏色為藍(lán)色(rgb(51, 164, 255)),提供明顯的視覺(jué)反饋
3. 多邊形預(yù)覽生成:
- 當(dāng)已有至少2個(gè)點(diǎn)時(shí),創(chuàng)建包含所有固定點(diǎn)和當(dāng)前鼠標(biāo)位置的臨時(shí)點(diǎn)數(shù)組
- 通過(guò)isNearFirstPoint函數(shù)檢測(cè)鼠標(biāo)是否靠近起點(diǎn),實(shí)現(xiàn)"吸附"效果
- 如果鼠標(biāo)接近起點(diǎn),使用起點(diǎn)坐標(biāo)替代鼠標(biāo)坐標(biāo),視覺(jué)上表示可以閉合
4. 多邊形樣式設(shè)置:
- 使用淺藍(lán)色填充(rgb(232, 241, 249))和相同顏色的邊框
- 使用globalCompositeOperation: 'multiply'實(shí)現(xiàn)半透明效果,使背景圖仍然可見(jiàn)
- 多邊形不可選擇,確保不會(huì)干擾繪制流程
5. 層級(jí)管理:
- 使用activeShape.sendToBack()確保多邊形在下層
- 使用bgImage.sendToBack()確保背景圖始終在最底層
- 這種層級(jí)管理確保了可視化元素的正確疊加順序
4. 事件處理
4.1 鼠標(biāo)點(diǎn)擊事件
鼠標(biāo)點(diǎn)擊事件處理是多邊形繪制的核心邏輯,處理啟動(dòng)繪制、添加頂點(diǎn)和完成多邊形等關(guān)鍵操作:
canvas.on('mouse:down', function(o) {
const pointer = canvas.getPointer(o.e);
// 檢查是否點(diǎn)擊在現(xiàn)有對(duì)象上
if (o.target && o.target.type === 'polygon' && !isDrawing) {
// 如果點(diǎn)擊了現(xiàn)有多邊形且不在繪制模式,只進(jìn)行選擇
return;
}
// 如果是第一次點(diǎn)擊,開(kāi)始繪制
if (!isDrawing) {
// 初始化繪制狀態(tài)
isDrawing = true;
points = [];
pointObjects = [];
lineObjects = [];
// 記錄第一個(gè)點(diǎn)
firstPoint = { x: pointer.x, y: pointer.y };
points.push(firstPoint);
// 創(chuàng)建第一個(gè)點(diǎn)的可視標(biāo)記(紅色表示起點(diǎn))
const firstPointObject = createPoint(pointer.x, pointer.y, true);
pointObjects.push(firstPointObject);
showMessage(`開(kāi)始繪制${sidesCount.value}邊形,請(qǐng)繼續(xù)點(diǎn)擊確定頂點(diǎn)位置`, 'info');
} else {
console.log('當(dāng)前點(diǎn)數(shù):', points.length, '最大點(diǎn)數(shù):', sidesCount.value);
// 檢查是否點(diǎn)擊了第一個(gè)點(diǎn)(閉合多邊形)
if (isNearFirstPoint(pointer.x, pointer.y)) {
// 如果已經(jīng)有至少3個(gè)點(diǎn)且點(diǎn)擊接近第一個(gè)點(diǎn),閉合多邊形
if (points.length >= 5) {
completePolygon();
} else {
showMessage(`需要${sidesCount.value}個(gè)點(diǎn)才能閉合多邊形。`, 'warning');
}
} else {
// 只有在未達(dá)到最大點(diǎn)數(shù)時(shí)才添加新點(diǎn)
if (points.length < sidesCount.value) {
// 繼續(xù)添加點(diǎn)
const newPoint = { x: pointer.x, y: pointer.y };
points.push(newPoint);
// 創(chuàng)建點(diǎn)的可視標(biāo)記
const pointObject = createPoint(pointer.x, pointer.y);
pointObjects.push(pointObject);
// 添加永久連接線(xiàn)
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);
}
// 如果剛好達(dá)到最大點(diǎn)數(shù),提示用戶(hù)
if (points.length === sidesCount.value) {
showMessage(`已達(dá)到最大點(diǎn)數(shù)${sidesCount.value},請(qǐng)點(diǎn)擊第一個(gè)點(diǎn)完成繪制`, 'warning');
}
} else {
// 已達(dá)到最大點(diǎn)數(shù),點(diǎn)擊無(wú)效,只能點(diǎn)擊起點(diǎn)完成繪制
showMessage(`已達(dá)到最大點(diǎn)數(shù)${sidesCount.value},請(qǐng)點(diǎn)擊第一個(gè)點(diǎn)完成繪制`, 'warning');
}
// 強(qiáng)制更新畫(huà)布
canvas.renderAll();
}
}
});
鼠標(biāo)點(diǎn)擊事件技術(shù)細(xì)節(jié)分析:
1. 事件上下文解析:
- 使用canvas.getPointer(o.e)獲取鼠標(biāo)在畫(huà)布上的準(zhǔn)確坐標(biāo)
- 通過(guò)o.target檢測(cè)點(diǎn)擊是否落在現(xiàn)有對(duì)象上,防止在已有多邊形上開(kāi)始新繪制
2. 繪制狀態(tài)初始化:
- 首次點(diǎn)擊時(shí),重置所有狀態(tài)變量(isDrawing, points, pointObjects, lineObjects)
- 創(chuàng)建第一個(gè)點(diǎn)并用紅色標(biāo)記,區(qū)別于后續(xù)點(diǎn)
- 通過(guò)消息提示告知用戶(hù)已開(kāi)始繪制
3. 多種點(diǎn)擊場(chǎng)景處理:
- 區(qū)分三種主要場(chǎng)景:首次點(diǎn)擊、點(diǎn)擊第一個(gè)點(diǎn)(閉合)、添加新點(diǎn)
- 使用狀態(tài)變量isDrawing區(qū)分是否處于繪制模式
4. 閉合多邊形邏輯:
- 使用isNearFirstPoint函數(shù)檢測(cè)點(diǎn)擊是否接近起點(diǎn)
- 當(dāng)點(diǎn)擊接近第一個(gè)點(diǎn)且已有足夠的點(diǎn)(>=5)時(shí),通過(guò)completePolygon()函數(shù)完成多邊形
- 如果點(diǎn)不足,顯示警告消息指導(dǎo)用戶(hù)
5. 頂點(diǎn)數(shù)量管理:
- 通過(guò)sidesCount.value控制多邊形的最大頂點(diǎn)數(shù)
- 實(shí)現(xiàn)嚴(yán)格的頂點(diǎn)數(shù)限制,達(dá)到最大數(shù)后只允許閉合操作
- 根據(jù)不同階段提供相應(yīng)的用戶(hù)提示
6. 可視化反饋:
- 每個(gè)點(diǎn)都有可視化標(biāo)記(白色圓形帶有邊框)
- 點(diǎn)與點(diǎn)之間有顏色鮮明的連接線(xiàn)
- 每次操作后立即更新畫(huà)布(canvas.renderAll())
4.2 鼠標(biāo)移動(dòng)事件
實(shí)時(shí)更新動(dòng)態(tài)線(xiàn)段和多邊形形狀:
canvas.on('mouse:move', function(o) {
if (!isDrawing) return;
const pointer = canvas.getPointer(o.e);
generatePolygon(pointer);
});
4.3 鍵盤(pán)事件
支持Esc鍵取消繪制:
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && isDrawing) {
// 清除所有臨時(shí)元素
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);
});
// 重置狀態(tài)
points = [];
pointObjects = [];
lineObjects = [];
activeLine = null;
activeShape = null;
isDrawing = false;
firstPoint = null;
canvas.renderAll();
showMessage('已取消繪制', 'info');
}
});
5. 完成多邊形繪制
當(dāng)用戶(hù)點(diǎn)擊接近起點(diǎn)時(shí),多邊形繪制完成:
const completePolygon = () => {
if (points.length < 3) {
showMessage('至少需要3個(gè)點(diǎn)才能形成多邊形', 'warning');
return;
}
// 清除動(dòng)態(tài)元素
if (activeLine) {
canvas.remove(activeLine);
activeLine = null;
}
// 清除動(dòng)態(tài)多邊形
if (activeShape) {
canvas.remove(activeShape);
activeShape = null;
}
// 移除所有線(xiàn)段
lineObjects.forEach(line => {
if (line) canvas.remove(line);
});
// 創(chuàng)建最終的多邊形
finalPolygon = new fabric.Polygon(points.map(p => ({ x: p.x, y: p.y })), {
fill: 'rgb(227, 242, 202)',
stroke: 'rgb(169, 224, 36)', // 線(xiàn)段顏色為rgb(169, 224, 36)
strokeWidth: 2,
selectable: true,
globalCompositeOperation: 'multiply' // 使用混合模式而不是透明度
});
// 移除所有臨時(shí)點(diǎn)
pointObjects.forEach(point => {
if (point) canvas.remove(point);
});
// 添加多邊形到畫(huà)布
canvas.add(finalPolygon);
canvas.renderAll();
// 重置狀態(tài)
points = [];
pointObjects = [];
lineObjects = [];
isDrawing = false;
firstPoint = null;
showMessage('多邊形繪制完成', 'success');
};
6. 用戶(hù)體驗(yàn)優(yōu)化
6.1 消息提示
使用Element Plus的消息組件提供友好的用戶(hù)反饋:
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像素范圍內(nèi)視為接近
};
6.2 接近檢測(cè)
實(shí)現(xiàn)智能的"吸附"功能,方便用戶(hù)閉合多邊形:
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像素范圍內(nèi)視為接近
};
7. 完整代碼
<template>
<div class="canvas-container">
<!-- 使用單獨(dú)的canvas元素,圖片將作為背景 -->
<canvas ref="canvasRef"></canvas>
</div>
</template>
<script setup>
import { ref, onMounted, reactive } from 'vue'
import { fabric } from 'fabric'
import { ElMessage } from 'element-plus' // 導(dǎo)入Element Plus的消息組件
// 獲取 canvas 元素引用
const canvasRef = ref(null)
// 多邊形邊數(shù)
const sidesCount = ref(5) // 默認(rèn)為5邊形
// 顯示提示信息的函數(shù),使用Element Plus
const showMessage = (message, type = 'info') => {
ElMessage({
message,
type,
duration: 3000,
showClose: true
});
}
onMounted(() => {
// 創(chuàng)建圖片對(duì)象
const imgElement = new Image();
imgElement.src = '/test.jpg'; // 圖片路徑,注意使用公共路徑
imgElement.onload = function() {
// 獲取圖片寬高
const imgWidth = imgElement.width;
const imgHeight = imgElement.height;
// 初始化 Fabric 畫(huà)布,尺寸與圖片一致
const canvas = new fabric.Canvas(canvasRef.value, {
width: imgWidth,
height: imgHeight
});
// 創(chuàng)建背景圖片
const bgImage = new fabric.Image(imgElement, {
scaleX: 1,
scaleY: 1,
selectable: false, // 設(shè)置背景圖片不可選
});
// 將圖片設(shè)置為背景
canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas));
// 定義變量
let isDrawing = false; // 是否正在繪制多邊形
let points = []; // 存儲(chǔ)多邊形的點(diǎn)
let pointObjects = []; // 存儲(chǔ)點(diǎn)的可視化對(duì)象
let lineObjects = []; // 存儲(chǔ)永久線(xiàn)段對(duì)象
let finalPolygon = null; // 最終多邊形
let firstPoint = null; // 第一個(gè)點(diǎn)(用于閉合多邊形)
// 動(dòng)態(tài)元素 - 隨鼠標(biāo)移動(dòng)更新
let activeLine = null; // 當(dāng)前鼠標(biāo)位置的活動(dòng)線(xiàn)
let activeShape = null; // 當(dāng)前動(dòng)態(tài)多邊形
// 創(chuàng)建點(diǎn)的函數(shù)(用于可視化)
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;
};
// 創(chuàng)建連接線(xiàn)的函數(shù)
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;
};
// 檢查點(diǎn)是否接近第一個(gè)點(diǎn)
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像素范圍內(nèi)視為接近
};
// 生成或更新動(dòng)態(tài)多邊形
const generatePolygon = (mousePointer) => {
// 確保有足夠的點(diǎn)
if (!isDrawing || points.length < 1) return;
// 清除舊的活動(dòng)線(xiàn)
if (activeLine) {
canvas.remove(activeLine);
activeLine = null;
}
// 清除舊的活動(dòng)形狀
if (activeShape) {
canvas.remove(activeShape);
activeShape = null;
}
// 創(chuàng)建動(dòng)態(tài)線(xiàn)段 - 從最后一個(gè)點(diǎn)到當(dāng)前鼠標(biāo)位置
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個(gè)點(diǎn),創(chuàng)建動(dòng)態(tài)多邊形
if (points.length >= 2) {
// 創(chuàng)建包含所有點(diǎn)和當(dāng)前鼠標(biāo)位置的點(diǎn)數(shù)組
let polygonPoints = [...points];
// 如果鼠標(biāo)接近第一個(gè)點(diǎn),使用第一個(gè)點(diǎn)作為閉合點(diǎn)
if (isNearFirstPoint(mousePointer.x, mousePointer.y)) {
polygonPoints.push(firstPoint);
} else {
polygonPoints.push({ x: mousePointer.x, y: mousePointer.y });
}
// 創(chuàng)建動(dòng)態(tài)多邊形
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' // 使用混合模式而不是透明度
}
);
// 添加到畫(huà)布
canvas.add(activeShape);
// 確保背景圖在最下層
activeShape.sendToBack();
bgImage.sendToBack();
}
canvas.renderAll();
};
// 完成多邊形繪制
const completePolygon = () => {
if (points.length < 3) {
showMessage('至少需要3個(gè)點(diǎn)才能形成多邊形', 'warning');
return;
}
// 清除動(dòng)態(tài)元素
if (activeLine) {
canvas.remove(activeLine);
activeLine = null;
}
// 清除動(dòng)態(tài)多邊形
if (activeShape) {
canvas.remove(activeShape);
activeShape = null;
}
// 移除所有線(xiàn)段
lineObjects.forEach(line => {
if (line) canvas.remove(line);
});
// 創(chuàng)建最終的多邊形
finalPolygon = new fabric.Polygon(points.map(p => ({ x: p.x, y: p.y })), {
fill: 'rgb(227, 242, 202)',
stroke: 'rgb(169, 224, 36)', // 線(xiàn)段顏色為rgb(169, 224, 36)
strokeWidth: 2,
selectable: true,
globalCompositeOperation: 'multiply' // 使用混合模式而不是透明度
});
// 移除所有臨時(shí)點(diǎn)
pointObjects.forEach(point => {
if (point) canvas.remove(point);
});
// 添加多邊形到畫(huà)布
canvas.add(finalPolygon);
canvas.renderAll();
// 重置狀態(tài)
points = [];
pointObjects = [];
lineObjects = [];
isDrawing = false;
firstPoint = null;
showMessage('多邊形繪制完成', 'success');
};
// 監(jiān)聽(tīng)鼠標(biāo)按下事件
canvas.on('mouse:down', function(o) {
const pointer = canvas.getPointer(o.e);
// 檢查是否點(diǎn)擊在現(xiàn)有對(duì)象上
// 注意:這里可能導(dǎo)致問(wèn)題,因?yàn)閍ctiveLine和activeShape也是對(duì)象
// 只有明確是polygon類(lèi)型的最終多邊形才應(yīng)該阻止操作
if (o.target && o.target.type === 'polygon' && !isDrawing) {
// 如果點(diǎn)擊了現(xiàn)有多邊形且不在繪制模式,只進(jìn)行選擇
return;
}
// 如果是第一次點(diǎn)擊,開(kāi)始繪制
if (!isDrawing) {
// 初始化繪制狀態(tài)
isDrawing = true;
points = [];
pointObjects = [];
lineObjects = [];
// 記錄第一個(gè)點(diǎn)
firstPoint = { x: pointer.x, y: pointer.y };
points.push(firstPoint);
// 創(chuàng)建第一個(gè)點(diǎn)的可視標(biāo)記(紅色表示起點(diǎn))
const firstPointObject = createPoint(pointer.x, pointer.y, true);
pointObjects.push(firstPointObject);
showMessage(`開(kāi)始繪制${sidesCount.value}邊形,請(qǐng)繼續(xù)點(diǎn)擊確定頂點(diǎn)位置`, 'info');
} else {
console.log('當(dāng)前點(diǎn)數(shù):', points.length, '最大點(diǎn)數(shù):', sidesCount.value);
// 檢查是否點(diǎn)擊了第一個(gè)點(diǎn)(閉合多邊形)
if (isNearFirstPoint(pointer.x, pointer.y)) {
// 如果已經(jīng)有至少3個(gè)點(diǎn)且點(diǎn)擊接近第一個(gè)點(diǎn),閉合多邊形
if (points.length >= 5) {
completePolygon();
} else {
showMessage(`需要${sidesCount.value}個(gè)點(diǎn)才能閉合多邊形。`, 'warning');
}
} else {
// 只有在未達(dá)到最大點(diǎn)數(shù)時(shí)才添加新點(diǎn)
if (points.length < sidesCount.value) {
// 繼續(xù)添加點(diǎn)
const newPoint = { x: pointer.x, y: pointer.y };
points.push(newPoint);
// 創(chuàng)建點(diǎn)的可視標(biāo)記
const pointObject = createPoint(pointer.x, pointer.y);
pointObjects.push(pointObject);
// 添加永久連接線(xiàn)
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}個(gè)點(diǎn),繼續(xù)點(diǎn)擊添加更多點(diǎn)`, 'info');
// }
// 如果剛好達(dá)到最大點(diǎn)數(shù),提示用戶(hù)
if (points.length === sidesCount.value) {
showMessage(`已達(dá)到最大點(diǎn)數(shù)${sidesCount.value},請(qǐng)點(diǎn)擊第一個(gè)點(diǎn)完成繪制`, 'warning');
}
} else {
// 已達(dá)到最大點(diǎn)數(shù),點(diǎn)擊無(wú)效,只能點(diǎn)擊起點(diǎn)完成繪制
showMessage(`已達(dá)到最大點(diǎn)數(shù)${sidesCount.value},請(qǐng)點(diǎn)擊第一個(gè)點(diǎn)完成繪制`, 'warning');
}
// 強(qiáng)制更新畫(huà)布
canvas.renderAll();
}
}
});
// 監(jiān)聽(tīng)鼠標(biāo)移動(dòng)事件
canvas.on('mouse:move', function(o) {
if (!isDrawing) return;
const pointer = canvas.getPointer(o.e);
// 生成動(dòng)態(tài)多邊形
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);
});
// 重置狀態(tài)
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實(shí)現(xiàn)交互式多邊形繪制功能的詳細(xì)內(nèi)容,更多關(guān)于Vue3 Fabric.js交互式多邊形繪制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue如何實(shí)現(xiàn)文件預(yù)覽和下載功能的前端上傳組件
在Vue.js項(xiàng)目中,使用ElementUI的el-upload組件可以輕松實(shí)現(xiàn)文件上傳功能,通過(guò)配置組件參數(shù)和實(shí)現(xiàn)相應(yīng)的方法,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-09-09
詳解如何在Vue Router中實(shí)現(xiàn)動(dòng)態(tài)路由匹配
在構(gòu)建復(fù)雜的單頁(yè)面應(yīng)用程序(SPA)時(shí),動(dòng)態(tài)路由匹配是一項(xiàng)強(qiáng)大的功能,它允許我們根據(jù)URL中的參數(shù)動(dòng)態(tài)加載內(nèi)容,Vue Router 提供了動(dòng)態(tài)路由匹配的能力,使得我們可以根據(jù)不同的URL參數(shù)展示不同的組件或數(shù)據(jù),本文將介紹如何在Vue Router中實(shí)現(xiàn)動(dòng)態(tài)路由匹配2025-05-05
element?table數(shù)據(jù)量太大導(dǎo)致網(wǎng)頁(yè)卡死崩潰的解決辦法
當(dāng)頁(yè)面數(shù)據(jù)過(guò)多,前端渲染大量的DOM時(shí),會(huì)造成頁(yè)面卡死問(wèn)題,下面這篇文章主要給大家介紹了關(guān)于element?table數(shù)據(jù)量太大導(dǎo)致網(wǎng)頁(yè)卡死崩潰的解決辦法,需要的朋友可以參考下2023-02-02
Vue使用moment處理不同時(shí)區(qū)的時(shí)間的操作方法
這篇文章主要介紹了如何在Vue項(xiàng)目中使用moment和moment-timezone插件處理時(shí)區(qū),包括安裝依賴(lài)、基礎(chǔ)使用方法(如時(shí)區(qū)轉(zhuǎn)換)、全局掛載以及注意事項(xiàng)(如時(shí)區(qū)標(biāo)識(shí)規(guī)范、UTC時(shí)間處理和Vue3適配),需要的朋友可以參考下2026-03-03
Electron中打包應(yīng)用程序及相關(guān)報(bào)錯(cuò)問(wèn)題的解決
這篇文章主要介紹了Electron中打包應(yīng)用程序及相關(guān)報(bào)錯(cuò)問(wèn)題的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03
ElementUI中el-dropdown-item點(diǎn)擊事件無(wú)效問(wèn)題
這篇文章主要介紹了ElementUI中el-dropdown-item點(diǎn)擊事件無(wú)效問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04
使用Vite搭建vue3+TS項(xiàng)目的實(shí)現(xiàn)步驟
本文主要介紹了使用Vite搭建vue3+TS項(xiàng)目的實(shí)現(xiàn)步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01
vue3.0路由自動(dòng)導(dǎo)入的方法實(shí)例
這篇文章主要給大家介紹了關(guān)于vue3.0路由自動(dòng)導(dǎo)入的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04
Vue3中多個(gè)彈窗同時(shí)出現(xiàn)的解決思路
這篇文章主要介紹了Vue3中多個(gè)彈窗同時(shí)出現(xiàn)的解決思路,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-02-02

