工作上需要实现电子签名组件,目前项目的技术栈是 vue3 + element plus,实现的功能有 画笔、橡皮、清空。
用到了鼠标事件、触摸事件,在这里先回顾一下相关的知识点。
鼠标事件相关
鼠标事件
鼠标事件 | 触发条件 |
---|---|
mouseover | 当鼠标经过被选元素和被选元素的子元素时都会触发mouseover事件,对应mouseout事件 |
mouseout | 无论鼠标离开被选元素还是被选元素的子元素都会触发 |
mousemove | 当鼠标移入被选元素内后,任意移动一个像素点都会触发 |
mouseup | 无论鼠标离开被选元素还是被选元素的子元素都会触发 |
mousedown | 鼠标按下触发 |
mouseenter | 不会冒泡,当鼠标经过被选元素才会触发,经过子盒子不触发,对应mouseleave事件 |
mouseleave | 不会冒泡,鼠标离开元素上会触发,离开子盒子不触发 |
事件属性
获取某个dom节点,对节点添加监听事件时,一般用的最多的就是 event
或者 event.target
下相关的属性。
const canvasDom = document.getElementById('canvas');
canvasDom.onmousemove = function (e) {
console.log(e) // event 事件
console.log(e.target) // 触发事件的对象 (某个DOM元素) 的引用
}
event
鼠标本身的位置相关的属性
-
pageX/Y
触发点相对文档区域左上角距离,会随着页面滚动而变化
兼容性:除IE6/7/8不支持外,其余浏览器均支持
-
clientX/Y
触发点相对浏览器可视区域左上角距离,不随页面滚动而变化
兼容性:所有浏览器均支持
-
screenX/Y
触发点相对显示器屏幕左上角的距离,不随页面滚动而改动
兼容性:所有浏览器均支持
-
offsetX/Y
触发点相对被触发dom的左上角距离,不过左上角基准点在不同浏览器中有区别,其中在IE中以内容区左上角为基准点不包括边框,如果触发点在边框上会返回负值,而chrome中以边框左上角为基准点。
兼容性:IE所有版本,chrome,Safari均完美支持,Firefox不支持
-
layerX/Y
触发点相对被触发dom左上角的距离,数值与offsetX/Y相同,这个变量就是firefox用来替代offsetX/Y的,基准点为边框左上角,但是有个条件就是,被触发的dom需要设置为position:relative或者position:absolute,否则会返回相对html文档区域左上角的距离。
兼容性:IE6/7/8不支持,opera不支持,IE9/10和Chrome、Safari均支持
event.target
触发事件的对象 (某个DOM元素) 的引用
-
offsetHeight/offsetWidth:元素自身可见高度 + padding + border + 滚动条(17),不包含margin,不包括溢出不可见部分的高度。
-
clientHeight/clientWidth:元素自身可见高度 + padding,不包含border与margin,不包括溢出不可见部分的高度。
-
scrollHeight/scrollWidth:只读属性是一个元素内容高度的度量,包括由于溢出导致的视图中不可见内容,包含width和padding,不包含border与margin
-
scrollTop/scrollLeft:获取元素滚动后的距离文档顶部的距离,也就是滚动条滚动的距离。
-
clientTop:获取元素边框的厚度,也就是border的宽度。 有个公式:target.scrollTop + target.offsetHeight === target.scrollHeight用于判断滚动条是否滚动到底。
touch事件
事件类型
- touchstart:触摸开始的时候触发
- touchmove : 手指在屏幕上滑动的时候触发
- touchend: 触摸结束的时候触发
- touchcancel:触摸时由于某些原因被中断时触发
事件属性
当触发事件的时候会生成一个event对象,以下是对象的属性列表
- touches:屏幕上所有触摸点的信息
- targetTouches:目标区域上所有触摸点的信息
- changedTouches:当前事件触摸点的信息
区别:
- 第一根手指触摸屏幕,三个事件获取到的信息是相同的。
- 第二根手指触摸屏幕,touches会获取两个触摸点的信息,如果触摸点是在同一个目标区域上的targetTouches也是获取到两个触摸点信息,而chengedTouched只保存第二个信息点。
- 若同时两根手指触摸屏幕,changedTouches会保存两个触摸点信息。
- 第一个触摸点和最后一个触摸点离开的时候,只有changedTouches才会保存离开的触摸点信息。
属性参数
// 获取event.changedTouches 输出
{
clientX: 603.6799926757812 // 返回触摸点相对于浏览器视口左边缘的X坐标,不包括任何滚动偏移。
clientY: 932.9600219726562 // 返回触摸点相对于浏览器视口上边缘的Y坐标,不包括任何滚动偏移。
force: 1 // 压力大小,是从 0.0(无压力)到 1.0(最大压力)的浮点数
identifier: 0 // 一次触摸动作的唯一标识符
pageX: 603.6799926757812 // 返回触摸点相对于文档左边缘的X坐标。与clientX此不同,此值包括水平滚动偏移
pageY: 932.9600219726562 // 返回触摸点相对于文档顶部的Y坐标。与clientY,此值不同,包括垂直滚动偏移
radiusX: 30.053333282470703 // 返回椭圆的X半径,该半径最接近地限定与屏幕的接触区域。该值的大小与像素相同screenX。
radiusY: 30.053333282470703 //返回椭圆的Y半径,该半径最接近地限定与屏幕的接触区域。该值的大小与像素相同screenY。
rotationAngle: 0 // 它是这样一个角度值:由radiusX 和 radiusY 描述的正方向的椭圆,需要通过顺时针旋转这个角度值,才能最精确地覆盖住用户和触摸平面的接触面
screenX: 447 // 返回触摸点相对于屏幕左边缘的X坐标。
screenY: 527 // 返回触摸点相对于屏幕上边缘的Y坐标。
target: body // 此次触摸事件的目标element
}
代码
<template>
<div class="canvas-dom">
<header>
<el-radio-group v-model="props.tool" class="mr-4">
<el-radio label="paint">画笔</el-radio>
<el-radio label="eraser">橡皮</el-radio>
</el-radio-group>
<el-button
type="danger"
round
class="mr-4"
@click="clearSign"
>
清空签名
</el-button>
<div v-show="(props.tool === 'eraser')" class="eraser-option">
<label>形状:</label>
<el-select v-model="shape" class="mr-4">
<el-option
v-for="item in ['rect', 'circle']"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<label>大小(px):</label>
<el-input-number v-model="size" :min="1" />
</div>
</header>
<canvas
ref="canvas"
height="200"
width="500"
@mousedown="onEventStart"
@mousemove.stop.prevent="onEventMove"
@mouseup="onEventEnd"
@touchstart="onEventStart"
@touchmove.stop.prevent="onEventMove"
@touchend="onEventEnd"
>
</canvas>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, computed, ref } from 'vue'
import { type VComponent } from './componentList'
import { useFormStore } from '@/store'
const props = defineProps<{
tool: string
}>()
// 橡皮的形状
const shape = ref<'rect'| 'circle'>('rect')
// 橡皮的大小
const size = ref(10)
const canvas = ref()
let ctx: CanvasRenderingContext2D
// 正在绘制中,用来控制 move 和 end 事件
let painting = false
// 获取触发点相对被触发dom的左、上角距离
const _getOffset = (event: MouseEvent|TouchEvent) => {
let offset: [number, number]
if ((event as MouseEvent).offsetX) {
// pc端
const { offsetX, offsetY } = event as MouseEvent
offset = [offsetX, offsetY]
} else {
// 移动端
const { top, left } = canvas.value.getBoundingClientRect()
const offsetX = (event as TouchEvent).touches[0].clientX - left
const offsetY = (event as TouchEvent).touches[0].clientY - top
offset = [offsetX, offsetY]
}
return offset
}
// 绘制起点
let startX = 0, startY = 0
// 鼠标/触摸 按下时,保存 触发点相对被触发dom的左、上 距离
const onEventStart = (event: MouseEvent|TouchEvent) => {
[startX, startY] = _getOffset(event)
painting = true
}
const onEventMove = (event: MouseEvent|TouchEvent) => {
if (painting) {
// 鼠标/触摸 移动时,保存 移动点相对 被触发dom的左、上 距离
const [endX, endY] = _getOffset(event)
if (props.value.tool === 'paint') {
paint(startX, startY, endX, endY, ctx)
} else {
eraser(startX, startY, endX, endY, ctx, size.value, shape.value)
}
// 每次绘制 或 清除结束后,起点要重置为上次的终点
startX = endX
startY = endY
}
}
const onEventEnd = () => {
if (painting) {
painting = false // 停止绘制
}
}
onMounted(() => {
ctx = canvas.value.getContext('2d') as CanvasRenderingContext2D
})
const clearSign = () => {
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height)
}
// canvas 画图
function paint(startX: number, startY: number, endX: number, endY: number, ctx: CanvasRenderingContext2D) {
ctx.beginPath()
ctx.globalAlpha = 1
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
ctx.moveTo(startX, startY)
ctx.lineTo(endX, endY)
ctx.closePath()
ctx.stroke()
}
// 橡皮
function eraser(
startX: number,
startY: number,
endX: number,
endY: number,
ctx: CanvasRenderingContext2D,
size: number,
shape: 'rect'| 'circle',
) {
ctx.beginPath()
ctx.globalAlpha = 1
switch (shape) {
case 'rect':
ctx.lineWidth = size
ctx.strokeStyle = '#fff'
ctx.moveTo(startX, startY)
ctx.lineTo(endX, endY)
ctx.closePath()
ctx.stroke()
break
case 'circle':
ctx.fillStyle = '#fff'
ctx.arc(startX, startY, size, 0, 2 * Math.PI)
ctx.fill()
break
}
}
</script>
<style scoped lang="scss">
.canvas-dom {
width: 100%;
canvas {
border: 1px solid #e6e6e6;
}
header {
width: 100%;
margin: 8px;
display: flex;
flex-flow: row nowrap;
align-items: center;
.eraser-option {
display: flex;
label {
white-space: nowrap;
}
}
}
}
</style>