javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > paper.js图片简单框选标注

利用paper.js实现图片简单框选标注功能

作者:梦凡尘

Paper.js是一个 JavaScript库用来制作绘图和动画,这篇文章主要介绍了利用paper.js实现图片简单框选标注功能的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

一,效果

在开发中的实际业务场景可能会更加复杂,这里只展示操作的核心代码,不包含任务业务逻辑。

二,引入并注册 paper.js, 绑定全局事件

1,安装paper.js

npm install paper   或者   yarn add paper

2,注册 paper.js ,绑定全局事件

<template>
   <div class="layout">
    <canvas ref="canvas" class="canvas-content"></canvas>

    <!-- 缩放按钮 -->
    <div class="scale">
      <i class="scale-reduce scale-icon" @click="reduceView"></i>
      <div class="scale-number">{{ (scale * 100).toFixed(0) }}%</div>
      <i class="scale-plus scale-icon" @click="magnifyView"></i>
    </div>
  </div>
</template>

<script setup lang='ts'>
import { ref, defineProps, PropType, watch, onMounted, defineEmits, onUnmounted } from 'vue';
import paper from 'paper';

const canvas = ref();
let myPaper: paper.PaperScope | null = null;
 // 当前缩放比例
 const scale = ref<number>(1);

const state = {
 // 工具类
 tool: null as paper.Tool | null,
}

onMounted(() => {
    if (myPaper) {
      myPaper.project.clear();
      myPaper = null;
    }
    if (canvas.value) {
      myPaper = new paper.PaperScope();
      // 注册画布
      myPaper.setup(canvas.value);
      state.tool = new myPaper.Tool();
      // 注册鼠标按下事件
      state.tool.onMouseDown = paperMouseDown;

      // 注册鼠标移动事件
      state.tool.onMouseMove = paperMouseMove;

      // 注册拖拽事件
      state.tool.onMouseDrag = paperMouseDrag;

      // 注册鼠标抬起事件
      state.tool.onMouseUp = paperMouseUp;
    }

    // 监听窗口大小改变事件
    window.addEventListener('resize', resizeHandle);
  });
</script>

三,初始化背景图,居中加载,缩放至适当比例

1,将背景图层及背景图路径保存在全局变量 state 中

后面代码中的state.xxxxx,默认是在 state 中已经声明好的。

const state = {
    // 背景图层
    bgLayer: null,
    // 背景图路径
    bgPath: null,
    // 背景图左上角顶点 x 坐标
    startX: 0,
    // 背景图左上角顶点 y 坐标
    startY: 0,
}

2,初始化背景图层

LayerName 是各个图层名称的枚举变量,给图层命名方便使用时直接通过名称获取需要操作的图层。

  /**
   * @description: 初始化背景图层
   * @return {*}
   */
  const initBgLayer = async () => {
    // 创建第一个背景图层
    if (!state.bgLayer) {
      state.bgLayer = new myPaper.Layer();
      state.bgLayer.name = LayerName.BG_LAYER;
    } else {
      state.bgLayer?.removeChildren();
    }
    if (!myPaper?.project.layers[LayerName.BG_LAYER as any]) {
      // 如果存在图层信息,则直接将图层添加到project 里
      myPaper?.project.addLayer(state.bgLayer);
    }
    // 设置bgLayer为活动图层
    // 使用 Promise 等待图片加载完成
    await new Promise((resolve, reject) => {
      state.bgPath= new myPaper.Raster({
        source: props.imageData.convertFileName,
        onLoad: () => {
          resolve(void 0); // 加载成功时调用 resolve
        },
        onError: () => {
          reject(new Error('加载失败')); // 加载失败时调用 reject
        },
      });
    });
    // 设置背景图路径名称
    state.bgPath.name = PATN_NAME.BG_IMAGE
    // 计算缩放比例
    calculateScale(props.imageData.imageWidth, props.imageData.imageHeight, myPaper.view.size.width, myPaper.view.size.height);
    
    // 根据图片的宽高和画布的宽高计算初始缩放比例
    myPaper.view.scale(scale.value);
    // 设置居中显示
    state.bgPath.position = myPaper.view.center;

     // 获取背景图左上角顶点坐标
      state.startX = state.bgPath.position.x - state.bgPath.bounds.width / 2;
      state.startY = state.bgPath.position.y - state.bgPath.bounds.height / 2;
  };

3,计算缩放比例

主要是比较图片和视口的宽高比,若图片的宽高比大于视口的宽高比则说明图片的宽度占比比较大,则计算缩放比例时只要适配图片的宽度即可。

  /**
   * @description: 计算缩放比例
   * @param {number} imageWidth
   * @param {number} imageHeight
   * @param {number} canvasWidth
   * @param {number} canvasHeight
   * @return {*}
   */
  const calculateScale = (imageWidth: number, imageHeight: number, canvasWidth: number, canvasHeight: number) => {
    // 计算照片的宽高比
    const imageRatio = imageWidth / imageHeight;
    // 计算画布的宽高比
    const canvasRatio = canvasWidth / canvasHeight;
    // 如果照片的宽高比大于画布的宽高比,则根据宽度计算缩放比例
    if (imageRatio > canvasRatio) {
      // 初始缩放比例按照窗口边缘20px留白为准
      scale.value = (canvasWidth - 40) / imageWidth;
    } else {
      // 如果照片的宽高比小于或等于画布的宽高比,则根据高度计算缩放比例
      scale.value = (canvasHeight - 40) / imageHeight;
    }
    // 将缩放比例保留两位小数
    scale.value = parseFloat(scale.value.toFixed(2));
  };

四,画布缩放

每次放大、缩小画布时都要执行 myPaper.view.zoom = 1。重置画布比例,这样执行 paper.view.scale();设置画布缩放时才能得到准确的结果。

1,缩小画布

 /**
   * @description: 缩小画布
   * @return {*}
   */
  const reduceView = () => {
    if (!myPaper) return;
    // 如果缩放比例小于0.1则不再缩小
    if (scale.value <= 0.1) {
      return;
    }
    // 每次缩小5%
    scale.value = scale.value - 0.05;
    // 清除画布缩放比例
    myPaper.view.zoom = 1;
    // 重新进行缩放
    myPaper.view.scale(scale.value);
  };

2,放大画布

  /**
   * @description: 放大画布
   * @return {*}
   */
  const magnifyView = () => {
    if (!myPaper) return;

    // 每次放大5%
    scale.value = scale.value + 0.05;
    // 清除画布缩放比例
    myPaper.view.zoom = 1;
    // 重新进行缩放
    myPaper.view.scale(scale.value);
  };

五,初始化选框位置

1,初始化选框图层

PATH_NAME 是图层中的所有类型的路径的名称,是一个枚举值。方便在某个 group 路径中直接找到对应的路径进行操作( 当 group 中的子路径不唯一时 )。

  /**
   * @description: 初始化选框信息图层
   * @return {*}
   */
  const initStep1Layer = () => {
    if (!state.step1Layer) {
      // 创建选框信息图层
      state.step1Layer = new myPaper.Layer();
      // 设置图层名称
      state.step1Layer.name = LayerName.RECT;
    } else {
      // 如果存在图层,则需要将图层中的所有路径删除
      state.step1Layer?.removeChildren();
    }

    // 如果当前项目中不包括框选图层,则要将框选图层添加到当前项目中
    if (!myPaper?.project.layers[LayerName.Rect as any]) {
      myPaper?.project.addLayer(state.step1Layer);
    }

    // 遍历选框信息数据绘制矩形框
    props.data?.forEach((item:Data) => {
      // 创建选框信息路径群组,可能选框会包括别的标签内容
      const group = new myPaper.Group();
      // 添加信息框选
      const info = new myPaper.Path.Rectangle({
        name:  PATH_NAME.INFO,
        point: [item.rect[0] + state.startX, item.rect[1] + state.startY],
        size: [item.rect[2], item.rect[3]],
        data: {
          ...item,
        },
      });
      // 根据步骤修改信息选框样式
      modifyInfoRectStyle(info, Step.STEP1);
      group.addChild(info);
      state.step1Layer?.addChild(group);
    });
  };

2,设置选框样式

根据条件或者状态的不同修改路径样式,infoStyle 是路径的样式配置对象

通过 path.set() 进行批量设置样式。

