HTML Canvas Drawing With Touch

Introduction

HTML canvas can be used for sketching. In this blog post, I would like to quickly discuss how to use JavaScript to implement a canvas with touch sketching.

Touch Canvas

The touch canvas can be used with any devices that use touch screen.

Line width : Color :

Source Code

The source code was modified based on the Mozilla touch drawing example with some bug fixes and feature enhancement.

touch_canvas.html
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
<style>
#canvas_div {
text-align: center;
display: block;
margin-left: auto;
margin-right: auto;
}
canvas {
border: 2px solid black;
}
</style>

<div id="canvas_div" style="overflow-x: auto;">
<canvas id="canvas" width="900" height="360"></canvas>
<button onclick="javascript:clearArea();return false;">Clear Area</button>
Line width : <select id="selWidth">
<option value="11">11</option>
<option value="13" selected="selected">13</option>
<option value="15">15</option>
</select>
Color : <select id="selColor">
<option value="black">black</option>
<option value="blue" selected="selected">blue</option>
<option value="red">red</option>
<option value="green">green</option>
<option value="yellow">yellow</option>
<option value="gray">gray</option>
</select>
</div>

<script>
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const viewport = window.visualViewport;
var offsetX;
var offsetY;

function startup() {
canvas.addEventListener('touchstart', handleStart);
canvas.addEventListener('touchend', handleEnd);
canvas.addEventListener('touchcancel', handleCancel);
canvas.addEventListener('touchmove', handleMove);
}

document.addEventListener("DOMContentLoaded", startup);

const ongoingTouches = [];

function handleStart(evt) {
evt.preventDefault();
const touches = evt.changedTouches;
offsetX = canvas.getBoundingClientRect().left;
offsetY = canvas.getBoundingClientRect().top;
console.log(offsetX, offsetY)
for (let i = 0; i < touches.length; i++) {
ongoingTouches.push(copyTouch(touches[i]));
}
}

function handleMove(evt) {
evt.preventDefault();
const touches = evt.changedTouches;
for (let i = 0; i < touches.length; i++) {
const color = document.getElementById('selColor').value;
const idx = ongoingTouchIndexById(touches[i].identifier);
if (idx >= 0) {
context.beginPath();
context.moveTo(ongoingTouches[idx].clientX - offsetX, ongoingTouches[idx].clientY - offsetY);
context.lineTo(touches[i].clientX - offsetX, touches[i].clientY - offsetY);
context.lineWidth = document.getElementById('selWidth').value;
context.strokeStyle = color;
context.lineJoin = "round";
context.closePath();
context.stroke();
ongoingTouches.splice(idx, 1, copyTouch(touches[i])); // swap in the new touch record
}
}
}

function handleEnd(evt) {
evt.preventDefault();
const touches = evt.changedTouches;
for (let i = 0; i < touches.length; i++) {
const color = document.getElementById('selColor').value;
let idx = ongoingTouchIndexById(touches[i].identifier);
if (idx >= 0) {
context.lineWidth = document.getElementById('selWidth').value;
context.fillStyle = color;
ongoingTouches.splice(idx, 1); // remove it; we're done
}
}
}

function handleCancel(evt) {
evt.preventDefault();
const touches = evt.changedTouches;
for (let i = 0; i < touches.length; i++) {
let idx = ongoingTouchIndexById(touches[i].identifier);
ongoingTouches.splice(idx, 1); // remove it; we're done
}
}

function copyTouch({ identifier, clientX, clientY }) {
return { identifier, clientX, clientY };
}

function ongoingTouchIndexById(idToFind) {
for (let i = 0; i < ongoingTouches.length; i++) {
const id = ongoingTouches[i].identifier;
if (id === idToFind) {
return i;
}
}
return -1; // not found
}

function clearArea() {
context.setTransform(1, 0, 0, 1, 0, 0);
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
}
</script>

Caveats

The coordinate computation in the canvas is very confusing and error-prone, especially when the HTML layout is complicated and there was scroll happened. For the best practice, we should recompute the offsets when the touch event starts.

1
2
3
4
5
6
7
8
9
10
function handleStart(evt) {
evt.preventDefault();
const touches = evt.changedTouches;
offsetX = canvas.getBoundingClientRect().left;
offsetY = canvas.getBoundingClientRect().top;
console.log(offsetX, offsetY)
for (let i = 0; i < touches.length; i++) {
ongoingTouches.push(copyTouch(touches[i]));
}
}

Notice that the offsets that getBoundingClientRect returns is the coordinates relative to the viewport. When we try to obtain the coordinates of the touch event, we should use clientX and clientY which are also relative to the viewport, so that the touch event coordinates and canvas offsets are relative to the same object, and the touch event coordinates on the canvas can be computed correctly.

References

Author

Lei Mao

Posted on

12-05-2022

Updated on

12-05-2022

Licensed under


Comments