Note
This is Part 2 of a miniseries in which I'm revisiting an old weekend hack of mine from college, which I called jSphere (because I was lazy and couldn't come up with anything more interesting).If you haven't read Part 1, which was all about the HTML and CSS, I recommend giving it a skim, or at least reading the Backstory section at the beginning.
As I said last time, I'm not going to discuss what the code does in this post, just its structure and syntax. But I do plan to write a proper walkthrough and tutorial later.
Last time...
In Part 1, time we went over the markup and styling of , and I already found that pretty interesting. But, I mean, this project is primarily a JavaScript project. It's all about that <canvas>
!
For reference, here's what we're rebuilding:
Okay. Where to begin? Man, there was so much to change about this code, and a lot of it was interwoven, which makes it tough to really talk about step-by-step. So what I'll have to do is just... go for it, I guess.
Please, feel free to critique or otherwise comment on the code here! I love feedback and (constructive) criticism, I'm always interested in how other people would approach the same tasks, and I've even been known to enjoy a little bikeshedding now and then.
Minutia: nah.
Let me mention up front here that there are some changes I won't explicitly discuss, because they're too minor. Yes, I changed all the var
s to const
s and let
s, I reordered some functions, I changed all the TitleCasedMethods to normalCamelCase (why did I capitalize the first letter of all my methods? was I insane?). But you don't care about all those things. I'm going to focus on the really large things, the things I found interesting or funny, and the things that really demonstrate how far the language has come in seven years.
Let's start with a funny one.
Bro, do you even loop?
I fixed a spot where I had done something super weird, maybe because I was rushing and didn't think it through. This is in Sphere
's constructor, when I'm generating all the Dot
instances needed to represent the points on the surface of the sphere. I use a nested for
loop to move around the sphere in rings, top to bottom, adding dots to its surface as I went. This is fine, but I wrote the loops in a super weird way:
this.points = new Array();
var angstep = Math.PI/10;
var i = 0;
// Loop from 0 to 2*pi, creating one row of points at each step
for (var angxy=0; angxy<2*Math.PI; angxy+=angstep){
this.points[i] = new Array();
var j=0;
for (var angyz=0; angyz<2*Math.PI; angyz+=angstep) {
// Loop from 0 to 2*pi, creating one point at each step
var px = r * Math.cos(angxy) * Math.sin(angyz) + x,
py = r * Math.sin(angxy) * Math.sin(angyz) + y,
pz = r * Math.cos(angyz) + z,
pfg = pz > z;
this.points[i][j] = new Dot(px,py,pz,pfg);
j++;
}
i++;
}
What's up with the i
and j
variables there, hanging out outside of the loops? Why aren't they created and incremented within the loops? I definitely knew that a for
loop can have multiple variables; why didn't I just do this?
for (var i=0, angxy=0; angxy<2*Math.PI; i++, angxy+=angstep) { ... }
But also, why do I even need those index counters in the first place? Why do I use this.points[i] =
and this.points[i][j] =
to add to the arrays? Why aren't I just using [].push()
to add stuff to the array? And why on Earth did I use new Array()
instead of []
? How much OOP was I on???
So I got rid of all that nonsense. I also decided to make this.points
a single flat list of points, rather than a 2D matrix with rows, because I realized while refactoring some other code that the only way I ever referenced this.points
was with a couple of nested loops to reach each point, usually with weird for ... in
loops like this:
for (var i in this.points) {
for (var j in this.points[i]) {
this.points[i][j]...
}
}
First, it does work, but using for...in
loops on arrays like this is strange, and it's unexpected to say the least. I was very confused when I first saw it, and I imagine any other devs reading it would be thrown off as well.
Second, I never used i
or j
for anything other than indexing into this.points
, which makes this a great candidate for the much more elegant for...of
loop, which didn't exist when I originally wrote this code.
I refactored to fix all of these problems, and here's where I wound up. The constructor loops were updated to this (which also includes a change to the Dot constructor that I'll talk about in a bit):
// The angle delta to use when calculating the surface point positions;
// a larger angstep means fewer points on the surface
const angstep = Math.PI/10;
this.points = [];
// Loop from 0 to 2*pi, creating one row of points at each step
for (let angxy=0; angxy<2*Math.PI; angxy+=angstep){
for (let angyz=0; angyz<2*Math.PI; angyz+=angstep) {
// Loop from 0 to 2*pi, creating one point at each step
this.points.push(new Dot({
x: r * Math.cos(angxy) * Math.sin(angyz) + x,
y: r * Math.sin(angxy) * Math.sin(angyz) + y,
z: r * Math.cos(angyz) + z,
fg: Math.cos(angyz) > 0
}));
}
}
And the loops over the elements of this.points
now look like this:
for (const point of this.points) {
point...
}
Ahh... So much better. ๐
Classes!
Since this project was all about this interactive sphere composed of a bunch of dots on its surface, and since I was doing a CS degree in 2012 and the Functional Revolution of the last few years hadn't begun yet, the project is largely composed of two high-level object types:
-
Dot
- represents a single dot on the surface of the sphere -
Sphere
- represents the whole sphere, with all of itsDot
s
In 2012, the native class
was yet to be standardized, so I wrote everything in the old-school function
-as-a-constructor style. For exapmle, here's part of the Sphere
constructor:
function Dot(x,y,z,fg,fgColor,bgColor) {
// Default Values: { x: 0, y: 0, z: 0, fg: true, fgcolor: #7EE37E, bgcolor: "#787878" }
this.x = typeof x !== 'undefined' ? x : 0;
this.y = typeof y !== 'undefined' ? y : 0;
this.z = typeof z !== 'undefined' ? z : 0;
this.fg = typeof fg !== 'undefined' ? fg : true;
this.fgColor = typeof fgColor !== 'undefined' ? fgColor : "#7EE37E";
this.bgColor = typeof bgColor !== 'undefined' ? bgColor : "#787878";
this.Draw = function(ctx) {
// Store the context's fillStyle
var tmpStyle = ctx.fillStyle;
// Set the fillStyle for the dot
ctx.fillStyle = (this.fg ? this.fgColor : this.bgColor);
// Draw the dot
ctx.fillCircle(x,y,this.fg?10:5);
// Restore the previous fillStyle
ctx.fillStyle = tmpStyle;
}
}
Now, there's another issue here, which probably had to do with my limited understanding of JS prototypes: every instance of Dot
defines its own copy of the Draw
function, rather than referring to a class method. What I should have written is this:
function Dot(x,y,z,fg,fgColor,bgColor) {
// ...
}
Dot.protoype.Draw = function(ctx) {
// Store the context's fillStyle
var tmpStyle = ctx.fillStyle;
// Set the fillStyle for the dot
ctx.fillStyle = (this.fg ? this.fgColor : this.bgColor);
// Draw the dot
ctx.fillCircle(x,y,this.fg?10:5);
// Restore the previous fillStyle
ctx.fillStyle = tmpStyle;
}
This declares a single copy of the function, rather than wasting memory on a new function every time.
But that's a side issue anyway, since the native class
es introduced in ES2015 (aka ES6) handles that under the hood!
But before we get to that, let mention one more thing: ES2015 also introduced object destructuring, function parameter default values, and using both of those things together to basically get the equivalent of named parameters with default values, as seen in languages like Python and Ruby!
Aaaaand one more thing: ES2015 also introduced Object.assign()
, which I like to use to initialize this
with properties much more succinctly than the traditional one-by-one method. (Man, ES2015 was some good stuff!)
Using these two techniques together, I can eliminate this nasty snippet from the the Dot
constructor:
// Gross ๐คข
this.x = typeof x !== 'undefined' ? x : 0;
this.y = typeof y !== 'undefined' ? y : 0;
this.z = typeof z !== 'undefined' ? z : 0;
this.fg = typeof fg !== 'undefined' ? fg : true;
this.fgColor = typeof fgColor !== 'undefined' ? fgColor : "#7EE37E";
this.bgColor = typeof bgColor !== 'undefined' ? bgColor : "#787878";
Putting all these improvements together, I can rewrite the Dot
like this:
class Dot {
constructor({x=0, y=0, z=0, fg=true, fgColor='#7EE37E', bgColor='#787878'}={}) {
Object.assign(this, {x, y, z, fg, fgColor, bgColor});
}
draw(ctx) {
// Store the context's fillStyle
var tmpStyle = ctx.fillStyle;
// Set the fillStyle for the dot
ctx.fillStyle = (this.fg ? this.fgColor : this.bgColor);
// Draw the dot
ctx.fillCircle(x, y, this.fg ? 10 : 5);
// Restore the previous fillStyle
ctx.fillStyle = tmpStyle;
}
}
So much cleaner!
Re-separating my concerns
As I was rewriting my classes as I just described, I started to find places where Dot
s were doing stuff that was better handled by Sphere
, and Sphere
was repeatedly performing operations on each Dot
that could be much more cleanly handled internally by each of them.
First, that Draw
method of the Dot
class up there? Yeah, it was never actually being called. It turned out that I needed some context from the Sphere
about each Dot
in order to draw it correctly, namely where it was located on the surface of the sphere and how the sphere was currently rotated. And rather than pass all that context around to each Dot
, it was much cleaner to do all my drawing right from the Sphere
class. So... I deleted the Draw
method on Dot
, as well as the fgColor
and bgColor
properties that went with it.
Second, there are two major ways that the Dot
s are changed to move the Sphere
around: they're translated (moved around), and they're scaled (grown or shrunken). I had a bunch of repeated code in my event handlers where each Dot
's properties were being manually recalculated and updated in the same way, so I figured I'd DRY up my code and move the logic to change a Dot
into the Dot
class itself. I gave Dot
two new methods: scale
and translate
.
After all that, my Dot
class looks like this:
class Dot {
constructor({x=0, y=0, z=0, fg=true}={}) {
Object.assign(this, {x, y, z, fg});
}
/**
* Scale this dot's position by multiplying all coordinates by the given scale factor
* @param {Number} scaleFactor
*/
scale(scaleFactor) {
this.x *= scaleFactor
this.y *= scaleFactor
this.z *= scaleFactor
}
/**
* Move this dot to a new position indicated by the given x, y, and z distances
* @param {Number} [x=0]
* @param {Number} [y=0]
* @param {Number} [z=0]
*/
translate({x=0, y=0, z=0}) {
this.x += x
this.y += y
this.z += z
}
}
Don't mess with the prototype!
I made several changes that came from experience. Before I left college, I really had little to no knowledge of accepted community best practices, or why they were important. For example, I have a few utility functions for drawing on the canvas. In my old code, I added them to the CanvasRenderingContext2D
prototype directly, as follows:
Object.getPrototypeOf(ctx).fillCircle = ctxFillCircle;
This is not ideal. Suppose a real ctx.fillCircle
were added to the spec one day, and it was different from my code. In this minor demo it probably doesn't matter much, as I'm the only one working with the code, but in a project with multiple authors, it could be very confusing to come across a standardized method that looks different than it should. Even future me might be confused!
So I refactored: a ctx
parameter was added to each function, all references to this
in the functions were changed to ctx
, I removed the above lines that modified the CanvasRenderingContext2D
prototype, and I changed the code to call the functions directly and pass ctx
as the first argument rather than calling them as a method of ctx
.
Removing jQuery
Finally, jQuery. I love it, it did a lot of good for the web, but in most cases, including mine, it's no longer needed.
I basically used jQuery for three things in this project, two of which are trivially replaced:
- I used the
$(function() { ... })
wrapper to delay code execution until the page loaded. As I discussed in Part 1, this is unnecessary if you add thedefer
attribute to any<script>
tags that need to wait. (Thanks again, @crazytim!) - I used the classic
$()
selector function in several places to get elements. This is now (and actually was then, though I was unfamiliar with it) a native part of the platform withdocument.querySelector
anddocument.querySelectorAll
. - The tough one: I used the
jquery.events.drag
plugin to handle mouse-based interactivity. This one takes slightly more doing to replace.
The first two are super basic, and I won't bother showing them. Instead, I'll focus on that third one: the jquery.event.drag
event plugin.
Here's how I was using it:
$("#canvas")
.drag("init", function(e){
startX = e.clientX;
startY = e.clientY;
})
.drag(function(e){
if (e.ctrlKey) {
if (e.altKey) sphere.HiddenFun2(e.clientX-startX,e.clientY-startY);
else sphere.Zoom(e.clientX-startX, e.clientY-startY, -1);
} else if (e.shiftKey) {
if (e.altKey) sphere.HiddenFun1(e.clientX-startX,e.clientY-startY);
else sphere.Pan(e.clientX-startX,e.clientY-startY);
} else {
sphere.Rotate(e.clientX-startX,e.clientY-startY);
}
startX = e.clientX;
startY = e.clientY;
});
So when dragging begins, store the starting position of the cursor, then when dragging continues, perform certain actions based on the modifier keys used, and update the cursor position for the next movement.
So how would we do this with regular old mouse
events?
I implemented it in three stages: drag start, drag, and drag end. The drag start event is mousedown
; we'll record the starting position of the cursor, and add the drag and drag end event listeners. The drag event will be mousemove
; here we'll do all the logic to check for modifier keys and actually manipulate the sphere. Finally, I'll use two events for drag stop: mouseup
and mouseout
. That way, we don't get that weird case where you move your mouse out of the window and then let go, and the app is stuck in the dragging state until you click again. Those events just remove the event handlers registered on drag start.
I decided to add mousemove
, mouseup
, and mouseout
to the window
so that you can move your mouse around the page after clicking. And I had to suppress the mouseout
event on the canvas
and body
elements using event.stopPropagation()
to prevent prematurely triggering the drag stop when moving your mouse in and out of the canvas.
Here's what all that looks like:
let dragOrigin;
canvas.addEventListener('mousedown', dragStartHandler)
function dragStartHandler(e){
dragOrigin = {
x: e.clientX,
y: e.clientY
};
document.addEventListener('mousemove', dragHandler);
window.addEventListener('mouseup', dragStopHandler);
window.addEventListener('mouseout', dragStopHandler);
canvas.addEventListener('mouseout', stopPropagation);
document.body.addEventListener('mouseout', stopPropagation);
}
function dragHandler(e){
if (e.ctrlKey || e.metaKey) {
if (e.altKey) sphere.hiddenFun2(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y);
else sphere.zoom(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y, -1);
}
else if (e.shiftKey) {
if (e.altKey) sphere.hiddenFun1(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y);
else sphere.pan(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y);
}
else {
sphere.rotate(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y);
}
dragOrigin.x = e.clientX;
dragOrigin.y = e.clientY;
}
function dragStopHandler(e) {
document.removeEventListener('mousemove', dragHandler);
document.removeEventListener('mouseup', dragStopHandler);
document.removeEventListener('mouseout', dragStopHandler);
canvas.removeEventListener('mouseout', stopPropagation);
document.body.removeEventListener('mouseout', stopPropagation);
}
function stopPropagation(e) {
e.stopPropagation();
}
So even that wasn't too bad! But then I realized I had forgotten about...
Touch support for mobile ๐ญ
After implementing my updated code, I visited the page on my phone; nothing worked. Then it hit me: phones don't have mouses! ๐คฆ
Some quick research reminded me of the relatively recent pointer
events (pointermove
, pointerup
, etc.), which are made to unify lots of different kinds of pointer-ish interactions: mouse, touch, stylus, new stuff we haven't thought of yet. The problem (for now) is that mobile support is spotty: pointer
events work on Chrome for Android, Opera Mobile, Android Browser, Samsung Browser, and IE Mobile (???), but not Firefox for Android, iOS Safari, or most other mobile browsers..
The other problem is that the controls to do anything other than spin the sphere currently require modifier keys on the keyboard to be pressed (Shift+drag to pan, Ctrl+drag to zoom). So pointer
events on their own will only enable rotating, nothing else.
A proper solution would be to learn about touch
events and how to implement gestures like pinch-zoom and 2-finger-pan, but I'll be honest, I looked into it a little and it's so complex! More than I want to get into right now. So the pointer
events will have to do as a half-measure that only fixes rotating and only works on some mobile devices. And I'm not thrilled about it ๐
Anyway, here's what all of that looks like. I'm testing for pointer
event support by checking whether the <body>
has an onpointermove
property, and if not I fall back to mouse
events, since there are desktop browsers that don't support pointer
events, too (looking at you, Safari ๐ก).
// Prefer 'pointer' events when available
const pointerEvent = (
'onpointermove' in document.body
? 'pointer'
: 'mouse'
);
// Shorthands for 'mouse' or 'pointer' events
const [downEvt, upEvt, moveEvt, outEvt] = (
['down', 'up', 'move', 'out']
.map(evtType => pointerEvent + evtType)
);
// Drag events
let dragOrigin;
canvas.addEventListener(downEvt, dragStartHandler)
function dragStartHandler(e){
dragOrigin = {
x: e.clientX,
y: e.clientY
};
document.addEventListener(moveEvt, dragHandler);
window.addEventListener(upEvt, dragStopHandler);
window.addEventListener(outEvt, dragStopHandler);
canvas.addEventListener(outEvt, stopPropagation);
document.body.addEventListener(outEvt, stopPropagation);
}
function dragHandler(e){
if (e.ctrlKey || e.metaKey) {
if (e.altKey) sphere.hiddenFun2(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y);
else sphere.zoom(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y, -1);
}
else if (e.shiftKey) {
if (e.altKey) sphere.hiddenFun1(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y);
else sphere.pan(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y);
}
else {
sphere.rotate(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y);
}
dragOrigin.x = e.clientX;
dragOrigin.y = e.clientY;
}
function dragStopHandler(e) {
document.removeEventListener(moveEvt, dragHandler);
document.removeEventListener(upEvt, dragStopHandler);
document.removeEventListener(outEvt, dragStopHandler);
canvas.removeEventListener(outEvt, stopPropagation);
document.body.removeEventListener(outEvt, stopPropagation);
}
function stopPropagation(e) {
e.stopPropagation();
}
By the way, while working on this I discovered that Chrome's dev tools are very nice for emulating mobile devices with touch events!
Oh, one final thing. I had to add a CSS rule to the canvas to make touch work properly: touch-action: none;
. This basically prevents the browser from capturing touch events to try and scroll the page, and lets me use them in my JavaScript.
Finished product
It was a lot of iteration, and to be honest I tweaked it a bunch more while writing this article, but it's out there! Here's what it looks like with all the updates:
Conclusion
There are still things I'd like to fix. The biggest issue I have is that it's not mobile friendly, really at all. Not only do zoom and pan not work on mobile, but the webpage itself is almost completely non-responsive. I'd like to add some CSS to handle especially small page sizes more cleanly. Maybe I'll tweak it some more after this ๐ค As I said at the top, I'd love any feedback you might have! Heck, if you're feeling generous, make a PR against the repo!
But here's my overall takeaway: HTML, CSS, and JavaScript have improved a ton in the last 7 years, and so have I as a developer. Experience is an amazing thing!
In the final part of this series, which I'll write...eventually, I'll walk through the JavaScript itself and talk about how it works. That one will really be a standalone tutorial on how to build a thing with <canvas>
in vanilla JavaScript, not really dependent on these previous two posts.
Top comments (17)
Awesome! Thanks for sharing!
Canvas is an interesting and powerful thing and we're using it way less it deserves to be used. Maybe a proper UI library that eliminate the verbosity of working with canvas would make it more attractive.
The expressiveness of Javascript increased orders of magnitude more, than anyone would expect. The rewrite of the
Dot
constructor makes it immediately visible. It's shorter, easier to understand and less error prone, than the ternary typeof checks of arguments. A single!
there is easily overseen or accidentally introduced and yields somewhat around an hour of debugging fun.I'm looking forward to your next article. ๐๐
Jonas, you mean something like d3.js or chart.js or an actual UI library with components and such?
Well both libraries serve the needs for data visualizations.
What I meant, were libraries to build full blown interactive UIs, based on canvas. In the (not so recent) past, there was a boom of super fancy Web
AppsSites using Flash only to render their contents.Building user interactions within canvas is quite verbose, same for rendering UI elements (accessibility could be shimmed by an underlying html construct to support screen readers and text selection).
From what I've heard there is a React renderer that renders to canvas. I don't know how usable it is in reality - but I think that goes into the direction I was thinking of.
Edit: For drawing UI elements I found React Canvas and for drawing interactive graphic content I found React Cova. Their use cases seem to be orthogonal to each other i.e. you cant replace one with the other).
I have only one question: why? Why this would be something that's needed? I'm not sure I understand. Thank you!
Games are the first thing that come to my mind. Title screens and menus and such with clickable components. Entire web apps in a Flash style are hopefully gone for good though ๐ฌ
Google Maps is a prominent example, that uses interactive canvas to render large pieces of the UI. Typical use cases would include any interactivity, that goes beyond the capabilities of boxes and text (like zoomable super high resolution images, virtual flip-charts or street maps.)
Hahaha, yeah ...hopefully ๐
@kenbellows right, games! I have the feeling that we'll skip canvas UI libraries to go straight to WebAssembly for that type of apps.
For general apps (games included) I'm curious to see if Flutter's Hummingbird will amount to something. With Flutter they are targeting Skia, a 2D graphics library Google built in C++. With Hummingbird they might be able to target the Canvas instead.
BTW it seems that the HTML5 standard is quite strongly against text editing controls implemented in the canvas.
For good reason, I've tried it and there are so many problems that have to be solved just to get it to act like a regular text input, not to mention the accessibility nightmare it creates! (Although I suppose any predominately canvas based app is going to face huge a11y issues...) If I ever need text input in a mostly-canvas-based situation again, I'll just absolute-position an actual
<input>
tag where I want it and set focus to it or somethingAs for WASM, I haven't looked into this topic yet, but I have two questions:
How much canvas access does WASM have? Wouldn't you still need JS for the actual draw actions, even if some other heavy processing of game states happened in WASM? Is there a planned future where WASM has canvas access, if it doesn't now?
How does how does WASM relate and compare to WebGL for heavy canvas processing? I've actually seen some significant use of WebGL over the last few years for browser games, FPSes and such, and the results have been pretty impressive.
Agreed, the list of reasons not to do that it's quite long on the HTML5 spec :D
Yeah, wasm still needs JS. It doesn't have direct access to the canvas but you can access the wasm memory. This is an example of the game of life written in Rust and the canvas API.
They are working on something to help with that. Right now wasm only understand numbers and that doesn't really help if the goal is to have it talk to the DOM. Look for "Easy and fast data exchange" in this article.
I think you can access WebGL from WebAssembly:
There's a lot going on :D
You know, now that I'm thinking more about it, there actually is a group of libraries that attempts to do this, and I think they use the canvas: CreateJS, and specifically EaselJS, their main drawing library. I played around with it several years ago and remember having a pretty good experience with it, but I didn't do anything super advanced with it.
Thanks for the compliment! ๐
One thing I find really interesting is the emphasis that the TC39 working group (that is, the folks that develop the ECMAScript spec that becomes JavaScript) often put on intentionally writing very low-level APIs, with the express intention that community members should layer more convenient libraries on top of them. This way everyone isn't stuck with a high-level API that a lot of people dislike. A good recent example of this kind of low-level API is the Shared Memory & Atomics proposal that adds some very important concurrency features to the language. Using these APIs directly would probably be horrible, but there are lots of ways for people with deep understanding of concurrency to layer very useable convenient concurrency libraries on top.
All that to say, I think you're right on the money about Canvas: it could really benefit from some higher level libraries layered on top of it to make it more usable. It's rather tedious to use by hand for anything more complicated than jSphere, but I think that's by design. We really need someone to come along and publish that perfect library that will make the canvas easier to use...
Thanks for your answer! ๐
Low level APIs are the very common across recent Browser/Web APIs. Working directly with those low level APIs usually works out until you approach a certain degree of complexity. At some point you are required to build an abstraction layer on top of these low level apis, that serves your needs.
The Shared Memory and Atomics, you mentioned is one example. Another currently very emerging example are Web Component. Writing Web-Components with the browser APIs only is certainly not as verbose as fighting with concurrent memory management, but starts becoming verbose as soon as you want to sync more than two attributes with props of your web component.
Within the last year a lot of libraries for building web-components were released. Each with their own benefits and promises.
So you're absolutely right: low level APIs are a very common and necessary pattern (to serve everyones needs).
Nice adventure Ken!
Have you considered using a library for drag & drop and touch handling? It seems like the sort of thing that a library that's already worked out the kinks an compatibility issues would provide for "free".
Oh yeah, there are several libraries out there. If I decide to come back to it I might use one, though I may just use it as an excuse to learn the details of touch gestures, which I expect would be tedious, but worth doing once for the deep knowledge