大背景:试卷拆录图片识别功能,须要根据后端返回的数据实现识别框:前端
识别框:react
1)显示在图片中,奇数框为实线、偶数框为虚线。识别框不可调整大小及位置;canvas
2)框的左侧显示当前题的编号,编号从1开始,按照从上至下,从左至右的顺序递增;后端
这是UI设计的题目编号,以下图:数组
拿到需求以后细化一下功能点,开始搞起。bash
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]],
}
]
复制代码
<canvas
width={width}
height={(backgroundImg.height * (width / backgroundImg.width)).toFixed(2)}
ref={canvasTarget}
/>
复制代码
说明一下:由于父组件的宽度是定死的,因此proportion是直接用父组件div的宽度/图片的原始widthui
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);
}
}
}
复制代码
const ctx = canvasTarget.current.getContext('2d');
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = '#84B0F0';
ctx.rect(x, y, width, height);
ctx.stroke();
ctx.closePath();
复制代码
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();
复制代码
由于画图的时候考虑到按照比例实现,故采用第一种方式画矩形框;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();
}, []);
复制代码
这部分我是拆开实现的,一共是3个功能点:
const drawTitleNumber = useCallback((ctx, index, rectParams, proportion) => {
drawRectangle(ctx, index, rectParams, proportion);
drawTriangles(ctx, rectParams, proportion);
}, []);
复制代码
须要注意的是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();
}, []);
复制代码
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();
}, []);
复制代码
之因此这么一层一层的去写,是由于须要有一个按部就班的过程让本身再看下是否有不妥或者遗漏的地方,欢迎指正,一块儿进步。
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;
复制代码
实现效果: