阅读 111

Canvas还能这么玩?(Canvas实现图片标注)

我的Canvas入门记录,利用Canvas实现图片标注

title.png

大致需求

服务端返回图片地址,前端需要加载图片并在其上进行标注区域,然后把点位数组返回给后端。 一图流效果如下: 20220309_142319.gif

实现过程

绘制底图

首先需要把后端返回的图片地址,渲染在Canvas当中(这里有2个Canvas标签,一个是绘制底图, 一个是绘制标注图层,需要保证2个Canvas宽高一致,利用CSS使标注图层覆盖在底图之上), 利用React useEffect监听props里的src改变,从而渲染底图。由于不知道图片的大小,为了图片渲染出来比例一样,所以需要根据Canvas宽高与图片宽高来计算缩放比例(用useRef进行了存储,方便后面计算坐标点位)。

 useEffect(() => {         if(!src) return;         const ctx = baseCanvas.current.getContext("2d");         setLoading(true);         //图片加载完后,将其绘制在canvas中         const img = new Image();         img.src = src;         img.onload = function () {             ctx.clearRect(0, 0, width, height); // 清除底图             let _width = img.width, _height = img.height;             imgInfo.current = {                 width: _width,                 height: _height             } // 存储图片宽高 防止计算导致的误差             if (img.width > width || img.height > height) {                 scale.current = img.width > img.height ? img.width / width : img.height / height;                 _width = img.width / scale.current;                 _height = img.height / scale.current;             } // 等比例缩放             baseCanvas.current.width = topCanvas.current.width = _width;             baseCanvas.current.height = topCanvas.current.height = _height;             ctx.drawImage(this, 0, 0, _width, _height);             setLoading(false);             handleClear(); // 清除标注         }         img.onerror = () => {             warn_barry('图片加载失败');             setLoading(false);         }     }, [src]); 复制代码

好了,到这一步,已经差不多完成了对吧。可以摸2天鱼,剩下的周五再说,手动狗头。

QQ图片20220310111841.gif

描点与连线

定义一个point数组,用来存储每次新增标注的点位信息。 在Canvas上绑定点击事件,点击之后在标注图层进行描点,将该点与point数组里的最后一个点位信息进行连线,再把信息存入point数组。

let points: Array<point> = []; const clickFn = (e: any) => {                 if (brush !== 'add') return;                 clearTimeout(timer);                 timer = setTimeout(() => {                     let x = e.offsetX, y = e.offsetY;                     const index = points.findIndex(item => Math.abs(equalScale(item[0], false) - x) < 10 && Math.abs(equalScale(item[1], false) - y) < 10);                     if (index > 0) { console.error('请连接起点'); return; }                     if (index === 0) { x = equalScale(points[0][0], false); y = equalScale(points[0][1], false); }                     const ctx = topCanvas.current.getContext("2d");                     ctx.fillStyle = "rgba(24, 144, 255, 1)";                     ctx.fillRect(x - 3, y - 3, 6, 6);                     let prevPoint;                     if (prevPoint = points[points.length - 1]) {                         ctx.strokeStyle = "rgba(24, 144, 255, 1)";                         ctx.beginPath();                         ctx.moveTo(equalScale(prevPoint[0], false), equalScale(prevPoint[1], false));                         ctx.lineTo(x, y);                         ctx.stroke();                         ctx.closePath();                     }                     points.push([equalScale(x), equalScale(y)]);                 }, 200);             } // 单击绘制坐标点及连线  topCanvas.current.addEventListener('click', clickFn); // 给标注图层绑定点击事件 复制代码

双击退出新增,绘制标注区域

上面的范围绘制出来之后,提供保存操作(保存之后不允许再次进行描点与连线,只能拖拽节点,改变形状),进行区域绘制。

// 绘制标注方法  const draw = () => {         const ctx = topCanvas.current.getContext("2d");         ctx.clearRect(0, 0, width, height);         markArr.current.forEach(mark => { // 遍历标注数组,绘制所有的标注区域             const { points } = mark;             ctx.beginPath();             ctx.fillStyle = "rgba(24, 144, 255, 1)";             points.forEach((item, index) => {                 const x = equalScale(item[0], false), y = equalScale(item[1], false);                 ctx.fillRect(x - 3, y - 3, 6, 6);                 index === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);             });             ctx.stroke();             ctx.fillStyle = 'rgba(24, 144, 255, .5)';             ctx.fill();             ctx.closePath();         });     }  const dbClickFn = () => {                 clearTimeout(timer);                 if(points.length < 3) {                     setBrush('');                      return; // 构不成平面则不保存                 }                 if (JSON.stringify(points[0]) !== JSON.stringify(points[points.length - 1])) points.push(points[0]); // 默认连接起点                 markArr.current.push({                     key: new Date().getTime(),                     points                 }); // 把临时点位数组存起来                 handleChange(); // 触发props传入的onChange事件                 draw(); // 绘制标注区域                 setBrush(''); // 退出新增状态             } // 双击保存当前标注 复制代码

节点拖拽改变标注形状

上面的操作已经成功把标注区域的范围绘制了出来,下面需要进行微调区域,需要实现拖拽节点进行重新绘制。 定义了一个current对象,用来存储当前移动点位。

  • 监听鼠标移动事件,改变current里的点位信息,重新绘制点位以及区域

  • 监听鼠标按下事件,判断当前位置是否与标注点位进行重合,如果重合把该点位信息存入current

  • 监听鼠标抬起事件,清空current对象,触发props里的onChange事件,把最终的点位信息传递出去

            let current: null | point | undefined = null;             const moveFn = throttle((e: any) => {                 topCanvas.current.style.cursor = 'default';                 const x = e.offsetX, y = e.offsetY;                 if (current) {                     current[0] = equalScale(x);                     current[1] = equalScale(y);                     draw();                 }             }, 10);             const mousedownFn = (e: any) => {                 const x = equalScale(e.offsetX), y = equalScale(e.offsetY);                 for (let i = 0; i < markArr.current.length; i++) {                     const { points } = markArr.current[i];                     current = points.find(item => Math.abs(item[0] - x) < 20 && Math.abs(item[1] - y) < 20);                     if(current) break;                 }             }             const mouseupFn = () => {                 current = null;                 handleChange();             }             topCanvas.current.addEventListener('mousemove', moveFn);             topCanvas.current.addEventListener('mousedown', mousedownFn);             topCanvas.current.addEventListener('mouseup', mouseupFn); 复制代码

到这里,已经差不多完成了。

QQ图片20220310112125.png

json数据双向绑定

现在大致需求已经实现了,但客户又提出了需要可视化json数据,并且能进行更改。

微信图片_20220310111627.jpg

大致意思就是用户不想描点,通过直接改变点位信息来改变形状(很迷惑)。所以就有了右边这一串的json数据,用户可以更改json里面的点位信息来达到绘制区域的效果。由于我把标注组件封装成了一个类似antd Input的效果,它是没有自己的状态的,数据全通过props里的value,onChange来进行更改,唯一要做的就是缓存Canvas元素,避免不必要的渲染,每次新增标注完成和拖拽完成再触发props里的onChange,切忌在拖拽过程中去调用onChange,你可能会发现页面很卡。

QQ图片20220310105519.png


作者:hahayq
链接:https://juejin.cn/post/7073308299843600421


文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