vue3+ts项目采用canvas模拟批注功能

news2024/9/27 6:37:11

vue3+ts项目模拟批注

一、项目需求:

       移动端:实现点击“批注”,随手指绘制出线条,线条封闭之后,视为圈记成功,进而输入评论内容——批注;

二、实现思路:

       1.“批注”按钮控制canvas画布显示,输入框回车确认代表完成此次批注,画布隐藏;
       2.获取touch的坐标,将所有获取到的(x,y)坐标存储至lineList
       3.线条是否满足闭合条件:
           ①线条只是一个点,即lineList.length为1,是不满足条件的,手指松开瞬间之后清空画布,并提示;
           ②线条没有相交的区域,没有即为不满足,清空画布,并提示;
           (注意:这里不能单纯的判断lineList中是否有坐标一致的点,线条看上去是连着的,实际上是有无数小点组成,每个小点之间是有间隙的)
在这里插入图片描述

三、重点部分

       1.segmentsIntr 函数:判断两条线段是否相交;
       2.在isClose函数中进行线条是否满足条件判断;
       3.handleReset:重置canvas与视口间距,主要解决在浏览器滚动到不同程度之后,画布中touch的位置与实际获取位置不符合,存在偏差的问题;
       4.水平与垂直方向缩放因子的获取:不同设备缩放因子不一致,避免在一个设备绘制之后,另一设备查看时存在较大偏差;

四、canvas部分的实现代码

<template>
  <div class="postil">
    <canvas ref="postil" class="canvas" id="canvas" @touchstart="drawStart" @touchmove="drawing" @touchend="drawEnd">
      你的浏览器不支持canvas,请升级浏览器.浏览器不支持
    </canvas>
  </div>
</template>

<script setup lang="ts">
import { drNotify } from '@/utils/vantHint';

// props类型
interface Props {

  // 画布宽
  canvasWidth?: number,

  // 画布宽
  canvasHeight?: number,

  //canvas 背景色
  canvasBackground: string,

  //线条颜色
  lineColor: string,

  //线条宽度
  lineWidth: number,

  //线条两端形状
  lineRound: string,

}

// 设置props默认
const props = withDefaults(defineProps<Props>(), {

  // 宽高需要默认为日志内容主体宽高

  canvasWidth: document.documentElement.clientWidth,

  canvasHeight: document.documentElement.clientHeight,

  canvasBackground: 'transparent',

  lineColor: '#4979E7',

  lineWidth: 3,

  lineRound: 'round',

})

console.log(props, 'props');

let direction = shallowRef(false); // 屏幕方向 true:横屏   false:竖屏

let el = ref<any>(null);   // canvas dom

let postil = ref(null);    // 绑定ref为postil

let ctx = reactive<any>({});  // canvas内容

let startX = shallowRef(0);  // 绘制开始pageX

let startY = shallowRef(0);  // 绘制开始pageY

let endX = shallowRef(0);  // 绘制结束pageX

let endY = shallowRef(0);  // 绘制结束pageY

const gap = shallowRef(20);  // 两点差距值

let lineList = reactive([]); // 绘制线条的点集合

// canvas 距离视口x,y
const gapCanvas = reactive({

  x: 0,

  y: 0,

  // 缩放
  scaleX: 1,

  scaleY: 1,

})

// 判断当前手机为竖屏还是横屏
const initPhoneDirection = () => {

  drawLine();
  // window.addEventListener(
  //   "onorientationchange" in window ? "oorientationchange" : "resize",
  //   () => {
  //     console.log(window.orientation, 'window.orientation');
  //     if (window.orientation === 180 || window.orientation === 0) {
  //       direction.value = false;
  //       drawLine();
  //       console.log(direction.value, '竖屏');
  //     }
  //     if (window.orientation === 90 || window.orientation === -90) {
  //       direction.value = true;
  //       console.log(direction.value, '横屏');
  //       drawLine();
  //     }
  //   },
  //   false
  // );
}

