Skip to content

Commit fc9d0c3

Browse files
authored
Merge pull request #72 from syzer/fix/mobile-swipe
Handle mobile touch gestures for scene controls
2 parents 1d62bd9 + 7038893 commit fc9d0c3

6 files changed

Lines changed: 451 additions & 7 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ jobs:
4747
run: |
4848
cp ../../wasm/* .
4949
cp ../../vizmat-app/index.html .
50+
cp ../../vizmat-app/mobile-gesture.js .
5051
cp ../../vizmat-app/style.css .
5152
npm install
5253
npm run build

vizmat-app/index.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
<head>
44
<meta charset="UTF-8">
55
<title>vizmat: a cross platform crystal visualizer</title>
6-
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<meta
7+
name="viewport"
8+
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
9+
>
710
<link rel="preconnect" href="https://fonts.googleapis.com">
811
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
912
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&display=swap" rel="stylesheet">
@@ -18,9 +21,11 @@
1821
data-cargo-no-default-features="true" />
1922
<link data-trunk rel="copy-dir" href="assets" />
2023
<link data-trunk rel="copy-file" href="index.js" />
24+
<link data-trunk rel="copy-file" href="mobile-gesture.js" />
2125
<link data-trunk rel="copy-file" href="style.css" />
2226
<link href="style.css" rel="stylesheet" />
2327
<script type="module" src="./index.js"></script>
28+
<script type="module" src="./mobile-gesture.js"></script>
2429
<h1 class="site-title">vizmat <span>molecule / crystal</span> visual lab</h1>
2530
<header>
2631
<a href="https://github.com/rs4rse/vizmat" target="_blank">

vizmat-app/mobile-gesture.js

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
(function () {
2+
const canvas = document.getElementById("bevy-canvas");
3+
4+
const emitTouchGesture = (gesture) => {
5+
window.dispatchEvent(
6+
new CustomEvent("vizmat-touch-gesture", {
7+
detail: gesture,
8+
}),
9+
);
10+
};
11+
12+
const toVec = (touch) => ({ x: touch.clientX, y: touch.clientY });
13+
14+
const touchPanState = {
15+
one: null,
16+
two: {
17+
active: false,
18+
midpoint: null,
19+
distance: null,
20+
},
21+
};
22+
23+
const getMidpoint = (a, b) => ({
24+
x: (a.x + b.x) * 0.5,
25+
y: (a.y + b.y) * 0.5,
26+
});
27+
28+
const getDistance = (a, b) => {
29+
const dx = a.x - b.x;
30+
const dy = a.y - b.y;
31+
return Math.hypot(dx, dy);
32+
};
33+
34+
const clearTouchState = () => {
35+
touchPanState.one = null;
36+
touchPanState.two = {
37+
active: false,
38+
midpoint: null,
39+
distance: null,
40+
};
41+
};
42+
43+
const emitIf = (condition, payload) => {
44+
if (!condition) return false;
45+
emitTouchGesture(payload);
46+
return true;
47+
};
48+
49+
if (canvas) {
50+
canvas.style.touchAction = "none";
51+
52+
if (window.PointerEvent) {
53+
const pointerState = new Map();
54+
55+
const removePointer = (event) => {
56+
if (event.pointerType !== "touch") return;
57+
if (!event.currentTarget || !pointerState.has(event.pointerId)) return;
58+
pointerState.delete(event.pointerId);
59+
60+
if (pointerState.size === 0) {
61+
clearTouchState();
62+
return;
63+
}
64+
65+
if (pointerState.size === 1) {
66+
touchPanState.one = [...pointerState.values()][0];
67+
touchPanState.two = {
68+
active: false,
69+
midpoint: null,
70+
distance: null,
71+
};
72+
return;
73+
}
74+
75+
const points = [...pointerState.values()];
76+
touchPanState.two = {
77+
active: true,
78+
midpoint: getMidpoint(points[0], points[1]),
79+
distance: getDistance(points[0], points[1]),
80+
};
81+
};
82+
83+
const updateSinglePointer = (point) => {
84+
if (!touchPanState.one) {
85+
touchPanState.one = point;
86+
return;
87+
}
88+
89+
const dx = point.x - touchPanState.one.x;
90+
const dy = point.y - touchPanState.one.y;
91+
const emitted = emitIf(dx !== 0 || dy !== 0, {
92+
gesture: "Rotate",
93+
dx,
94+
dy,
95+
scale_delta: 0,
96+
});
97+
if (emitted) {
98+
touchPanState.one = point;
99+
}
100+
};
101+
102+
const updateTwoPointer = (a, b) => {
103+
if (!touchPanState.two.active) {
104+
touchPanState.two = {
105+
active: true,
106+
midpoint: getMidpoint(a, b),
107+
distance: getDistance(a, b),
108+
};
109+
return;
110+
}
111+
112+
const midpoint = getMidpoint(a, b);
113+
const distance = getDistance(a, b);
114+
const panDx = midpoint.x - touchPanState.two.midpoint.x;
115+
const panDy = midpoint.y - touchPanState.two.midpoint.y;
116+
const scaleDelta = touchPanState.two.distance > 0
117+
? (distance - touchPanState.two.distance) / touchPanState.two.distance
118+
: 0;
119+
120+
const emitted = emitIf(panDx !== 0 || panDy !== 0 || scaleDelta !== 0, {
121+
gesture: "TwoFinger",
122+
dx: panDx,
123+
dy: panDy,
124+
scale_delta: scaleDelta,
125+
});
126+
127+
if (emitted) {
128+
touchPanState.two.midpoint = midpoint;
129+
touchPanState.two.distance = distance;
130+
}
131+
};
132+
133+
canvas.addEventListener(
134+
"pointerdown",
135+
(event) => {
136+
if (event.pointerType !== "touch") return;
137+
if (!event.target) return;
138+
const point = toVec(event);
139+
pointerState.set(event.pointerId, point);
140+
event.currentTarget.setPointerCapture(event.pointerId);
141+
touchPanState.one = point;
142+
touchPanState.two = {
143+
active: false,
144+
midpoint: null,
145+
distance: null,
146+
};
147+
event.preventDefault();
148+
},
149+
{ passive: false },
150+
);
151+
152+
canvas.addEventListener(
153+
"pointermove",
154+
(event) => {
155+
if (event.pointerType !== "touch") return;
156+
if (!pointerState.has(event.pointerId)) return;
157+
158+
const point = toVec(event);
159+
pointerState.set(event.pointerId, point);
160+
161+
if (pointerState.size === 1) {
162+
updateSinglePointer(point);
163+
} else if (pointerState.size >= 2) {
164+
const points = [...pointerState.values()];
165+
const a = points[0];
166+
const b = points[1];
167+
updateTwoPointer(a, b);
168+
}
169+
170+
event.preventDefault();
171+
},
172+
{ passive: false },
173+
);
174+
175+
canvas.addEventListener(
176+
"pointerup",
177+
removePointer,
178+
{ passive: false },
179+
);
180+
canvas.addEventListener("pointercancel", removePointer, {
181+
passive: false,
182+
});
183+
canvas.addEventListener("pointerleave", removePointer, {
184+
passive: false,
185+
});
186+
canvas.addEventListener("pointerout", removePointer, { passive: false });
187+
canvas.addEventListener("pointerlostcapture", removePointer);
188+
} else {
189+
canvas.addEventListener(
190+
"touchstart",
191+
(event) => {
192+
if (!event.target) return;
193+
if (event.touches.length === 1) {
194+
touchPanState.one = toVec(event.touches[0]);
195+
touchPanState.two.active = false;
196+
} else if (event.touches.length === 2) {
197+
const a = toVec(event.touches[0]);
198+
const b = toVec(event.touches[1]);
199+
touchPanState.two = {
200+
active: true,
201+
midpoint: getMidpoint(a, b),
202+
distance: getDistance(a, b),
203+
};
204+
touchPanState.one = null;
205+
}
206+
event.preventDefault();
207+
},
208+
{ passive: false },
209+
);
210+
211+
canvas.addEventListener(
212+
"touchmove",
213+
(event) => {
214+
if (event.touches.length === 1 && touchPanState.one) {
215+
const current = toVec(event.touches[0]);
216+
const dx = current.x - touchPanState.one.x;
217+
const dy = current.y - touchPanState.one.y;
218+
219+
if (dx !== 0 || dy !== 0) {
220+
emitTouchGesture({
221+
gesture: "Rotate",
222+
dx,
223+
dy,
224+
scale_delta: 0,
225+
});
226+
touchPanState.one = current;
227+
}
228+
event.preventDefault();
229+
return;
230+
}
231+
232+
if (event.touches.length === 2 && touchPanState.two.active) {
233+
const a = toVec(event.touches[0]);
234+
const b = toVec(event.touches[1]);
235+
const midpoint = getMidpoint(a, b);
236+
const distance = getDistance(a, b);
237+
238+
const panDx = midpoint.x - touchPanState.two.midpoint.x;
239+
const panDy = midpoint.y - touchPanState.two.midpoint.y;
240+
const scaleDelta = touchPanState.two.distance > 0
241+
? (distance - touchPanState.two.distance) /
242+
touchPanState.two.distance
243+
: 0;
244+
245+
if (panDx !== 0 || panDy !== 0 || scaleDelta !== 0) {
246+
emitTouchGesture({
247+
gesture: "TwoFinger",
248+
dx: panDx,
249+
dy: panDy,
250+
scale_delta: scaleDelta,
251+
});
252+
touchPanState.two.midpoint = midpoint;
253+
touchPanState.two.distance = distance;
254+
}
255+
256+
event.preventDefault();
257+
}
258+
},
259+
{ passive: false },
260+
);
261+
}
262+
263+
const clearTouch = () => clearTouchState();
264+
canvas.addEventListener("touchend", clearTouch, { passive: false });
265+
canvas.addEventListener("touchcancel", clearTouch, { passive: false });
266+
canvas.addEventListener("touchleave", clearTouch, { passive: false });
267+
}
268+
})();

vizmat-app/style.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ html[data-theme="light"] {
4747
html, body {
4848
width: 100%;
4949
height: 100%;
50+
touch-action: none;
51+
overscroll-behavior: none;
52+
user-select: none;
53+
-webkit-user-select: none;
54+
-webkit-touch-callout: none;
5055
}
5156
body {
5257
margin: 0;
@@ -70,6 +75,7 @@ body {
7075
color: var(--accent);
7176
}
7277
header {
78+
touch-action: none;
7379
position: fixed;
7480
top: 10px;
7581
right: 10px;
@@ -97,6 +103,7 @@ header a img {
97103
height: 20px;
98104
}
99105
.hint {
106+
touch-action: none;
100107
position: fixed;
101108
left: 12px;
102109
bottom: 12px;
@@ -124,6 +131,11 @@ canvas {
124131
height: 100%;
125132
display: block;
126133
background: transparent;
134+
touch-action: none;
135+
overscroll-behavior: none;
136+
user-select: none;
137+
-webkit-user-select: none;
138+
-webkit-touch-callout: none;
127139
}
128140
.canvas-shell {
129141
position: fixed;
@@ -137,6 +149,10 @@ canvas {
137149
border: 1px solid var(--line);
138150
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
139151
background: var(--canvas-bg);
152+
touch-action: none;
153+
overscroll-behavior: none;
154+
user-select: none;
155+
-webkit-user-select: none;
140156
}
141157
.loader {
142158
position: fixed;

0 commit comments

Comments
 (0)