Tuesday, December 25, 2012

Pufftygraph, an HTML5 Spirograph with touch-driven gears

First, thank you, now defunct Kenner, for the most excellent Spirograph toy. I had a Kenner 401 Spirograph set as a boy, and I fondly remember the joy of trying to drive the gears with a pen, and watching as the hidden shapes were revealed.

About a year back, I did some searches for online spirograph apps, thinking little Scout, then 6 years old, would enjoy it. I found some nice sites talking about how hypotrochoid math works, a complete listing of the gear ratios of one of the Kenner 401 set with close-up images of each gear, and several Flash, java, and HTML5 apps to draw hypotrochoids with given gear ratio inputs. However, nowhere did I find an app that showed the gears move as you drew, which as far as I was concerned was the main draw.

I decided that was a perfect excuse to brush up on my HTML5 animation skills, and within a few days I had code that would draw actual gears that followed mouse movements or touch gestures, drawing a hypotrochoid in its wake. I added the necessary elements to let it function as a smartphone web app, posted it here, and showed it to my daughter Scout, who thought it was a fun enough idle amusement for a minute or two, but since it could only make the one shape, didn't have much replay value.

I shelved the code and moved onto other things, but late this year I returned to the app and spruced it up some, and presented it to Scout under the title Pufftygraph ("Puff" is one of her nicknames) as one of her official Christmas presents. She and her mom enjoyed playing with it while Christmas cookies were in the oven, and it seemed to be a big hit with both of them. Here's an example of a drawing it can produce now:

...and here's a video of me demonstrating it. (I intended to show a webcam video of this live on an actual iPhone, but my webcam is in dire need of being upgraded, so this is me using the app on a laptop with a screen capture program.)

The web app is available to play with here: etchapps.appspot.com/pg.html... and here's that splash-screen, in case it went by too fast:

That's no overstatement; Scout is the best big sister I could ever have hoped baby Adelaide could have, and she's definitely worth the effort it took to create the app.

To be clear, what I have written is a hypotrochoid generator that is unaffiliated with Hasbro, Kenner, Denys Fisher, or the trademarked Spirograph product line. My use of the word "Spirograph" is equal parts a throwback to my generation's habit of adopting popular brand names in place of generic terms (e.g., Coke and Kleenex instead of soda and tissue) and the fact that "hypotrochoid generator" sounds flat and mathy, where "spirograph" sounds fun and nostalgic.

Mathy stuff

The first problem I had to solve was simply how to draw a gear. I decided to make the teeth isosceles trapezoids, where the base and height were the same, and the top line would be half the width of the base:

The teeth would always be a fixed size, regardless of the other gear properties, which allows the teeth of the inner gear (the smaller gear that turns) to visually "fit into" the outer ring (the larger gear that stays in a fixed place):

So to draw a gear, take a circle, find its edge, draw a tooth on top of it, rotate a fraction, and repeat. The fraction changes with the number of teeth you want on the gear, and the circle's radius adjusts to make the gears fit comfortably side by side. For the inner gears that have holes on them, compare an official Spirograph gear with what my algorithm creates with the same number of teeth and holes:

Similar. The physical gear appears to be a spiral where the holes are more or less lined up with the "spokes" on the gear, which leads to some unevenness in appearance as you get close to the edge. I kept the holes a fixed distance away from each other, and evenly spread from the gear's center to edge. This was accomplished by creating increasingly large circles from the gear's center, and fixed-size circles at the previous hole's center. Where the two circles intersected was the location of the next hole.

In my app, everything is drawn on the canvas except for the actual gears, which are separate image elements rotated with a CSS rule for "webkit-transform", for example:

gearImage.style['-webkit-transform'] = "rotate(1.7rad)";

My first build had the gears also drawn on the canvas directly, but the overhead of constant updates to the canvas' transformation matrix and redrawing the gear shapes after each movement was far less efficient than setting the image's position and rotation CSS elements and letting the browser handle the redraw on its own. Although I get a lot of joy out of figuring out how to do things by rote, in some cases micromanaging works against you.