// info 选框样式
export const infoStyle = {
  // 默认样式
  default: {
    strokeColor: new paper.Color('#aaaaaa'),
    strokeWidth: 2,
    // 填充颜色
    fillColor: new paper.Color('rgba(0, 0, 0, 0.01)'),
  },
  //禁用样式
  disabled: {
    strokeColor: new paper.Color('#637176'),
    strokeWidth: 2,
    // 填充颜色
    fillColor: new paper.Color('rgba(0, 0, 0, 0.01)'),
  },
};
  /**
   * @description: 修改信息选框样式
   * @param {paper.Path} path
   * @param {Step} activeStep
   * @return {*}
   */
  const modifyInfoRectStyle = (path: paper.Path, activeStep: Step) => {
    if (条件a) {
      // 设置路径样式
      path.set(infoStyle.default);
    } else {
      // 否则绘制禁用状态
      path.set(infoStyle.disabled);
    }
  };

六,初始化操作按钮,添加点击事件

给按钮路径绑定点击事件,在 paper.js 中的路径对象中都能监听各种鼠标事件。

  /**
   * @description: 初始化操作按钮图层
   * @return {*}
   */
  const initOperatorLayer = () => {
    if (!state.operatorLayer) {
      state.operatorLayer = new myPaper.Layer();
      state.operatorLayer.name = LayerName.OPERATOR_LAYER;
    } else {
      if (!myPaper?.project.layers[LayerName.OPERATOR_LAYER as any]) {
        // 如果存在图层信息,则直接将图层添加到project 里
        myPaper?.project.addLayer(state.operatorLayer);
      }
      // 如果存在操作按钮图层,则直接退出
      return;
    }

    // 创建完成按钮
    state.finishBtn = addOperatorBtn(OperatorBtnType.FINISH);
    state.operatorLayer?.addChild(state.finishBtn);
    // 注册完成按钮点击事件
    state.finishBtn.onClick = finishBtnClick;
    state.finishBtn.onMouseMove = operatorBtnMouseMove;
   
    // 创建删除按钮
    state.deleteBtn = addOperatorBtn(OperatorBtnType.DELETE);
    state.operatorLayer?.addChild(state.deleteBtn);
    // 注册删除按钮点击事件
    state.deleteBtn.onClick = deleteBtnClick;
    state.deleteBtn.onMouseMove = operatorBtnMouseMove;
  
  };



  /**
   * @description: 给当前操作路径又下角添加操作按钮
   * @param {*} startX
   * @param {*} startY
   * @param {*} name
   * @return {*}
   */
  const addOperatorBtn = (btnType: OperatorBtnType): paper.Group => {
    let name;
    if (btnType === OperatorBtnType.DELETE) {
      name = '删除';
    } else if (btnType === OperatorBtnType.FINISH) {
      name = '完成';
    }
    // 创建一个组合
    const group = new myPaper.Group();
    // 设置操作按钮名称,方便删除
    group.name = btnType;
    // 创建删除矩形路径
    const rect = new myPaper.Path.Rectangle({
      name: PATH_NAME.OPERATOR,
      point: [state.startX, state.startY],
      size: [operatorStyle.width, operatorStyle.height],
      fillColor: operatorStyle.backgroundColor,
      strokeColor: operatorStyle.backgroundColor,
      strokeWidth: 2,
      radius: operatorStyle.borderRadius,
    });
    group.addChild(rect);

    // 创建删除文本路径放置在上面的矩形路径中,字体颜色为白色
    const text = new myPaper.PointText({
      name: PATH_NAME.OPERATOR,
      content: name,
      fillColor: operatorStyle.textColor,
      fontSize: operatorStyle.fontSize,
    });
    text.justification = 'center';
    text.strokeColor = operatorStyle.textColor;
    text.strokeWidth = 1;
    // 操作按钮文本居中
    text.position.x = rect.bounds.topLeft.x + operatorStyle.width / 2;
    text.position.y = rect.bounds.topLeft.y + operatorStyle.height / 2;
    // 将文本路径添加进矩形路径中
    group.addChild(text);
    // 初始默认隐藏操作按钮
    group.visible = false;
    return group;
  };

七,新增框选,移动缩放选框全局事件处理

新增框选需要事件:mousedown,mousedrag,mouseup

移动选框需要事件:mousedown,mousedrag

缩放选框需要事件:mousedown,mousedrag

1,mousedown 获取所有需要进行碰撞检测的路径,(还有切换选框编辑状态的功能)

CurrentPathStatus 是当前路径操作的枚举值,有创建状态 CurrentPathStatus.CREATE,编辑状态 CurrentPathStatus.EDIT,默认状态 CurrentPathStatus.DEFAULT。

