最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

基于Vue3和Fabric.js實(shí)現(xiàn)交互式多邊形繪制功能

 更新時(shí)間:2025年05月18日 08:29:05   作者:jnsytgsyqjgm  
最近學(xué)習(xí)Fabric.js,正好想著利用這個(gè)機(jī)會(huì)去實(shí)現(xiàn)一個(gè)多邊形的繪制工具,該工具允許用戶(hù)在圖片上通過(guò)點(diǎn)擊創(chuàng)建多邊形區(qū)域,支持自定義邊數(shù),并提供友好的用戶(hù)交互體驗(yàn),這種工具通常用于圖像標(biāo)注、區(qū)域選擇和交互式地圖應(yīng)用等場(chǎng)景,需要的朋友可以參考下

需求概述

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如何實(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)路由匹配

    詳解如何在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è)卡死崩潰的解決辦法

    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項(xiàng)目添加vite支持詳解

    如何為老vue項(xiàng)目添加vite支持詳解

    Vite是一個(gè)開(kāi)發(fā)環(huán)境工具,旨在提高我們的開(kāi)發(fā)速度,下面這篇文章主要給大家介紹了關(guān)于如何為老vue項(xiàng)目添加vite支持的相關(guān)資料,需要的朋友可以參考下
    2021-09-09
  • Vue使用moment處理不同時(shí)區(qū)的時(shí)間的操作方法

    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)題的解決

    這篇文章主要介紹了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)題

    這篇文章主要介紹了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)步驟

    本文主要介紹了使用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í)例

    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)的解決思路

    這篇文章主要介紹了Vue3中多個(gè)彈窗同時(shí)出現(xiàn)的解決思路,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-02-02

最新評(píng)論

木兰县| 乌鲁木齐市| 大竹县| 彰武县| 金乡县| 当雄县| 仪征市| 治县。| 南宫市| 石河子市| 青海省| 洛隆县| 平远县| 双牌县| 柘荣县| 寻甸| 三穗县| 宜昌市| 清河县| 屏山县| 德州市| 丰宁| 南丰县| 河津市| 华容县| 永清县| 浠水县| 德令哈市| 南安市| 定兴县| 鹿邑县| 景宁| 伊川县| 宁强县| 阿荣旗| 湖口县| 云浮市| 阿图什市| 金湖县| 高州市| 嘉峪关市|