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;

复制代码

实现效果:

相关文章
相关标签/搜索