isCreate.value  表示当前是否可以创建选框

由于当前场景比较简单,所有需要进行碰撞检测的路径即为 state.step1Layer 图层中的所有路径

 /**
   * @description: paper鼠标按下事件
   * @param {*} event
   * @return {*}
   */
  const paperMouseDown = (event: paper.ToolEvent) => {
    // 初始化鼠标按下碰撞路径和路径顶点
    state.hitPath = null as paper.Path | null;
    state.hitSegment = null;
    // 鼠标点位碰撞检测
    const hitResult = myPaper?.project.hitTest(event.point, hitOptions);
    // 操作按钮有单独的点击事件,不执行一下逻辑
    if (hitResult?.item?.name ===PATN_NAME.OPERATOR) return;
    // 如果当前是创建状态,则只能点击新建选框
    // if (state.currentPathStatus === CurrentPathStatus.CREATE && hitResult?.item !== state.currentPath) return;
    if (hitResult) {
      state.hitPath = hitResult.item;

      // 点击选框设置为编辑状态
      if (
        !isCreate.value &&
        state.currentPathStatus === CurrentPathStatus.DEFAULT &&
        state.hitPath.name ===PATH_NAME.INFO &&
        !state.hitPath.selected 
      ) {
        // 当前是默认状态时,设置选框为编辑状态
        switchPathEditStatus(state.hitPath as paper.Path);
        state.currentPath = state.hitPath as paper.Path;
        // 获取碰撞检测群体路径
        // 由于场景比较简单,不需要单独获取碰撞检测群体路径,state.step1Layer 图层中的路径即为碰撞检测路径。
      } else if (
        !isCreate.value &&
        state.currentPathStatus === CurrentPathStatus.EDIT &&
        state.hitPath?.name === PATH_NAME.INFO &&
        !state.hitPath.selected 
      ) {
        // 当前是编辑状态,则需要将当前路径设置为默认状态,然后将点击路径设置为编辑状态,并更新state.currentPath
        switchPathDefaultStatus(state.currentPath as paper.Path);
        // 将碰撞路径切换至编辑状态
        switchPathEditStatus(state.hitPath as paper.Path);
        // 更新当前路径
        state.currentPath = state.hitPath as paper.Path;
        // 获取碰撞检测群体路径
        // 由于场景比较简单,不需要单独获取碰撞检测群体路径,state.step1Layer 图层中的路径即为碰撞检测路径。
      }

      // 创建选框时,鼠标按下事件获取所有需要碰撞检测的路径
      if (
        isCreate.value &&
        state.hitPath?.name === PATH_NAME.BG_IMAGE
      ) {
       
        // 获取创建选框时需要的碰撞检测路径
         // 由于场景比较简单,不需要单独获取碰撞检测群体路径,state.step1Layer 图层中的路径即为碰撞检测路径。
      } else if (state.currentPathStatus === CurrentPathStatus.CREATE && state.hitPath?.name === PATH_NAME.INFO) {
        // 此时是刚创建完选框时,点击拖拽移动新创建选框位置
        // 获取当前选框碰撞群体数据
        state.currentPath = state.hitPath as paper.Path;
        // 由于场景比较简单,不需要单独获取碰撞检测群体路径,state.step1Layer 图层中的路径即为碰撞检测路径。
      }

      if (state.currentPathStatus !== CurrentPathStatus.DEFAULT && state.hitPath?.name === PATH_NAME.INFO && hitResult.type === 'segment') {
        // 创建和编辑状态时可以点击选框顶点进行缩放选框大小
        // 记录当前点击的路径顶点
        state.hitSegment = hitResult.segment;
      }
    }
  };


  /**
   * @description: 切换路径默认状态
   * @param {*} path
   * @return {*}
   */
  const switchPathDefaultStatus = (path: paper.Path) => {
    // 切换路径默认状态
    state.currentPathStatus = CurrentPathStatus.DEFAULT;
    if (path) {
      // 设置选框路径样式为默认状态
      path.set(infoStyle.default);
      // 设置没有选中
      path.selected = false;
    }
    // 隐藏操作按钮
    setOperatorBtnHide();
  };

  /**
   * @description: 设置操作按钮隐藏
   * @return {*}
   */
  const setOperatorBtnHide = () => {
    // 隐藏操作按钮
    if (state.deleteBtn) state.deleteBtn.visible = false;
    if (state.finishBtn) state.finishBtn.visible = false;
  };

  /**
   * @description: 切换路径编辑状态
   * @param {*} path
   * @return {*}
   */
  const switchPathEditStatus = (path: paper.Path) => {
    // 切换路径编辑状态
    state.currentPathStatus = CurrentPathStatus.EDIT;
    if (path) {
      // 设置选框路径样式为编辑状态
      path.set(infoStyle.edit);
      path.selectedColor = infoStyle.edit.strokeColor;
      // 设置选中
      path.selected = true;
    }
    // 移动操作按钮到路径的右下角
    setOperatorBtnShow(path);
  };


  /**
   * @description: 设置操作按钮显示
   * @param {*} path
   * @return {*}
   */
  const setOperatorBtnShow = (path: paper.Path) => {
    // 如果存在删除按钮
      if (state.deleteBtn) {
        state.deleteBtn.position.x = path.bounds.bottomRight.x + operatorStyle.width / 2 + 5;
        state.deleteBtn.position.y = path.bounds.bottomRight.y + operatorStyle.height / 2 + 5;
        state.deleteBtn.visible = true;
      }

      if (state.finishBtn) {
        state.finishBtn.position.x = path.bounds.bottomRight.x + (operatorStyle.width / 2) * 3 + operatorStyle.margin + 5;
        state.finishBtn.position.y = path.bounds.bottomRight.y + operatorStyle.height / 2 + 5;
        state.finishBtn.visible = true;
      }
  };