// 添加绘制线
const drawLine = () => {

  document.addEventListener("touchmove", (e) => {

    e.preventDefault()

  }, {

    passive: false,

  });

  el.value = postil.value;

  initCanvas();
}

// 初始化canvas配置
const initCanvas = () => {

  el.value.width = props.canvasWidth;

  el.value.height = props.canvasHeight;

  ctx = el.value.getContext('2d');

  setCanvas();
}

// canvas配置
const setCanvas = () => {

  ctx.fillStyle = props.canvasBackground;

  // 绘制矩形
  if (direction.value) {
    // 横屏
    // 立即对当前矩形进行fill填充
    ctx.fillRect(0, 0, props.canvasHeight, props.canvasWidth)
  } else {
    // 竖屏
    ctx.fillRect(0, 0, props.canvasWidth, props.canvasHeight)
  }

  // 设置线条颜色
  ctx.strokeStyle = props.lineColor;

  // 设置线条宽度
  ctx.lineWidth = props.lineWidth;

  // 设置线条两端形状
  ctx.lineCap = props.lineRound;

}

// 开始绘制
const drawStart = (e: any) => {

  console.log(gapCanvas.x, gapCanvas.y, '画布距离视口的距离');

  // clearSign();

  lineList = [];

  startX.value = (e.changedTouches[0].pageX - gapCanvas.x) * gapCanvas.scaleX;

  startY.value = (e.changedTouches[0].pageY - gapCanvas.y) * gapCanvas.scaleY;

  // 开始路径:核心的作用是将 不同绘制的形状进行隔离
  ctx.beginPath();

  //绘制起点 
  ctx.moveTo((e.changedTouches[0].pageX - gapCanvas.x) * gapCanvas.scaleX, (e.changedTouches[0].pageY - gapCanvas.y) * gapCanvas.scaleY)

  drawing(e);
}

// 绘制过程
const drawing = (e: any) => {

  lineList.push({ x: (e.changedTouches[0].pageX - gapCanvas.x) * gapCanvas.scaleX, y: (e.changedTouches[0].pageY - gapCanvas.y) * gapCanvas.scaleY })

  // 绘制直线:绘制一条直线至起点或者上一个线头点
  ctx.lineTo((e.changedTouches[0].pageX - gapCanvas.x) * gapCanvas.scaleX, (e.changedTouches[0].pageY - gapCanvas.y) * gapCanvas.scaleY);

  // 描边:根据路径绘制线
  ctx.stroke();
}

// 绘制结束
const drawEnd = (e: any) => {

  endX.value = (e.changedTouches[0].pageX - gapCanvas.x) * gapCanvas.scaleX;

  endY.value = (e.changedTouches[0].pageY - gapCanvas.y) * gapCanvas.scaleY;

  // 闭合路径:自动把最后的线头和开始的线头连在一起
  ctx.closePath();

  isClose();
}

// 重置
const clearSign = () => {

  initCanvas();
}

interface Emits {
  (event: 'drawPie', bool: any, array?: any): void
}

const $emits = defineEmits<Emits>();

// 提交
const saveSign = () => {

  // toDataURL:把canvas绘制的内容输出成base64内容
  const imageBase64 = el.value.toDataURL();
  
  // 调用父组件写批注事件
  $emits('drawPie', true, imageBase64)

}

// 判断两条线段是否相交
const segmentsIntr = (a: any, b: any, c: any, d: any) => {

  // 三角形abc 面积的2倍  
  var area_abc = (a.x - c.x) * (b.y - c.y) - (a.y - c.y) * (b.x - c.x);

  // 三角形abd 面积的2倍  
  var area_abd = (a.x - d.x) * (b.y - d.y) - (a.y - d.y) * (b.x - d.x);

  // 面积符号相同则两点在线段同侧,不相交 (对点在线段上的情况,本例当作不相交处理);  
  if (area_abc * area_abd >= 0) {

    return false;

  }

  // 三角形cda 面积的2倍  
  var area_cda = (c.x - a.x) * (d.y - a.y) - (c.y - a.y) * (d.x - a.x);

  // 三角形cdb 面积的2倍  
  // 注意: 这里有一个小优化.不需要再用公式计算面积,而是通过已知的三个面积加减得出.  
  var area_cdb = area_cda + area_abc - area_abd;

  if (area_cda * area_cdb >= 0) {

    return false;

  }

  //计算交点坐标  
  var t = area_cda / (area_abd - area_abc);

  var dx = t * (b.x - a.x),

    dy = t * (b.y - a.y);

  return { x: a.x + dx, y: a.y + dy };

}

/** 判断绘制的是否满足封闭条件:
  1.开始与结束点距离小于等于20px  
  2.绘制的点集合大于1(不止一个点)
  3.线条相交,形成一个闭合区间
*/
const isClose = () => {
  // ctx.save() 保存当前环境的状态 可以把当前绘制环境进行保存到缓存中。

  const addX = startX.value + gap.value;
  const loseX = startX.value - gap.value;
  const addY = startY.value + gap.value;
  const loseY = startY.value - gap.value;

  const satifyX = (endX.value < addX || endX.value == addX) && (endX.value > loseX || endX.value == loseX);
  const satifyY = (endY.value < addY || endY.value == addY) && (endY.value > loseY || endY.value == loseY);

  let count = 0;

  // 线条的交点
  for (var i = 0; i < lineList.length - 1; i++) {

    for (var j = i + 1; j < lineList.length - 1; j++) {

      var crossoverPoint = segmentsIntr(lineList[i], lineList[i + 1], lineList[j], lineList[j + 1])

      if (crossoverPoint != false) {
        // 有交点
        count++;

      }
    }

  }

  if (((satifyX && satifyY) && lineList.length > 1) || count) {
    console.log('符合条件');
    saveSign();

  } else {
    drNotify('请将线条首尾相连!', 'danger')
    console.log('不符合条件');
    clearSign();

  }

}

// 重置canvas与视口间距
const handleReset = () => {

  // DOM元素到浏览器可视范围的距离
  const rect = el.value.getBoundingClientRect();

  gapCanvas.x = rect.left;

  gapCanvas.y = rect.top;

}

// 监听滚动条
const handleScroll = () => {

  window.addEventListener('scroll', async () => {

    await handleReset();

  }, true)

}

onMounted(() => {

  initPhoneDirection();

  handleScroll();

  handleReset();

  let style = window.getComputedStyle(el.value, null);

  let cssWidth = parseFloat(style["width"]);

  let cssHeight = parseFloat(style["height"]);

  gapCanvas.scaleX = el.value.width / cssWidth; // 水平方向的缩放因子

  gapCanvas.scaleY = el.value.height / cssHeight; // 垂直方向的缩放因子

  console.log(gapCanvas.scaleX, gapCanvas.scaleY, '缩放');

})

onBeforeUnmount(() => {

  window.removeEventListener('scroll', () => { })
})

</script>


<style lang="scss">
.postil {
  width: 100%;
  height: 100%;
  background-color: rgba(250, 235, 215, 0.382);

  .canvas {
    width: 100%;
    height: 100%;
    display: block;
  }
}
</style>

五、思考

1. 可以实现只存canvas中的路径吗?或是存储绘制出来的线?
    理想效果:
           回显的时候,通过点击不同的线条能够切换展示不同的评论
    现状:
           canvas转为base64存储的,存储的为整个画布,多条线段展示即是多个画布大小的图片叠加,并不能准确获取到对应线条

暂时没有处理思路,欢迎提供解决办法!!!!!!!感谢

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/349695.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

关于cFosSpeed如何配置

