JS中的touch事件与canvas绘图

Touch事件分类

  1. touchstart:当手指触摸屏幕时触发。不管有多少个手指放在了屏幕上,只要再触摸一下屏幕就会触发
  2. touchmove:当手指在屏幕上滑动的时候触发该是事件,在这期间可以通过event.preventDefault()来阻止滚动
  3. touchend:手指从屏幕中移开的时候触发
  4. touchcancel:当系统停止跟踪触摸时触发(例如:创建了太多的触控点)

例如

1
this.canvas.addEventListener("touchstart", this._touchStart);

Touch事件相关对象属性

Event对象属性

属性均为只读。

Property Type Description
target EventTarget 事件目标 (DOM 树最上方的目标).
type DOMString 事件类型
bubbles Boolean 事件是否正常冒泡
cancelable Boolean 事件是否可以取消
view WindowProxy document.defaultView (window of the document)
detail long (float) 常量 0
touches TouchList Touch 列表,由触摸平面当前的接触点组成
targetTouches TouchList Touch 列表,是包含了如下触点的 Touch 对象:触摸起始于当前事件的目标 element 上,并且仍然没有离开触摸平面的触点。
changedTouches TouchList Touch 列表,由从触摸平面移除了的接触点组成
ctrlKey boolean 如果事件发生时按下了 ctrl 键则为 true,否则为 false
shiftKey boolean 如果事件发生时按下了 shift 键则为 true,否则为 false
altKey boolean 如果事件发生时按下了 alt 键则为 true,否则为 false
metaKey boolean 如果事件发生时按下了 meta 键则为 true,否则为 false

其中有三个相似的属性touchestargetToucheschangedTouches,它们有什么不同呢?

  1. touches:表示屏幕上触摸操作的touch对象的属性;
  2. targetTouches:表示对应DOM上触摸操作的Touch对象的数组。
  3. changeTouches:表示从上一次触摸以来,发生了改变的touch对象的数组。

通过一个例子来区分一下触摸事件中的这三个属性:

  • 用一个手指接触屏幕,触发事件,此时这三个属性有相同的值。

  • 用第二个手指接触屏幕,此时,touches有两个元素,每个手指触摸点为一个值。

    当两个手指触摸相同元素时,targetTouchestouches的值相同,否则targetTouches 只有一个值。

    changedTouches此时只有一个值,为第二个手指的触摸点。

  • 用两个手指同时接触屏幕,此时changedTouches有两个值,每一个手指的触摸点都有一个值

  • 手指滑动时,三个值都会发生变化

  • 一个手指离开屏幕,touchestargetTouches中对应的元素会同时移除,

    changedTouches仍然会存在元素。

  • 手指都离开屏幕之后,touchestargetTouches中将不会再有值,

    changedTouches还会有一个值,此值为最后一个离开屏幕的手指的接触点。

Touch对象属性

所有属性均为只读属性。

Touch.identifier

此 Touch 对象的唯一标识符. 一次触摸动作(我们指的是手指的触摸)在平面上移动的整个过程中, 该标识符不变. 可以根据它来判断跟踪的是否是同一次触摸过程.

Touch.screenX

触点相对于屏幕左边沿的的X坐标.

Touch.screenY

触点相对于屏幕上边沿的的Y坐标.

Touch.clientX

触点相对于可见视区(visual viewport)左边沿的的X坐标. 不包括任何滚动偏移.

Touch.clientY

触点相对于可见视区(visual viewport)上边沿的的Y坐标. 不包括任何滚动偏移.

Touch.pageX

触点相对于HTML文档左边沿的的X坐标. 当存在水平滚动的偏移时, 这个值包含了水平滚动的偏移.

Touch.pageY

触点相对于HTML文档上边沿的的Y坐标. 当存在垂直滚动的偏移时, 这个值包含了垂直滚动的偏移.

Touch.radiusX

能够包围用户和触摸平面的接触面的最小椭圆的水平轴(X轴)半径. 这个值的单位和screenX 相同.

Touch.radiusY

能够包围用户和触摸平面的接触面的最小椭圆的垂直轴(Y轴)半径. 这个值的单位和screenY 相同.

Touch.rotationAngle

它是这样一个角度值:由radiusXradiusY 描述的正方向的椭圆,需要通过顺时针旋转这个角度值,才能最精确地覆盖住用户和触摸平面的接触面.

Touch.force

手指挤压触摸平面的压力大小, 从0.0(没有压力)到1.0(最大压力)的浮点数.

Touch.target

当这个触点最开始被跟踪时(在 touchstart 事件中), 触点位于的HTML元素. 哪怕在触点移动过程中, 触点的位置已经离开了这个元素的有效交互区域, 或者这个元素已经被从文档中移除. 需要注意的是, 如果这个元素在触摸过程中被移除, 这个事件仍然会指向它, 但是不会再冒泡这个事件到 windowdocument 对象. 因此, 如果有元素在触摸过程中可能被移除, 最佳实践是将触摸事件的监听器绑定到这个元素本身, 防止元素被移除后, 无法再从它的上一级元素上侦测到从该元素冒泡的事件.

接触点

  • screenX是相对于屏幕左上角的坐标

  • clientX是相对于窗口可视区的左上角坐标

  • pageX是相对于窗口内页面的左上角的坐标(常用)

所以相对于绘制区域的坐标可以这样取到

1
2
3
4
{
x: event.pageX - this.canvas.offsetLeft,
y: event.pageY - this.canvas.offsetTop
}

接触面

Touch.radiusX, Touch.radiusY, 和 Touch.rotationAngle 描述了用户和触摸平面的接触面. 这在面向非精确触摸设备(由手指直接操作的触摸屏)开发时非常有用. 这些值描述了一个尽可能接近实际接触面(例如用户的指尖)的椭圆.

MouseEvent属性

属性/方法 描述
clientX 触发鼠标事件时,返回鼠标指针相对于当前窗口的水平坐标
clientY 触发鼠标事件时,返回鼠标指针相对于当前窗口的垂直坐标
pageX 触发鼠标事件时,返回鼠标指针相对于文档的水平坐标
pageY 触发鼠标事件时,返回鼠标指针相对于文档的垂直坐标
screenX 触发事件时,返回鼠标指针相对于屏幕的水平坐标
screenY 触发事件时,返回鼠标指针相对于屏幕的垂直坐标
offsetX 返回鼠标指针相对于目标元素边缘位置的水平坐标
offsetY 返回鼠标指针相对于目标元素边缘位置的垂直坐标
movementX 返回鼠标指针相对于上一个mousemove事件位置的水平坐标
movementY 返回鼠标指针相对于上一个mousemove事件位置的垂直坐标
target 返回与触发鼠标事件的元素相关的元素
which 返回触发鼠标事件时按下的鼠标按钮
altKey 返回触发鼠标事件时是否按下ALT
ctrlKey 返回触发鼠标事件时是否按下CTRL
shiftKey 返回触发事件时是否按下SHIFT
metaKey 返回触发事件时是否按下META

和Touch对象相比screenXclientXpageX两者都有。

Cavas绘图

画线常用的有两种方式lineToquadraticCurveTo

quadraticCurveTo绘制的线比较圆滑,但是每次都要全图绘制,

所以我先用的方式就是在画线的过程中用lineTo,鼠标抬起或者触屏离开时重新进行全屏绘制,但是会突然一变,最后还是决定在光标移动中就不停的全部quadraticCurveTo绘制,这样也没有明显的慢,就决定用后来的这种方式了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
ZJSketchpad.prototype.drawStroke = function(stroke) {
this.context.beginPath();
this.context.lineJoin = "round";
this.context.lineCap = "round";
this.context.strokeStyle = stroke.color;
this.context.lineWidth = stroke.size;
if (stroke.type === this.typelist.pen) {
this.context.globalCompositeOperation = "source-over";
} else {
this.context.globalCompositeOperation = "destination-out";
}

let startpot = stroke.points[0];
this.context.moveTo(startpot.x, startpot.y);

if (stroke.type === this.typelist.erase) {
for (let i = 0; i < stroke.points.length; i++) {
let potthis = stroke.points[i];
this.context.lineTo(potthis.x, potthis.y);
}
} else {
for (let i = 1; i < stroke.points.length - 2; i++) {
let potthis = stroke.points[i];
let potnext = stroke.points[i + 1];
let ctrlPoint = {};
ctrlPoint.x = (potthis.x + potnext.x) / 2;
ctrlPoint.y = (potthis.y + potnext.y) / 2;
this.context.quadraticCurveTo(
potthis.x,
potthis.y,
ctrlPoint.x,
ctrlPoint.y
);
}
if (stroke.points.length >= 2) {
let lastpoint1 = stroke.points[stroke.points.length - 2];
let lastpoint2 = stroke.points[stroke.points.length - 1];
this.context.quadraticCurveTo(
lastpoint1.x,
lastpoint1.y,
lastpoint2.x,
lastpoint2.y
);
}
}

this.context.stroke();
this.context.restore();
};

