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存储的,存储的为整个画布,多条线段展示即是多个画布大小的图片叠加,并不能准确获取到对应线条
暂时没有处理思路,欢迎提供解决办法!!!!!!!感谢