vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue3 Fabric.js交互式多边形绘制

基于Vue3和Fabric.js实现交互式多边形绘制功能

作者:jnsytgsyqjgm

最近学习Fabric.js,正好想着利用这个机会去实现一个多边形的绘制工具,该工具允许用户在图片上通过点击创建多边形区域,支持自定义边数,并提供友好的用户交互体验,这种工具通常用于图像标注、区域选择和交互式地图应用等场景,需要的朋友可以参考下

需求概述

1.基础画布设置

2.多边形绘制功能

3.视觉反馈系统

4.智能交互

技术实现详解

1. 基础架构

项目使用Vue 3的<script setup>

import { ref, onMounted, reactive } from 'vue'
import { fabric } from 'fabric'
import { ElMessage } from 'element-plus'    

2. 画布初始化

在组件挂载时,通过以下步骤初始化画布:

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;
};

注意这里的实现细节:

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. 动态线段创建:

3. 多边形预览生成:

4. 多边形样式设置:

5. 层级管理:

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. 事件上下文解析:

2. 绘制状态初始化:

3. 多种点击场景处理:

4. 闭合多边形逻辑:

5. 顶点数量管理:

6. 可视化反馈:

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交互式多边形绘制的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文