Moving the gears

There are two parts to consider when the user drags the gear, namely what position the screen touch means, and how much to rotate the gear to get it there.

I decided first that touches must be inside the inner gear (which is intuitive, that's the thing you're trying to move, after all) and all the action would be clockwise. After that, position is a function of finding the angle inside the outer ring that corresponded to where the touch happened. This is straightforward enough, just take the arctangent of the touch after setting the origin to the outer ring's center, and you have the radian value of the touch.

The next part is less intuitive, namely figuring out how much to rotate the gear. Some basic math is performed to figure out what outer ring tooth corresponds to the touch event, then how many teeth away from the current position that is. Once the new ring and gear tooth numbers are determined, rotation works like this:

var rotation = ringPos * ring.step - gearPos * gear.step;

The "ring.step" and "gear.step" constants are the radian values of turning one tooth. If the gear has 63 teeth, gear.step is 1/63rd of a circle. In the CSS rotation rule, positive numbers are clockwise rotations, and negative numbers are counter-clockwise. So, as the gear's position moves clockwise around the circle, its rotation is counter-clockwise.

Consider this example, a 63-cog gear moving through all 63 teeth on a 96-cog outer ring:

The gear has rotated counter-clockwise a full circle, and if it were moving across a straight line, it would be rotated back to where it started. But it's not rotating along a straight line, it's rotating around a circle. So while it has gone counter-clockwise by 63 of its own teeth, it has gone clockwise by 63 of the outer gear's 96 teeth, or about 2/3rds of a circle.

Drawing the hypotrochoid

When the ring, gear, and pen hole are all set, I build each position in the hypotrochoid ahead of time, before any drawing happens, and slap it into an array. When the user draws with the gears, I look up the location of each position the gears move through, and just ctx.lineTo them. Here's the code for building the positions, followed by a simpler summary:

  function buildPositions() {
    var offset = gear.radius - ring.radius;
    var hole = gear.holes[gear.activeHole];

    for (var n = 0; n < ring.cogs; n++) {
      var rotation = (n % ring.cogs) * ring.step;
      var x = (-Math.sin(rotation) * offset);
      var y = (Math.cos(rotation) * offset);
      ringPositions.push([x, y]);
    // Least common multiple, which I define earlier in the code
    var lcm = Math.lcm(ring.cogs, gear.cogs);
    for (var n = 0; n < lcm; n++) {
      var ringPos = n % ring.cogs;
      var gearPos = n % gear.cogs;
      var rotation = ringPos * ring.step - gearPos * gear.step;
      var sin = Math.sin(rotation);
      var cos = Math.cos(rotation)
      var x = (cos * hole[0] - sin * hole[1]) + ringPositions[ringPos][0] + centerX;
      var y = (sin * hole[0] + cos * hole[1]) + ringPositions[ringPos][1] + centerY;
      gearPositions.push([x, y, rotation]);

The first loop, iterating through ring.cogs, creates a circle that follows the inner gear's center as it moves around the outer ring. The second loop, iterating through all the possible gear positions until the first teeth of the gear and ring are touching again, follows the first circle, and then adds to it the x and y of the active pen hole at the gear's current rotation. This is similar to the official hypotrochiod formula shown on Wolfram, but I arrived at my method independently, just trying to follow the pen holes as they moved around.

The position building is all done before any drawing happens. It may seem a inefficient to pre-calculate all the positions and stick them in an array, but it's not as bad as you might think. The majority of gear/ring combinations comes in at under 1000 possible positions, and the largest was only 6,720, where the 105-tooth ring and 64-tooth gear are married, the largest co-prime pairing.

It may also seem ill-advised to calculate where a pen hole from a separate image element should track through on the canvas, but if the math works, it works. This example, as well as the 4 screenshots of the 63/96 gears above, shows that the math does indeed work.

No comments:

Post a Comment