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

利用paper.js實現(xiàn)圖片簡單框選標注功能

 更新時間:2025年10月22日 08:28:44   作者:夢凡塵  
Paper.js是一個 JavaScript庫用來制作繪圖和動畫,這篇文章主要介紹了利用paper.js實現(xiàn)圖片簡單框選標注功能的相關(guān)資料,文中通過代碼介紹的非常詳細,需要的朋友可以參考下

一,效果

在開發(fā)中的實際業(yè)務(wù)場景可能會更加復(fù)雜,這里只展示操作的核心代碼,不包含任務(wù)業(yè)務(wù)邏輯。

二,引入并注冊 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;
 // 當(dāng)前縮放比例
 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;
    }

    // 監(jiān)聽窗口大小改變事件
    window.addEventListener('resize', resizeHandle);
  });
</script>

三,初始化背景圖,居中加載,縮放至適當(dāng)比例

1,將背景圖層及背景圖路徑保存在全局變量 state 中

后面代碼中的state.xxxxx,默認是在 state 中已經(jīng)聲明好的。

const state = {
    // 背景圖層
    bgLayer: null,
    // 背景圖路徑
    bgPath: null,
    // 背景圖左上角頂點 x 坐標
    startX: 0,
    // 背景圖左上角頂點 y 坐標
    startY: 0,
}

2,初始化背景圖層

LayerName 是各個圖層名稱的枚舉變量,給圖層命名方便使用時直接通過名稱獲取需要操作的圖層。

  /**
   * @description: 初始化背景圖層
   * @return {*}
   */
  const initBgLayer = async () => {
    // 創(chuàng)建第一個背景圖層
    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);
    }
    // 設(shè)置bgLayer為活動圖層
    // 使用 Promise 等待圖片加載完成
    await new Promise((resolve, reject) => {
      state.bgPath= new myPaper.Raster({
        source: props.imageData.convertFileName,
        onLoad: () => {
          resolve(void 0); // 加載成功時調(diào)用 resolve
        },
        onError: () => {
          reject(new Error('加載失敗')); // 加載失敗時調(diào)用 reject
        },
      });
    });
    // 設(shè)置背景圖路徑名稱
    state.bgPath.name = PATN_NAME.BG_IMAGE
    // 計算縮放比例
    calculateScale(props.imageData.imageWidth, props.imageData.imageHeight, myPaper.view.size.width, myPaper.view.size.height);
    
    // 根據(jù)圖片的寬高和畫布的寬高計算初始縮放比例
    myPaper.view.scale(scale.value);
    // 設(shè)置居中顯示
    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;
    // 如果照片的寬高比大于畫布的寬高比,則根據(jù)寬度計算縮放比例
    if (imageRatio > canvasRatio) {
      // 初始縮放比例按照窗口邊緣20px留白為準
      scale.value = (canvasWidth - 40) / imageWidth;
    } else {
      // 如果照片的寬高比小于或等于畫布的寬高比,則根據(jù)高度計算縮放比例
      scale.value = (canvasHeight - 40) / imageHeight;
    }
    // 將縮放比例保留兩位小數(shù)
    scale.value = parseFloat(scale.value.toFixed(2));
  };

四,畫布縮放

每次放大、縮小畫布時都要執(zhí)行 myPaper.view.zoom = 1。重置畫布比例,這樣執(zhí)行 paper.view.scale();設(shè)置畫布縮放時才能得到準確的結(jié)果。

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 路徑中直接找到對應(yīng)的路徑進行操作( 當(dāng) group 中的子路徑不唯一時 )。

  /**
   * @description: 初始化選框信息圖層
   * @return {*}
   */
  const initStep1Layer = () => {
    if (!state.step1Layer) {
      // 創(chuàng)建選框信息圖層
      state.step1Layer = new myPaper.Layer();
      // 設(shè)置圖層名稱
      state.step1Layer.name = LayerName.RECT;
    } else {
      // 如果存在圖層,則需要將圖層中的所有路徑刪除
      state.step1Layer?.removeChildren();
    }

    // 如果當(dāng)前項目中不包括框選圖層,則要將框選圖層添加到當(dāng)前項目中
    if (!myPaper?.project.layers[LayerName.Rect as any]) {
      myPaper?.project.addLayer(state.step1Layer);
    }

    // 遍歷選框信息數(shù)據(jù)繪制矩形框
    props.data?.forEach((item:Data) => {
      // 創(chuàng)建選框信息路徑群組,可能選框會包括別的標簽內(nèi)容
      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,
        },
      });
      // 根據(jù)步驟修改信息選框樣式
      modifyInfoRectStyle(info, Step.STEP1);
      group.addChild(info);
      state.step1Layer?.addChild(group);
    });
  };

