In part 1 of this series we've seen how to use Python/pyjs with django-mediagenerator. In this part we'll build a ridiculously simple HTML5 canvas drawing app in Python which can run on the iPad, iPhone, and a desktop browser.
Why ridiculously simple? There are a few details you have to keep in mind when writing such an app and I don't want these details to be buried under lots of unrelated code. So, in the end you will be able to draw on a full-screen canvas, but you won't be able to select a different pen color or an eraser tool, for example. These extras are easy to add even for a newbie, so feel free to download the code and make it prettier. It's a nice exercise.
Reset the viewport
With a desktop browser we could start hacking right away, but since we also support mobile browsers we need to fix something, first: One problem you face with mobile browsers is that the actual screen size is not necessarily the same as the reported screen size. In order to work with the real values we have to reset the viewport in the
<head> section of our template:
Now the reported and actual screen size will be the same.
Start touching the canvas
Before we look at mouse-based drawing we first implement touch-based drawing because that has a greater influence on our design. We'll implement a simple widget called
CanvasDraw which takes care of adding the canvas element to the DOM and handling the drawing process:
The last two lines configure the way lines are drawn. We draw lines instead of individual points because when tracking the mouse/finger the individual positions are too far apart and thus need to be connected with lines.
In case you've wondered: The
lineCap property can be
lineJoin property can be
(Note: both images are modifications of images used in a Mozilla tutorial.)
Next, let's add the event handlers and the mouse/finger tracking code. The problem here is that we can't receive any touch movement events while we're drawing something on the canvas. The touch events get lost in this case and the user will experience noticeably slower drawing and bad drawing precision in general. The solution to this problem is to record the touch events in an array and then draw the the lines asynchronously via a timer which gets executed every ca. 25ms and to limit the drawing process to ca. 10ms per timer event. You can fine-tune these numbers, but they worked well enough for us.
CanvasDraw stores the mouse/finger positions in
self.path. Here's the rest of the initialization code:
So far so good. The actual mouse/finger movement paths are represented as a list. The path is ordered such that old entries are at the end and new entries are added to the beginning of the list. When the touch event ends we terminate the path by adding
None to the list, so multiple paths are separated by
None. Here's an example of what
self.path could look like (read from right to left):
OK, let's have a look at the actual touch tracking code. We use only one method for both
touchmove events because they do exactly the same thing:
When the finger leaves the screen we terminate the path with
None. Note that the
touchend event only contains the positions currently being touched, but it's fired after your finger leaves the screen, so the
event.originalEvent.touches property is empty when the last finger leaves the screen. That's why we use
event.originalEvent.changedTouches in order to get the removed touch point:
Now we can implement the actual asynchronous drawing process. Remember, we use a timer to periodically (every 25ms) draw the mouse/finger path in
self.path. We also limit the drawing process to 10ms per timer event. This is our timer:
First we have to call
beginPath() to start a set of drawing instructions, then we tell the
context which lines to draw, and in the end we draw everything with
stroke(). The paths are processed in the
while loop by getting the oldest (last) two points from the path and connecting them with a line. When we reach the path terminator (
pop() it and continue with the next path.
The actual line drawing instructions are handled by
draw_line(). When we receive mouse/touch events the coordinates are relative to the browser window, so the
draw_line() method also converts them to coordinates relative to the canvas:
Finally, we have to instantiate the widget and also prevent scrolling on the rest of our page:
We need a minimum amount of CSS code. Of course, Sass would be nicer and the media generator supports it, but then you'd also have to install Ruby and Sass which makes things unnecessarily complicated for this simple CSS snippet in
The first part just resets margins, so we can fill the whole screen. The last part disables text selection on the iPad and iPhone and also turns off pop-ups like the one that appears when you hold your finger on a link. That's fine for normal websites, but we normally don't want that behavior in a web app.
Of course we also need to add the file to a bundle in
and the bundle must be added to the template:
What about mouse input?
Now that the infrastructure is in place the mouse tracking code is pretty straightforward:
Whew! As you've seen, writing a canvas drawing app with touch support is not difficult, but you need to keep more things in mind than with a mouse-based app. Let's quickly summarize what we've learned:
- Touch events get lost if the drawing instructions block the browser for too long
- Draw the image asynchronously and limit the amount of time spent on drawing
- You have to prevent scrolling in touch events via
event.touchesproperty doesn't contain the removed touch point, so you should use
- jQuery doesn't provide direct access to
event.touches, so you have to use
event.originalEvent.touches(same goes for
- You have to convert mouse/touch coordinates to coordinates relative to the canvas element
- Download the latest sample code to try it out yourself
In the next part we'll make our app offline-capable and allow for installing it like a native app on the iPad and iPhone. You'll also see that django-mediagenerator can help you a lot with making your app offline-capable. Update: Part 3 is published: HTML5 offline manifests with django-mediagenerator