大背景:試卷拆錄圖片識別功能,須要根據後端返回的數據實現識別框:前端
識別框: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;
複製代碼
實現效果: