canvas畫圖(矩形虛實框、數字填充矩形、填充三角形)

大背景:試卷拆錄圖片識別功能,須要根據後端返回的數據實現識別框:前端

識別框:react

  • 1)顯示在圖片中,奇數框爲實線、偶數框爲虛線。識別框不可調整大小及位置;canvas

  • 2)框的左側顯示當前題的編號,編號從1開始,按照從上至下,從左至右的順序遞增;後端

    這是UI設計的題目編號,以下圖:數組

拿到需求以後細化一下功能點,開始搞起。bash

  • 根據後端返回座標實現虛實框圈題;
    • 畫矩形實線框
    • 畫矩形虛線框
    • 畫矩形的兩種方式
    • 將picture畫在畫布上
  • 根據後端返回座標實現標題號;
    • 畫填充矩形
    • 畫填充三角形
1、後端返回結果數據mock

again吐槽,由於後端返回格式有點不盡人意,須要前端處理數據排序; 須要注意的是返回數據的座標是根據上傳的原圖定位的,可是當咱們在canvas中畫圖的話通常不是直接將原圖大小畫上去,須要必定的比例,因此處理數據的時候也要根據這個比例去顯示;字體

const dataList = [ 
	{
		itemPos: [[193, 93], [1204, 105], [1203, 231], [191, 219]],
	},
	{
		itemPos: [[194, 218], [1026, 227], [1025, 347], [193, 337]],
	},
	{
		itemPos: [[197, 338], [1066, 350], [1064, 547], [194, 536]],
	},
	{
		itemPos: [[193, 534], [1216, 547], [1212, 941], [188, 929]],
	}
]
複製代碼
2、canvas
  • width: 父組件傳過來的width(這個width是canvas外圍div設置的width,並非圖片的實際width)
  • height: 按照比例算的
  • ref: 根據ref獲取canvas元素
<canvas
  width={width}
  height={(backgroundImg.height * (width / backgroundImg.width)).toFixed(2)}
  ref={canvasTarget}
/>
複製代碼
3、將上傳圖片畫在畫布上

說明一下:由於父組件的寬度是定死的,因此proportion是直接用父組件div的寬度/圖片的原始widthui

  • backgroundImg:父組件傳過來的img標籤
  • width: 父組件的width
  • clearRect(): 清除指定大小的畫布,若是不清除的話,圖片會一個疊一個在畫布上;
  • drawImage(): 畫圖,width是定死的,直接使用父組件的width,height是根據比例算的
  • drawRectangularBox(): 識別框(虛實框)實現
  • drawTitleNumber(): 識別框題號實現
drawImage() {
	if (!backgroundImg) {
	  return;
	}
	const backgroundImgClone = backgroundImg.cloneNode(false); // 返回數據中的位置是按照圖片原像素,頁面圖片是處理過的,故須要得出比例
	let proportion = width / backgroundImgClone.width;
	const ctx = canvasTarget.current.getContext('2d');
	ctx.clearRect(0, 0, width, height);
	ctx.drawImage(backgroundImg, 0, 0, width, backgroundImgClone.height * proportion);
	if (!dataList || !Array.isArray(dataList)) {
	  return;
	}
	if (dataList.length > 0) {
	  for (let i = 0; i < dataList.length; i++) {
	    let { itemPos } = dataList[i][1];
	    drawRectangularBox(ctx, i, itemPos, proportion);
	    drawTitleNumber(ctx, i, itemPos[0], proportion);
	  }
	}
}
複製代碼
4、兩種畫矩形(邊框)的方式
  • 第一種:用一個座標和長寬畫圖
const ctx = canvasTarget.current.getContext('2d');
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = '#84B0F0';
ctx.rect(x, y, width, height);
ctx.stroke();
ctx.closePath();
複製代碼
  • 第二種:用四個座標畫圖(路徑繪製矩形):座標數據使用的mock的第一組數據
ctx.beginPath();  
ctx.moveTo(193, 93);  
ctx.lineTo(1204, 105);  
ctx.lineTo(1203, 231);  
ctx.lineTo(191, 219);  
ctx.lineTo(193, 93);  
ctx.stroke();
ctx.closePath();  
複製代碼
5、虛實框實現

由於畫圖的時候考慮到按照比例實現,故採用第一種方式畫矩形框;spa

setLineDash(): 設置邊框線的樣式,參數爲一個數組,數組爲空則爲實線;設計

官方文檔是這樣解釋的:它使用一組值來指定描述模式的線和間隙的交替長度。

const drawRectangularBox = useCallback((ctx, index, rectParams, proportion) => {
	let x = (rectParams[0][0]) * proportion;
	let y = (rectParams[0][1]) * proportion;
	let rectWidth = (rectParams[1][0] - rectParams[0][0]) * proportion;
	let rectHeight = (rectParams[3][1] - rectParams[0][1]) * proportion;
	ctx.beginPath();
	if (index % 2 === 0) {
	  ctx.setLineDash([5, 5]);
	} else {
	  ctx.setLineDash([]);
	}
	ctx.lineWidth = 1;
	ctx.strokeStyle = '#84B0F0';
	ctx.rect(x, y, rectWidth, rectHeight);
	ctx.stroke();
	ctx.closePath();
}, []);
複製代碼
6、題目編號實現

這部分我是拆開實現的,一共是3個功能點:

  • 第一:畫填充矩形
  • 第二:將數字填充到矩形
  • 第三:畫填充三角形
const drawTitleNumber = useCallback((ctx, index, rectParams, proportion) => {
	drawRectangle(ctx, index, rectParams, proportion);
	drawTriangles(ctx, rectParams, proportion);
}, []);
複製代碼
7、題目編號填充矩形

須要注意的是fillStyle只對本身最近的那個操做起做用(也有可能我表達的不許確,可是你懂就行),我用了2次,一次是設置填充矩形的顏色,一次是設置填充字體的顏色;

const drawRectangle = useCallback((ctx, index, rectParams, proportion) => {
	let x = rectParams[0] * proportion - 15; // 保證題目編號在虛實框的左側
	let y = rectParams[1] * proportion;
	ctx.beginPath();
	ctx.fillStyle = '#BAD3F6';
	ctx.fillRect(x, y, 15, 15);
	ctx.fillStyle = '#042044';
	ctx.fillText(index + 1, x + 2, y + 12);
	ctx.font = '12px "PingFangSC-Regular"';
	ctx.closePath();
}, []);
複製代碼
8、小三角實現
const drawTriangles = useCallback((ctx, rectParams, proportion) => {
	let x = rectParams[0] * proportion - 15;
	let y = rectParams[1] * proportion;
	ctx.beginPath();
	ctx.moveTo((x + 15), (y + 3));
	ctx.lineTo((x + 22), (y + 8));
	ctx.lineTo((x + 15), (y + 13));
	ctx.fillStyle = '#BAD3F6';
	ctx.fill();
	ctx.closePath();
}, []);
複製代碼
最後、整個CanvasDrawer組件,能夠直接使用;

之因此這麼一層一層的去寫,是由於須要有一個按部就班的過程讓本身再看下是否有不妥或者遺漏的地方,歡迎指正,一塊兒進步。

import { useEffect, useRef, useCallback } from 'react';

const CanvasDrawer = (props) => {
  const { width, height, backgroundImg, dataList } = props;
  const canvasTarget = useRef(null);

  const drawRectangle = useCallback((ctx, index, rectParams, proportion) => {
    let x = rectParams[0] * proportion - 15;
    let y = rectParams[1] * proportion;
    ctx.beginPath();
    ctx.fillStyle = '#BAD3F6';
    ctx.fillRect(x, y, 15, 15);
    ctx.fillStyle = '#042044';
    ctx.fillText(index + 1, x + 2, y + 12);
    ctx.font = '12px "PingFangSC-Regular"';
    ctx.closePath();
  }, []);

  const drawTriangles = useCallback((ctx, rectParams, proportion) => {
    let x = rectParams[0] * proportion - 15;
    let y = rectParams[1] * proportion;
    ctx.beginPath();
    ctx.moveTo((x + 15), (y + 3));
    ctx.lineTo((x + 22), (y + 8));
    ctx.lineTo((x + 15), (y + 13));
    ctx.fillStyle = '#BAD3F6';
    ctx.fill();
    ctx.closePath();
  }, []);

  const drawRectangularBox = useCallback((ctx, index, rectParams, proportion) => {
    let x = (rectParams[0][0]) * proportion;
    let y = rectParams[0][1] * proportion;
    let rectWidth = (rectParams[1][0] - rectParams[0][0]) * proportion;
    let rectHeight = (rectParams[3][1] - rectParams[0][1]) * proportion;
    ctx.beginPath();
    if (index % 2 === 0) {
      ctx.setLineDash([5, 5]);
    } else {
      ctx.setLineDash([]);
    }
    ctx.lineWidth = 1;
    ctx.strokeStyle = '#84B0F0';
    ctx.rect(x, y, rectWidth, rectHeight);
    ctx.stroke();
    ctx.closePath();
  }, []);

  const drawTitleNumber = useCallback((ctx, index, rectParams, proportion) => {
    drawRectangle(ctx, index, rectParams, proportion);
    drawTriangles(ctx, rectParams, proportion);
  }, []);

  useEffect(() => {
    if (!backgroundImg) {
      return;
    }
    const backgroundImgClone = backgroundImg.cloneNode(false); // 返回數據中的位置是按照圖片原像素,頁面圖片是處理過的,故須要得出比例
    let proportion = width / backgroundImgClone.width;
    const ctx = canvasTarget.current.getContext('2d');
    ctx.clearRect(0, 0, width, height);
    ctx.drawImage(backgroundImg, 0, 0, width, backgroundImgClone.height * proportion);
    if (!dataList || !Array.isArray(dataList)) {
      return;
    }
    if (dataList.length > 0) {
      for (let i = 0; i < dataList.length; i++) {
        let { itemPos } = dataList[i];
        drawRectangularBox(ctx, i, itemPos, proportion);
        drawTitleNumber(ctx, i, itemPos[0], proportion);
      }
    }
  }, [
    backgroundImg,
    height,
    width,
    dataList,
    drawRectangularBox,
    drawTitleNumber]);
  return (
    <canvas
      width={width}
      height={(backgroundImg.height * (width / backgroundImg.width)).toFixed(2)}
      ref={canvasTarget}
    />
  );
};

export default CanvasDrawer;

複製代碼

實現效果:

相關文章
相關標籤/搜索