触屏时是不显示光标的,所以我们没法通过光标来模拟黑板擦图标,所以只能用图片来模拟,移动时调整top和left的值来展现,但是touch事件会被图片给挡掉,最简单的方法就是按照下面最后两行来设置样式

1
2
3
4
5
6
7
8
9
10
11
12
13
.m_erase {
position: absolute;
top: 0;
left: 0;
width: 64px;
height: 64px;
margin-left: -32px;
margin-top: -32px;
z-index: 999;
display: none;
pointer-events: none;
touch-action: none;
}

绘制注意项

如果我们在视网膜屏幕上绘制图像,会发现按像素1:1绘制出来的效果会不清晰,这就要用到devicePixelRatio属性。

devicePixelRatio属性

该 Window 属性 devicePixelRatio 能够返回当前显示设备的物理像素分辨率与 CSS 像素分辨率的比率。此值也可以解释为像素大小的比率:一个 CSS 像素的大小与一个物理像素的大小的比值。简单地说,这告诉浏览器应该使用多少个屏幕的实际像素来绘制单个 CSS 像素。

devicePixelRatio属性语法

1
var scale = window.devicePixelRatio;

devicePixelRatio 属性值为一个 double。

devicePixelRatio属性示例

一个 canvas 在视网膜屏幕上可能显得太模糊。

使用 window.devicePixelRatio 以确定应该添加多少额外的像素密度以允许更清晰的图像。

HTML

1
<canvas id="canvas" style="width:200px;height:200px;"></canvas>

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var canvas = document.getElementById('canvas');

// Set actual size in memory (scaled to account for extra pixel density).
var devicePixelRatio = window.devicePixelRatio;
var dom_width = canvas.clientWidth;
var dom_height = canvas.clientHeight;
canvas.width = dom_width * devicePixelRatio;
canvas.height = dom_height * devicePixelRatio;

var ctx = canvas.getContext('2d');
// Normalize coordinate system to use css pixels.
ctx.scale(devicePixelRatio, devicePixelRatio);

ctx.fillStyle = "#bada55";
ctx.fillRect(0, 0, dom_width, dom_height);
ctx.fillStyle = "#ffffff";
ctx.font = '18px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';

var x = dom_width / 2;
var y = dom_height / 2;

var textString = "剑行者";
ctx.fillText(textString, x, y);

浏览器中查看到的DOM如下:

1
<canvas id="canvas" style="width:200px;height:200px;" width="400" height="400"></canvas>

这样上面的代码就绘制了一个背景为绿色,中间显示剑行者的图片了,并且十分的清晰。

其中有这么一个方法

1
ctx.scale(scalewidth,scaleheight);

scale() 方法缩放当前绘图,更大或更小。

如果您对绘图进行缩放,所有之后的绘图都会被缩放定位、宽高和画笔的大小都会被缩放
如果您 scale(2,2),那么绘图将定位于距离画布左上角两倍远的位置。

假设我们获取的window.devicePixelRatio为2,为了显示清晰我们把cavas的宽高也放大了两倍,但是我们通过touch拿到的坐标是相对于页面中cavas大小(和cavas内部的大小不一致)的定位,我们就需要把所有的坐标都放大两倍,就比较麻烦,我们就可以先调scale()方法,那么之后绘制的坐标都会自动放大后再绘制,相当的方便。