2,設(shè)置選框樣式

根據(jù)條件或者狀態(tài)的不同修改路徑樣式,infoStyle 是路徑的樣式配置對象

通過 path.set() 進行批量設(shè)置樣式。

// 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) {
      // 設(shè)置路徑樣式
      path.set(infoStyle.default);
    } else {
      // 否則繪制禁用狀態(tài)
      path.set(infoStyle.disabled);
    }
  };

六,初始化操作按鈕,添加點擊事件

給按鈕路徑綁定點擊事件,在 paper.js 中的路徑對象中都能監(jiān)聽各種鼠標事件。

  /**
   * @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;
    }

    // 創(chuàng)建完成按鈕
    state.finishBtn = addOperatorBtn(OperatorBtnType.FINISH);
    state.operatorLayer?.addChild(state.finishBtn);
    // 注冊完成按鈕點擊事件
    state.finishBtn.onClick = finishBtnClick;
    state.finishBtn.onMouseMove = operatorBtnMouseMove;
   
    // 創(chuàng)建刪除按鈕
    state.deleteBtn = addOperatorBtn(OperatorBtnType.DELETE);
    state.operatorLayer?.addChild(state.deleteBtn);
    // 注冊刪除按鈕點擊事件
    state.deleteBtn.onClick = deleteBtnClick;
    state.deleteBtn.onMouseMove = operatorBtnMouseMove;
  
  };



  /**
   * @description: 給當(dāng)前操作路徑又下角添加操作按鈕
   * @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 = '完成';
    }
    // 創(chuàng)建一個組合
    const group = new myPaper.Group();
    // 設(shè)置操作按鈕名稱,方便刪除
    group.name = btnType;
    // 創(chuàng)建刪除矩形路徑
    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);

    // 創(chuàng)建刪除文本路徑放置在上面的矩形路徑中,字體顏色為白色
    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 獲取所有需要進行碰撞檢測的路徑,(還有切換選框編輯狀態(tài)的功能)

CurrentPathStatus 是當(dāng)前路徑操作的枚舉值,有創(chuàng)建狀態(tài) CurrentPathStatus.CREATE,編輯狀態(tài) CurrentPathStatus.EDIT,默認狀態(tài) CurrentPathStatus.DEFAULT。

isCreate.value  表示當(dāng)前是否可以創(chuàng)建選框

由于當(dāng)前場景比較簡單,所有需要進行碰撞檢測的路徑即為 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);
    // 操作按鈕有單獨的點擊事件,不執(zhí)行一下邏輯
    if (hitResult?.item?.name ===PATN_NAME.OPERATOR) return;
    // 如果當(dāng)前是創(chuàng)建狀態(tài),則只能點擊新建選框
    // if (state.currentPathStatus === CurrentPathStatus.CREATE && hitResult?.item !== state.currentPath) return;
    if (hitResult) {
      state.hitPath = hitResult.item;

      // 點擊選框設(shè)置為編輯狀態(tài)
      if (
        !isCreate.value &&
        state.currentPathStatus === CurrentPathStatus.DEFAULT &&
        state.hitPath.name ===PATH_NAME.INFO &&
        !state.hitPath.selected 
      ) {
        // 當(dāng)前是默認狀態(tài)時,設(shè)置選框為編輯狀態(tài)
        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 
      ) {
        // 當(dāng)前是編輯狀態(tài),則需要將當(dāng)前路徑設(shè)置為默認狀態(tài),然后將點擊路徑設(shè)置為編輯狀態(tài),并更新state.currentPath
        switchPathDefaultStatus(state.currentPath as paper.Path);
        // 將碰撞路徑切換至編輯狀態(tài)
        switchPathEditStatus(state.hitPath as paper.Path);
        // 更新當(dāng)前路徑
        state.currentPath = state.hitPath as paper.Path;
        // 獲取碰撞檢測群體路徑
        // 由于場景比較簡單,不需要單獨獲取碰撞檢測群體路徑,state.step1Layer 圖層中的路徑即為碰撞檢測路徑。
      }

      // 創(chuàng)建選框時,鼠標按下事件獲取所有需要碰撞檢測的路徑
      if (
        isCreate.value &&
        state.hitPath?.name === PATH_NAME.BG_IMAGE
      ) {
       
        // 獲取創(chuàng)建選框時需要的碰撞檢測路徑
         // 由于場景比較簡單,不需要單獨獲取碰撞檢測群體路徑,state.step1Layer 圖層中的路徑即為碰撞檢測路徑。
      } else if (state.currentPathStatus === CurrentPathStatus.CREATE && state.hitPath?.name === PATH_NAME.INFO) {
        // 此時是剛創(chuàng)建完選框時,點擊拖拽移動新創(chuàng)建選框位置
        // 獲取當(dāng)前選框碰撞群體數(shù)據(jù)
        state.currentPath = state.hitPath as paper.Path;
        // 由于場景比較簡單,不需要單獨獲取碰撞檢測群體路徑,state.step1Layer 圖層中的路徑即為碰撞檢測路徑。
      }

      if (state.currentPathStatus !== CurrentPathStatus.DEFAULT && state.hitPath?.name === PATH_NAME.INFO && hitResult.type === 'segment') {
        // 創(chuàng)建和編輯狀態(tài)時可以點擊選框頂點進行縮放選框大小
        // 記錄當(dāng)前點擊的路徑頂點
        state.hitSegment = hitResult.segment;
      }
    }
  };


  /**
   * @description: 切換路徑默認狀態(tài)
   * @param {*} path
   * @return {*}
   */
  const switchPathDefaultStatus = (path: paper.Path) => {
    // 切換路徑默認狀態(tài)
    state.currentPathStatus = CurrentPathStatus.DEFAULT;
    if (path) {
      // 設(shè)置選框路徑樣式為默認狀態(tài)
      path.set(infoStyle.default);
      // 設(shè)置沒有選中
      path.selected = false;
    }
    // 隱藏操作按鈕
    setOperatorBtnHide();
  };

  /**
   * @description: 設(shè)置操作按鈕隱藏
   * @return {*}
   */
  const setOperatorBtnHide = () => {
    // 隱藏操作按鈕
    if (state.deleteBtn) state.deleteBtn.visible = false;
    if (state.finishBtn) state.finishBtn.visible = false;
  };

  /**
   * @description: 切換路徑編輯狀態(tài)
   * @param {*} path
   * @return {*}
   */
  const switchPathEditStatus = (path: paper.Path) => {
    // 切換路徑編輯狀態(tài)
    state.currentPathStatus = CurrentPathStatus.EDIT;
    if (path) {
      // 設(shè)置選框路徑樣式為編輯狀態(tài)
      path.set(infoStyle.edit);
      path.selectedColor = infoStyle.edit.strokeColor;
      // 設(shè)置選中
      path.selected = true;
    }
    // 移動操作按鈕到路徑的右下角
    setOperatorBtnShow(path);
  };


  /**
   * @description: 設(shè)置操作按鈕顯示
   * @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 當(dāng)鼠標點擊在 無選框區(qū)域 繪制選框,并在繪制的過程中進行碰撞檢測, 移動畫布,移動選框,縮放選框。

event.delta: 相對于上一次事件執(zhí)行時的位移矢量

新建選框沒有發(fā)生碰撞:刪除上一次拖拽創(chuàng)建的矩形路徑 -----> 創(chuàng)建矩形路徑 -----> 沒有發(fā)生碰撞 ----> 記錄當(dāng)前矩形路徑的坐標和尺寸

新建選框發(fā)生碰撞:刪除上一次拖拽創(chuàng)建的矩形路徑  -----> 創(chuàng)建矩形路徑 ------> 發(fā)生碰撞 -----> 刪除碰撞檢測失敗的矩形路徑 ------> 還原上次拖拽事件中創(chuàng)建的矩形路徑

移動畫布:移動 project 中所有的圖層即可。

 /**
   * @description: paper鼠標拖拽事件
   * @param {*} event
   * @return {*}
   */
  const paperMouseDrag = (event: paper.ToolEvent) => {
   // 如果存在碰撞路徑,且碰撞路徑是背景圖時,則可以創(chuàng)建選框
    if (
      isCreate.value &&
      state.hitPath &&
      state.hitPath.name === PATN_NAME.BG_IMAGE
    ) {
    // 創(chuàng)建選框
      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: {
         // 攜帶參數(shù)
        },
      });
      state.currentPath.set(infoStyle.edit);
      state.currentPath.removeOnDrag();
      // 碰撞檢測
      if (isHit(state.currentPath as paper.Path)) {
        // 如果發(fā)生碰撞,刪除當(dāng)前路徑
        state.currentPath.remove();
        state.currentPath = new myPaper.Path.Rectangle({
          name: PATH_NAME.INFO,
          point: event.downPoint,
          size: state.lastRectSize,
          data: {
           // 攜帶參數(shù)
          },
        });
        state.currentPath.set(infoStyle.edit);
        state.currentPath.removeOnDrag();
      } else {
        // 如果沒發(fā)生碰撞,保存當(dāng)前選框尺寸大小
        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)) {
        // 如果發(fā)生碰撞,則移動回原來位置
        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)) {
        // 如果發(fā)生碰撞,則移動回原來位置
        moveEditPath(Operator.SUBTRACT, event.delta);
      }
    } else if (!isCreate.value && (state.hitPath?.name ===PATH_NAME.BG_IMAGE) {
      // 移動畫布
      // 拖拽移動畫布位置,當(dāng)鼠標點擊空白區(qū)域時可以移動畫布位置,以及鼠標在非編輯狀態(tài)下點擊背景圖時可以移動畫布位置。
      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,碰撞檢測,判斷當(dāng)前路徑是否發(fā)生碰撞

state.hitTestPaths 中的路徑即為 state.step1Layer.children 中的路徑

  /**
   * @description: 是否發(fā)生碰撞
   * @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;
    }
    // 檢測碰撞群體是否發(fā)生碰撞
    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;
        }
      }
    }
    // 默認沒發(fā)生碰撞
    return false;
  };

2.2,移動編輯狀態(tài)路徑

  /**
   * @description: 移動編輯狀態(tài)選框到目標位置
   * @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,縮放選框

縮放選框,其實就是拖拽移動相關(guān)頂點位置,四個頂點的序號依次為 左下:0,左上:1,右上:2,右下:3。當(dāng)拖拽某一個頂點縮放選框時,其相鄰的兩個頂點的位置也會跟著發(fā)生變化。

  /**
   * @description: 移動編輯狀態(tài)選框頂點縮放選框大小
   * @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 繪制完成設(shè)置選框為選中狀態(tài),移動并顯示操作按鈕

  /**
   * @description: paper鼠標抬起事件
   * @param {*} event
   * @return {*}
   */
  const paperMouseUp = () => {
    if (isCreate.value && state.currentPathStatus === CurrentPathStatus.CREATE && state.currentPath) {
      // 創(chuàng)建選框鼠標抬起事件
      // 顯示操作按鈕
      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;
      // 修改選中狀態(tài)邊框樣式
      state.currentPath.selectedColor = infoStyle.edit.strokeColor;
    }
  };

八,操作按鈕綁定事件

根據(jù) state.currentPathStatus 當(dāng)前操作路徑狀態(tài)的值,刪除或者添加路徑

1,刪除按鈕綁定事件

/**
   * @description: 點擊刪除按鈕事件
   * @return {*}
   */
  const deleteBtnClick = () => {
    // 如果當(dāng)前是創(chuàng)建狀態(tài),直接刪除即可
    if (state.currentPathStatus === CurrentPathStatus.CREATE) {
      state.currentPath?.parent.remove();
      // 修改當(dāng)前狀態(tài)為默認狀態(tài)
      state.currentPathStatus = CurrentPathStatus.DEFAULT;
      // 隱藏操作按鈕
      setOperatorBtnHide();
      // 更新新增框選樣式
      emits('update-is-create', false);
    } else {
      // 刪除選框信息
      emits('delete-info', state.currentPath?.data);
    }
  };

2,完成按鈕綁定事件

  /**
   * @description: 點擊完成按鈕事件
   * @return {*}
   */
  const finishBtnClick = () => {
    // 如果當(dāng)前是創(chuàng)建狀態(tài),則新建選框信息
    if (state.currentPathStatus === CurrentPathStatus.CREATE) {
      // 添加選框信息
      emits('add-info',state.currentPath.data);
    } else {
      // 移動選框位置
      emits('update-info-rect',state.currentPath.data);
    }
  };

九,響應(yīng)式渲染

監(jiān)聽窗口尺寸變化,重置畫布大小,在每次初始化時執(zhí)行 Paper.view.setViewSize(), 可以使得畫布更加清晰。

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



    // 監(jiān)聽窗口大小改變事件
    window.addEventListener('resize', resizeHandle);


十,組件銷毀、事件解綁。

在組件卸載時及時卸載 paper.js 中綁定的事件,避免內(nèi)存泄露。

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

總結(jié) 

到此這篇關(guān)于利用paper.js實現(xiàn)圖片簡單框選標注功能的文章就介紹到這了,更多相關(guān)paper.js圖片簡單框選標注內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • JavaScript實現(xiàn)拖拽和縮放效果

    JavaScript實現(xiàn)拖拽和縮放效果

    這篇文章主要為大家詳細介紹了JavaScript實現(xiàn)拖拽和縮放效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-08-08
  • webpack自動化打包方式詳解

    webpack自動化打包方式詳解

    這篇文章主要介紹了webpack自動化打包的相關(guān)知識,本文結(jié)合實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧
    2023-02-02
  • JavaScript高級函數(shù)應(yīng)用之分時函數(shù)實例分析

    JavaScript高級函數(shù)應(yīng)用之分時函數(shù)實例分析

    這篇文章主要介紹了JavaScript高級函數(shù)應(yīng)用之分時函數(shù),結(jié)合實例形式分析了javascript通過合理分時函數(shù)應(yīng)用避免瀏覽器卡頓或假死的相關(guān)操作技巧,需要的朋友可以參考下
    2018-08-08
  • w3c編程挑戰(zhàn)_初級腳本算法實戰(zhàn)篇

    w3c編程挑戰(zhàn)_初級腳本算法實戰(zhàn)篇

    下面小編就為大家?guī)硪黄獁3c編程挑戰(zhàn)_初級腳本算法實戰(zhàn)篇。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-06-06
  • js 毫秒轉(zhuǎn)天時分秒的實例

    js 毫秒轉(zhuǎn)天時分秒的實例

    下面小編就為大家分享一篇js 毫秒轉(zhuǎn)天時分秒的實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2017-11-11
  • 淺談js函數(shù)中的實例對象、類對象、局部變量(局部函數(shù))

    淺談js函數(shù)中的實例對象、類對象、局部變量(局部函數(shù))

    下面小編就為大家?guī)硪黄獪\談js函數(shù)中的實例對象、類對象、局部變量(局部函數(shù))。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2016-11-11
  • js改變style樣式和css樣式的簡單實例

    js改變style樣式和css樣式的簡單實例

    下面小編就為大家?guī)硪黄猨s改變style樣式和css樣式的簡單實例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2016-06-06
  • layui 動態(tài)設(shè)置checbox 選中狀態(tài)的例子

    layui 動態(tài)設(shè)置checbox 選中狀態(tài)的例子

    今天小編就為大家分享一篇layui 動態(tài)設(shè)置checbox 選中狀態(tài)的例子,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2019-09-09
  • JavaScript中Array實例方法map的實現(xiàn)方法

    JavaScript中Array實例方法map的實現(xiàn)方法

    這篇文章主要介紹了JavaScript中Array實例方法map的實現(xiàn)方法,map() 方法創(chuàng)建一個新數(shù)組,其結(jié)果是原數(shù)組中的每個元素都調(diào)用一個提供的函數(shù)后返回的結(jié)果,文中有詳細的代碼示例供大家參考,需要的朋友可以參考下
    2024-03-03
  • 收藏,對比功能的JS部分實現(xiàn)代碼

    收藏,對比功能的JS部分實現(xiàn)代碼

    非常不錯的,收藏效果代碼,增加當(dāng)前頁的現(xiàn)實,實現(xiàn)對比
    2008-08-08

最新評論

北京市| 普兰店市| 庆城县| 全椒县| 中阳县| 于都县| 甘孜| 奉化市| 陆丰市| 呈贡县| 光泽县| 庆阳市| 沁水县| 彭泽县| 沛县| 义马市| 溆浦县| 凉城县| 五台县| 东港市| 康马县| 陕西省| 军事| 泸定县| 宜章县| 淅川县| 平和县| 连云港市| 宁晋县| 龙门县| 招远市| 上饶县| 奇台县| 苏尼特左旗| 思茅市| 蛟河市| 剑阁县| 张掖市| 抚顺市| 射阳县| 诸暨市|