cFosSpeed配置一、检查Calibration Done情况二、优化Ping时间和线路校准三、测网速四、cFosSpeed控制台五、配置参数一、检查Calibration Done情况 安装完毕&#xff0c;激活成功后。 右键------>选项------>设置&#xff0c; 打开适配器信息&#xff0c;查看Calibra…

leaflet: 一个marker的世界旅行动画(077)

第077个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中动态的设置marker,这里起个美丽的名字就叫做一个marker的世界旅行。 直接复制下面的 vue+leaflet源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源代码(共76行)相关API参考:专栏目…

内核经典数据结构list 剖析

前言&#xff1a;linux内核中有很多经典的数据结构&#xff0c;list(也称list_head)为其中之一&#xff0c;这些数据结构都是使用C语言实&#xff0c;并且定义和实现都在单独的头文件list.h中。可以随时拿出来使用。list.h的定义不同linux发行版本路径不同,我们可以在/usr/incl…

《python3网络爬虫开发实战 第二版》之基本库的使用-urllib的使用 详解

文章目录1 urllib 库的使用1.1 request模块1.1.1 urlopen类1.1.1.1 最简单的爬虫-爬取百度首页1.1.1.2 urlopen方法的参数1.1.1.2.1 data参数1.1.1.2.2 timeout参数1.1.1.2.3 其他参数1.1.2 Request 类1.1.3 Handler1.2 error模块1.2.1 URLError 类1.2.2 HTTPError类1.2.3 比较…

分布式-分布式服务

微服务API 网关 网关的概念来源于计算机网络&#xff0c;表示不同网络之间的关口。在系统设计中&#xff0c;网关也是一个重要的角色&#xff0c;其中最典型的是各大公司的开放平台&#xff0c;开放平台类网关是企业内部系统对外的统一入口&#xff0c;承担了很多业务&#xf…

【Java】虚拟机JVM

一、运行时数据区域 程序计数器 记录正在执行的虚拟机字节码指令的地址&#xff08;如果正在执行的是本地方法则为空&#xff09; Java虚拟机栈 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程&am…

Mysql数据库的时间(3)一如何用函数插入时间

暂时用下面四个日期函数插入时间 如:insert into Stu(time) values (now()); Mysql的时间函数描述对应的Mysql的时间类型now()/sysdate()NOW()函数以YYYY-MM-DD HH:MM:SS返回当前的日期时间date/time/dateTime/timeStamp/yearcurDate()/current_date()返回当前的日期YYYY-M…

计算机网络笔记(三)—— 数据链路层

数据链路层概述 数据链路层以帧为单位传输数据。 封装成帧&#xff1a;给网络层提供的协议数据单元添加帧头帧尾 差错检测&#xff1a;检错码封装在帧尾 可靠传输&#xff1a;尽管误码不能避免&#xff0c;但如果可以实现发送什么就接受什么&#xff0c;就叫可靠传输 封装成…

RTOS随笔之FreeRTOS启动与同步方法