2,mousedrag 当鼠标点击在 无选框区域 绘制选框,并在绘制的过程中进行碰撞检测, 移动画布,移动选框,缩放选框。

event.delta: 相对于上一次事件执行时的位移矢量

新建选框没有发生碰撞:删除上一次拖拽创建的矩形路径 -----> 创建矩形路径 -----> 没有发生碰撞 ----> 记录当前矩形路径的坐标和尺寸

新建选框发生碰撞:删除上一次拖拽创建的矩形路径  -----> 创建矩形路径 ------> 发生碰撞 -----> 删除碰撞检测失败的矩形路径 ------> 还原上次拖拽事件中创建的矩形路径

移动画布:移动 project 中所有的图层即可。

 /**
   * @description: paper鼠标拖拽事件
   * @param {*} event
   * @return {*}
   */
  const paperMouseDrag = (event: paper.ToolEvent) => {
   // 如果存在碰撞路径,且碰撞路径是背景图时,则可以创建选框
    if (
      isCreate.value &&
      state.hitPath &&
      state.hitPath.name === PATN_NAME.BG_IMAGE
    ) {
    // 创建选框
      state.currentPathStatus = CurrentPathStatus.CREATE;
      state.currentPath = new myPaper.Path.Rectangle({
        name:PATH_NAME.INFO,
        point: event.downPoint,
        size: [event.point.x - event.downPoint.x, event.point.y - event.downPoint.y],
        data: {
         // 携带参数
        },
      });
      state.currentPath.set(infoStyle.edit);
      state.currentPath.removeOnDrag();
      // 碰撞检测
      if (isHit(state.currentPath as paper.Path)) {
        // 如果发生碰撞,删除当前路径
        state.currentPath.remove();
        state.currentPath = new myPaper.Path.Rectangle({
          name: PATH_NAME.INFO,
          point: event.downPoint,
          size: state.lastRectSize,
          data: {
           // 携带参数
          },
        });
        state.currentPath.set(infoStyle.edit);
        state.currentPath.removeOnDrag();
      } else {
        // 如果没发生碰撞,保存当前选框尺寸大小
        state.lastRectSize = [event.point.x - event.downPoint.x, event.point.y - event.downPoint.y];
      }
    } else if (state.hitSegment) {
      // 缩放选框
      moveEditPathSegment(Operator.ADD, event.delta);
      // 缩放过程中进行碰撞检测
      if (isHit(state.currentPath as paper.Path)) {
        // 如果发生碰撞,则移动回原来位置
        moveEditPathSegment(Operator.SUBTRACT, event.delta);
      }
    } else if (state.currentPathStatus !== CurrentPathStatus.DEFAULT && state.hitPath?.name ===PATH_NAME.INFO && state.hitPath?.selected) {
      // 移动选框
      moveEditPath(Operator.ADD, event.delta);
      // 移动路径过程中进行碰撞检测
      if (isHit(state.currentPath as paper.Path)) {
        // 如果发生碰撞,则移动回原来位置
        moveEditPath(Operator.SUBTRACT, event.delta);
      }
    } else if (!isCreate.value && (state.hitPath?.name ===PATH_NAME.BG_IMAGE) {
      // 移动画布
      // 拖拽移动画布位置,当鼠标点击空白区域时可以移动画布位置,以及鼠标在非编辑状态下点击背景图时可以移动画布位置。
      myPaper?.project.layers?.forEach((item: paper.Layer) => {
        item.position.x += event.delta.x;
        item.position.y += event.delta.y;
      });
      // 画布位置移动之后,需要重新计算 startX 和 startY
      // 获取背景图左上角的点位坐标
      if (state.bgPath) {
        state.startX = state.bgPath.position.x - state.bgPath.bounds.width / 2;
        state.startY = state.bgPath.position.y - state.bgPath.bounds.height / 2;
      }
    }
  };

2.1,碰撞检测,判断当前路径是否发生碰撞

state.hitTestPaths 中的路径即为 state.step1Layer.children 中的路径

  /**
   * @description: 是否发生碰撞
   * @param {*} path
   * @return {*}
   */
  const isHit = (path: paper.Path): boolean => {
    // 获取背景图路径边界
    let bounds = state.bgPath?.bounds;
   
    if (bounds && (path.bounds.left < bounds.left || path.bounds.right > bounds.right || path.bounds.top < bounds.top || path.bounds.bottom > bounds.bottom)) {
      return true;
    }
    // 检测碰撞群体是否发生碰撞
    for (let i = 0; i < state.hitTestPaths.length; i++) {
      if (state.hitTestPaths[i] !== path) {
        const overlop = path.intersect(state.hitTestPaths[i]);
        overlop?.remove();
        if (overlop?.area > 0) {
          return true;
        }
      }
    }
    // 默认没发生碰撞
    return false;
  };

2.2,移动编辑状态路径

  /**
   * @description: 移动编辑状态选框到目标位置
   * @param {*} operator
   * @param {*} point
   * @return {*}
   */
  const moveEditPath = (operator: Operator, point: paper.Point) => {
    if (operator === Operator.ADD && state.currentPath && state.finishBtn) {
      // 移动选框
      state.currentPath.parent.position.x += point.x;
      state.currentPath.parent.position.y += point.y;
      // 移动操作按钮
      state.finishBtn.position.x += point.x;
      state.finishBtn.position.y += point.y;
      if (state.deleteBtn) {
        state.deleteBtn.position.x += point.x;
        state.deleteBtn.position.y += point.y;
      }
    } else if (operator === Operator.SUBTRACT && state.currentPath && state.finishBtn) {
      // 移动选框
      state.currentPath.parent.position.x -= point.x;
      state.currentPath.parent.position.y -= point.y;
      // 移动操作按钮
      state.finishBtn.position.x -= point.x;
      state.finishBtn.position.y -= point.y;
      if (state.deleteBtn) {
        state.deleteBtn.position.x -= point.x;
        state.deleteBtn.position.y -= point.y;
      }
    }
  };

2.3,缩放选框

缩放选框,其实就是拖拽移动相关顶点位置,四个顶点的序号依次为 左下:0,左上:1,右上:2,右下:3。当拖拽某一个顶点缩放选框时,其相邻的两个顶点的位置也会跟着发生变化。

  /**
   * @description: 移动编辑状态选框顶点缩放选框大小
   * @param {*} operator
   * @param {*} point
   * @return {*}
   */
  const moveEditPathSegment = (operator: Operator, point: paper.Point) => {
    if (operator === Operator.ADD && state.hitSegment) {
      state.hitSegment.point.x += point.x;
      state.hitSegment.point.y += point.y;
      // 水平方向修改顶点位置
      if (state.hitSegment._index % 2 == 0) {
        state.hitSegment.next.point.x += point.x;
        state.hitSegment.previous.point.y += point.y;
      } else {
        // 垂直方向修改顶点位置
        state.hitSegment.previous.point.x += point.x;
        state.hitSegment.next.point.y += point.y;
      }
    } else if (operator === Operator.SUBTRACT && state.hitSegment) {
      state.hitSegment.point.x -= point.x;
      state.hitSegment.point.y -= point.y;
      // 水平方向修改顶点位置
      if (state.hitSegment._index % 2 == 0) {
        state.hitSegment.next.point.x -= point.x;
        state.hitSegment.previous.point.y -= point.y;
      } else {
        // 垂直方向修改顶点位置
        state.hitSegment.previous.point.x -= point.x;
        state.hitSegment.next.point.y -= point.y;
      }
    }
    // 移动操作按钮
    setOperatorBtnShow(state.currentPath as paper.Path);
  };

3,mouseup 绘制完成设置选框为选中状态,移动并显示操作按钮

  /**
   * @description: paper鼠标抬起事件
   * @param {*} event
   * @return {*}
   */
  const paperMouseUp = () => {
    if (isCreate.value && state.currentPathStatus === CurrentPathStatus.CREATE && state.currentPath) {
      // 创建选框鼠标抬起事件
      // 显示操作按钮
      setOperatorBtnShow(state.currentPath as paper.Path);
      const group = new myPaper.Group();
      group.addChild(state.currentPath);

      // 将新增选框添加进选框图层中
      state.step1Layer?.addChild(group);
      // 删除上一次新增选框
      state.lastCreatePath?.remove();
      state.lastCreatePath = group;

      state.currentPath.selected = true;
      // 修改选中状态边框样式
      state.currentPath.selectedColor = infoStyle.edit.strokeColor;
    }
  };

八,操作按钮绑定事件

根据 state.currentPathStatus 当前操作路径状态的值,删除或者添加路径

1,删除按钮绑定事件

/**
   * @description: 点击删除按钮事件
   * @return {*}
   */
  const deleteBtnClick = () => {
    // 如果当前是创建状态,直接删除即可
    if (state.currentPathStatus === CurrentPathStatus.CREATE) {
      state.currentPath?.parent.remove();
      // 修改当前状态为默认状态
      state.currentPathStatus = CurrentPathStatus.DEFAULT;
      // 隐藏操作按钮
      setOperatorBtnHide();
      // 更新新增框选样式
      emits('update-is-create', false);
    } else {
      // 删除选框信息
      emits('delete-info', state.currentPath?.data);
    }
  };

2,完成按钮绑定事件

  /**
   * @description: 点击完成按钮事件
   * @return {*}
   */
  const finishBtnClick = () => {
    // 如果当前是创建状态,则新建选框信息
    if (state.currentPathStatus === CurrentPathStatus.CREATE) {
      // 添加选框信息
      emits('add-info',state.currentPath.data);
    } else {
      // 移动选框位置
      emits('update-info-rect',state.currentPath.data);
    }
  };

九,响应式渲染

监听窗口尺寸变化,重置画布大小,在每次初始化时执行 Paper.view.setViewSize(), 可以使得画布更加清晰。

   /**
   * @description: 窗口大小发生改变
   * @return {*}
   */
      const resizeHandle = () => {
        if (myPaper?.view && canvas.value) { 
                       myPaper.view.setViewSize(canvas.value?.clientWidth,canvas.value?.clientHeight);
        }
  };



    // 监听窗口大小改变事件
    window.addEventListener('resize', resizeHandle);


十,组件销毁、事件解绑。

在组件卸载时及时卸载 paper.js 中绑定的事件,避免内存泄露。

onUnmounted(() => {
    if (state.deleteBtn) {
      state.deleteBtn.onClick = null;
      state.deleteBtn.onMouseMove = null;
    }

    if (state.finishBtn) {
      state.finishBtn.onClick = null;
      state.finishBtn.onMouseMove = null;
    }

    if (state.tool) {
      // 注册鼠标按下事件
      state.tool.off('onMouseDown', paperMouseDown);

      // 注册鼠标移动事件
      state.tool.off('onMouseMove', paperMouseMove);

      // 注册拖拽事件
      state.tool.off('onMouseDrag', paperMouseDrag);

      // 注册鼠标抬起事件
      state.tool.off('onMouseUp', paperMouseUp);
    }

    if (canvas.value) {
      canvas.value.remove();
    }

    window.removeEventListener('resize', resizeHandle);
  });

总结 

到此这篇关于利用paper.js实现图片简单框选标注功能的文章就介绍到这了,更多相关paper.js图片简单框选标注内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

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