Skip to content

Commit 69b8d7f

Browse files
committed
feat: add multi-touch HTML examples
1 parent bf1747e commit 69b8d7f

File tree

2 files changed

+398
-0
lines changed

2 files changed

+398
-0
lines changed

event/multi-touch.html

Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Multi Touch</title>
7+
<style>
8+
* {
9+
touch-action: none;
10+
user-select: none;
11+
-webkit-user-select: none;
12+
}
13+
body {
14+
margin: 0;
15+
}
16+
main {
17+
width: 100vw;
18+
height: 100vh;
19+
background-color: skyblue;
20+
padding: 24px;
21+
box-sizing: border-box;
22+
display: flex;
23+
flex-direction: column;
24+
}
25+
h2 {
26+
margin: 10px 0;
27+
}
28+
.container {
29+
flex: 1;
30+
background: white;
31+
border: 1px solid black;
32+
position: relative;
33+
}
34+
.box {
35+
width: 100px;
36+
height: 100px;
37+
position: absolute;
38+
}
39+
#box1 {
40+
background-color: red;
41+
left: 10px;
42+
top: 10px;
43+
}
44+
#box2 {
45+
background-color: blue;
46+
left: 120px;
47+
top: 10px;
48+
}
49+
.box.long-press {
50+
animation: animating 2s infinite;
51+
}
52+
.box.double-tap {
53+
animation: animating 0.5s;
54+
}
55+
#log {
56+
background-color: rgba(0,0,0,0.7);
57+
color: white;
58+
padding: 10px;
59+
font-size: 12px;
60+
max-height: 200px;
61+
overflow-y: auto;
62+
}
63+
#log p {
64+
margin: 0;
65+
}
66+
.button-container {
67+
width: 100%;
68+
position: absolute;
69+
left: 0;
70+
bottom: 10px;
71+
display: flex;
72+
justify-content: center;
73+
align-items: center;
74+
gap: 10px;
75+
}
76+
.action-button {
77+
background-color: #aaa;
78+
color: white;
79+
border-radius: 10px;
80+
}
81+
.action-button.on {
82+
background-color: blue;
83+
}
84+
@keyframes animating {
85+
0% {
86+
background-color: red;
87+
}
88+
50% {
89+
background-color: blue;
90+
}
91+
}
92+
</style>
93+
</head>
94+
<body>
95+
<main>
96+
<h2>multi touch</h2>
97+
<div id="container1" class="container">
98+
<div id="box1" class="box" data-draggable="true"></div>
99+
<div id="box2" class="box" data-draggable="true"></div>
100+
<div class="button-container">
101+
<button class="action-button" data-action="pinchZoom">pinchZoom</button>
102+
<button class="action-button" data-action="rotate">rotate</button>
103+
</div>
104+
</div>
105+
106+
<div id="log">
107+
<p>
108+
<b>action type:</b> <span class="action-type"></span>
109+
</p>
110+
<p>
111+
<b>event type:</b> <span class="event-type"></span>
112+
</p>
113+
<p>
114+
<b>length:</b> <span class="length"></span>
115+
</p>
116+
<p>
117+
<b>touches:</b> <span class="touches"></span>
118+
</p>
119+
<p>
120+
<b>distance:</b> <span class="distance"></span>
121+
</p>
122+
<p>
123+
<b>diff:</b> <span class="diff"></span>
124+
</p>
125+
<p>
126+
<b>angle:</b> <span class="angle"></span>
127+
</p>
128+
<p>
129+
<b>error:</b> <span class="error"></span>
130+
</p>
131+
<p>
132+
<b>rotate:</b> <span class="rotate"></span>
133+
</p>
134+
</div>
135+
</main>
136+
137+
<script>
138+
const body = document.querySelector('body');
139+
const log = document.getElementById('log');
140+
const box = document.getElementById('box');
141+
const container = document.getElementsByClassName('container')[0];
142+
143+
let logEvents = false;
144+
let eventCache = null;
145+
let currentActions = ['pinchZoom'];
146+
let pressTime = null;
147+
148+
currentActions.forEach(action => {
149+
document.querySelector(`button[data-action="${action}"]`).classList.add('on');
150+
});
151+
152+
// --------------------------------------------------------------------------
153+
// helper
154+
const printLog = (event) => {
155+
if(!event) return;
156+
157+
const touches = event.touches ? Array.from(event.touches) : [];
158+
log.querySelector('.action-type').innerText = currentActions.join(', ');
159+
log.querySelector('.event-type').innerText = event.type;
160+
log.querySelector('.length').innerText = touches.length;
161+
log.querySelector('.touches').innerText = touches.map(touch => `${touch.clientX?.toFixed(2)}, ${touch.clientY?.toFixed(2)}`).join('\n');
162+
}
163+
164+
const getRotationFromCSS = (element) => {
165+
const style = window.getComputedStyle(element);
166+
const transform = style.getPropertyValue('transform') ||
167+
style.getPropertyValue('-webkit-transform') ||
168+
style.getPropertyValue('-moz-transform');
169+
170+
if (transform && transform !== 'none') {
171+
const values = transform.split('(')[1].split(')')[0].split(',');
172+
const a = values[0];
173+
const b = values[1];
174+
const angle = Math.round(Math.atan2(b, a) * (180 / Math.PI));
175+
return angle;
176+
}
177+
178+
return 0;
179+
}
180+
181+
const getStyle = (element) => {
182+
const {left, top, right, bottom, ...rest} = window.getComputedStyle(element);
183+
return {left: parseInt(left), top: parseInt(top), right: parseInt(right), bottom: parseInt(bottom), ...rest};
184+
}
185+
186+
const isDoubleTap = (el) => {
187+
const lastTouch = el.dataset.lastTouch;
188+
if(!lastTouch) return false;
189+
190+
const now = new Date().getTime();
191+
const delta = now - lastTouch;
192+
if (delta < 80 && delta > 0) {
193+
el.classList.add('double-tap');
194+
setTimeout(() => {
195+
el.classList.remove('double-tap');
196+
}, 500);
197+
return true;
198+
}
199+
return false;
200+
}
201+
202+
// --------------------------------------------------------------------------
203+
// event handler configuration
204+
const start = (event) => {
205+
if(!event || event.touches.length === 0) return;
206+
printLog(event);
207+
eventCache = event;
208+
event.target.dataset.lastTouch = new Date().getTime();
209+
210+
if(event.touches.length === 1) {
211+
longPress(event);
212+
}
213+
if(event.touches.length === 3) {
214+
onThreePointer(event);
215+
}
216+
if(event.touches.length === 4) {
217+
onFourPointer(event);
218+
}
219+
}
220+
221+
const move = (event) => {
222+
if(!event || event.touches.length === 0) return;
223+
try {
224+
if(event.touches.length === 1 || event.touches.length === 3) {
225+
drag(event);
226+
}
227+
if (event.touches.length === 2) {
228+
if(currentActions.includes('pinchZoom')) {
229+
pinchZoom(event);
230+
}
231+
if(currentActions.includes('rotate')) {
232+
rotate(event);
233+
}
234+
}
235+
} catch (e) {
236+
log.querySelector('.error').innerText = e.message;
237+
}
238+
239+
printLog(event);
240+
eventCache = event;
241+
}
242+
243+
const end = (event) => {
244+
if(!event || event.touches.length === 0) return;
245+
printLog(eventCache);
246+
eventCache = null;
247+
248+
if(pressTime) {
249+
clearTimeout(pressTime);
250+
}
251+
252+
if (isDoubleTap(event.target)) {
253+
console.log('double tap');
254+
}
255+
}
256+
257+
const setHandlers = (id) => {
258+
const el = document.getElementById(id);
259+
el.ontouchstart = start;
260+
el.ontouchmove = move;
261+
el.ontouchend = end;
262+
}
263+
264+
const init = () => {
265+
const body = document.querySelector('main');
266+
body.ontouchstart = (event) => {
267+
event.preventDefault();
268+
}
269+
const draggable = document.querySelectorAll('[data-draggable="true"]');
270+
draggable.forEach(el => {
271+
setHandlers(el.id);
272+
});
273+
}
274+
275+
init();
276+
277+
// --------------------------------------------------------------------------
278+
/**
279+
* Multi-Touch Functionality
280+
* */
281+
// handle 1 Pointer
282+
const longPress = (event) => {
283+
if(pressTime) {
284+
clearTimeout(pressTime);
285+
event.target.classList.remove('long-press');
286+
}
287+
pressTime = setTimeout(() => {
288+
event.target.classList.add('long-press');
289+
}, 3000);
290+
}
291+
292+
const drag = (event) => {
293+
if(eventCache === null) return;
294+
try{
295+
const touch = Array.from(event.touches)[0];
296+
const diffX = touch.clientX - eventCache.touches[0].clientX;
297+
const diffY = touch.clientY - eventCache.touches[0].clientY;
298+
log.querySelector('.diff').innerText = diffX + ', ' + diffY;
299+
300+
const {top, left} = getStyle(event.target);
301+
302+
event.target.style.top = `${top + diffY}px`;
303+
event.target.style.left = `${left + diffX}px`;
304+
} catch(e) {
305+
log.querySelector('.error').innerText = "[error][drag] " + e.message;
306+
}
307+
}
308+
309+
const copy = (event) => {
310+
try{
311+
const copyId = event.target.id.includes('copy') ? event.target.id.split('-copy')[0] + '-copy' + (parseInt(event.target.id.split('-copy')[1]) + 1) : event.target.id + '-copy1';
312+
313+
const style = getStyle(event.target);
314+
const copiedElement = event.target.cloneNode(true);
315+
copiedElement.style.backgroundColor = style.backgroundColor;
316+
copiedElement.id = copyId;
317+
copiedElement.style.left = style.left + 'px';
318+
copiedElement.style.top = style.top + 'px';
319+
320+
container.appendChild(copiedElement);
321+
setHandlers(copyId);
322+
} catch(e) {
323+
log.querySelector('.error').innerText = "[error][copy] " + e.message;
324+
}
325+
}
326+
327+
// handle 2 Pointer ----------------------------------------------------------
328+
const pinchZoom = (event) => {
329+
const touches = Array.from(event.touches);
330+
const touch1 = touches[0];
331+
const touch2 = touches[1];
332+
333+
const distance = Math.sqrt(Math.pow(touch1.clientX - touch2.clientX, 2) + Math.pow(touch1.clientY - touch2.clientY, 2));
334+
log.querySelector('.distance').innerText = distance;
335+
336+
const prevDistance = Math.sqrt(Math.pow(eventCache.touches[0].clientX - eventCache.touches[1].clientX, 2) + Math.pow(eventCache.touches[0].clientY - eventCache.touches[1].clientY, 2));
337+
338+
const diff = distance - prevDistance;
339+
log.querySelector('.diff').innerText = diff;
340+
341+
let scale = 1 + diff / 100;
342+
const width = event.target.clientWidth;
343+
const height = event.target.clientHeight;
344+
345+
const widthScale = Math.floor(width * scale);
346+
const heightScale = Math.floor(height * scale);
347+
348+
const minTrashhold = 16;
349+
const maxTrashhold = container.clientWidth;
350+
351+
if(widthScale >= minTrashhold && heightScale >= minTrashhold && widthScale <= maxTrashhold) {
352+
event.target.style.width = `${widthScale}px`;
353+
event.target.style.height = `${heightScale}px`;
354+
event.target.innerText = `${widthScale}, ${heightScale}`;
355+
}
356+
}
357+
358+
const rotate = (event) => {
359+
const touches = Array.from(event.touches);
360+
const touch1 = touches[0];
361+
const touch2 = touches[1];
362+
363+
const angle = Math.atan2(touch2.clientY - touch1.clientY, touch2.clientX - touch1.clientX);
364+
log.querySelector('.angle').innerText = angle;
365+
366+
const rotate = getRotationFromCSS(event.target);
367+
const newRotation = rotate + 1;
368+
event.target.style.transform = `rotate(${newRotation}deg)`
369+
}
370+
371+
// handle 3 Pointer ----------------------------------------------------------
372+
const onThreePointer = (event) => {
373+
}
374+
375+
// handle 4 Pointer ----------------------------------------------------------
376+
const onFourPointer = (event) => {
377+
}
378+
379+
// --------------------------------------------------------------------------
380+
// button event
381+
const actionButtons = document.querySelectorAll('.action-button');
382+
Array.from(actionButtons).forEach(button => {
383+
button.ontouchend = (e) => e.stopPropagation();
384+
button.ontouchstart = (e) => e.stopPropagation();
385+
386+
button.addEventListener('pointerdown', (event) => {
387+
if(currentActions.includes(button.dataset.action)) {
388+
currentActions = currentActions.filter(action => action !== button.dataset.action);
389+
button.classList.remove('on');
390+
} else {
391+
currentActions.push(button.dataset.action);
392+
button.classList.add('on');
393+
}
394+
});
395+
});
396+
</script>
397+
</body>
398+
</html>

0 commit comments

Comments
 (0)