RTOS启动与同步机制RTOS启动任务切换场景任务同步机制队列信号量事件组任务通知任务延时RTOS启动 FreeRTOS在任务创建完成后调用函数vTaskStartScheduler()启动任务调度器。 vTaskStartScheduler()任务启动函数详解 void vTaskStartScheduler( void ) {BaseType_t xReturn;xR…

项目管理工具dhtmlxGantt甘特图入门教程(九):支持哪些数据格式(下篇)

这篇文章给大家讲解 dhtmlxGantt可以加载或支持哪些数据格式。 dhtmlxGantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表&#xff0c;可满足应用程序的所有需求&#xff0c;是最完善的甘特图图表库 DhtmlxGantt正版试用下载&#xff08;qun&#xff1a;764148812&am…

虚拟机NAT模式无法连外网

虚拟机使用NAT模式连接外网设置时要注意两点 虚拟机的网段要与物理机保持一致各个节点的IP中的GATEWAY要与虚拟机的网关保持一致 1、虚拟机的网段要与物理机保持一致 1.1首先查看物理机的ip&#xff0c;看虚拟机和物理机ip是否在同一网段 winR cmd计入控制台&#xff0c;然…

uni-app前端H5页面底部内容被tabbar遮挡

如果你想在原生 tabbar 上方悬浮一个菜单&#xff0c;之前写 bottom:0。这样的写法编译到 h5 后&#xff0c;这个菜单会和 tabbar 重叠&#xff0c;位于屏幕底部。 原码&#xff1a; <view style"position: fixed;bottom:0;left: 0;background-color: #007AFF;right: …

PythonWeb Django PostgreSQL创建Web项目(二)

安装数据库PostgreSQL并创建数据库 我第一次尝试使用PostgreSQL数据库&#xff0c;why&#xff1f;我喜欢它提供的丰富的数据类型&#xff0c;例如货币类型、枚举类型、几何类型(点、直线、线段、矩形等等)、网络地址类型、文本搜索类型、XML类型JSON类型等等&#xff0c;非常…

Web3中文|聊聊这个让Opensea头疼的新对手Blur ($BLUR)

2022年10月19日&#xff0c;NFT市场迎来一个新的平台。 这个被精心设计的NFT交易市场和聚合器被命名为Blur。 与其他NFT平台不同&#xff0c;Blur旨在提升专业交易者的NFT交易体验。它的开发团队认为其他交易平台存在界面混杂、无法获取分析数据和工具、处理速度缓慢等问题。…

postman使用简介

1、介绍 postman是一款功能强大的网页调试和模拟发送HTTP请求的Chrome插件&#xff0c;支持几乎所有类型的HTTP请求 2、下载及安装 官方文档&#xff1a;https://www.getpostman.com/docs/v6/ chrome插件&#xff1a;chrome浏览器应用商店直接搜索添加即可&#xff08;需墙&…

在魔改PLUS-F5280开发板上使用合封qsp iflash

文章目录引言硬件调整软件调整总结引言 由于目前灵动官网暂未发布正式版的PLUS-F5280开发板&#xff0c;可以使用现有的PLUS-F5270 v1.2开发板&#xff08;下文简称PLUS-F5270开发版&#xff09;替换为MM32F5280微控制器芯片&#xff0c;改装为PLUS-F5280开发板。本文记录了使…

【Mybatis源码解析】mapper实例化及执行流程源码分析

文章目录简介环境搭建源码解析基础环境&#xff1a;JDK17、SpringBoot3.0、mysql5.7 储备知识&#xff1a;《【Spring6源码・AOP】AOP源码解析》、《JDBC详细全解》 简介 基于SpringBoot的Mybatis源码解析&#xff1a; 1.如何对mapper实例化bean 在加载BeanDefinition时&a…

哈希表题目:矩阵置零

文章目录题目标题和出处难度题目描述要求示例数据范围进阶解法一思路和算法代码复杂度分析解法二思路和算法代码复杂度分析解法三思路和算法代码复杂度分析题目 标题和出处 标题&#xff1a;矩阵置零 出处&#xff1a;73. 矩阵置零 难度 3 级 题目描述 要求 给定一个 m…

oracle单库重建undo表空间步骤

前言&#xff1a;undo表空间不足可直接增加空间&#xff1b; alter tablespace UNDOTBS1 add datafile /data/oradata/datafile/UNDOTBS102.dbf size 30g autoextend off; 注&#xff1a;单次增加最大不得超过32G 回收空间 缩小表空间直接可以resize命令缩小&#xff1b…

webpack(高级)--创建自己的loader 同步loader 异步loader loader参数校验

webpack 创建自己的loader loader是用于对模块的源代码进行转换&#xff08;处理&#xff09; 我们使用过很多loader 比如css-loader style-loader babel-loader 我么如果想要自己创建一个loader 首先创建webpack环境 pnpm add webpack webpack-cli -D 之后创建loader模块…