Curve Stitching: An Ode to Canvas

David Ng
Vertical Learning
Published in
8 min readJan 28, 2017

--

My niece recently re-introduced me to curve stitching. We were discussing the randomly-generated animated cityscape I had drawn on canvas using JavaScript when her mom said, “Why don’t you show Uncle Dave those cool drawings you’ve been doing in math?” My niece pulled out a straightedge and, moments later, she produced a parabola. “Very cool!” I said, beaming.

To draw a parabola, start with two lines at a 90˚ angle marked at even intervals.

I remembered drawing parabolas myself when I was her age. But I’d never ventured beyond the few basic curves I could draw using a sheet of graph paper as my template. I think it was my perfectionist tendencies. By using graph paper, I could ensure the marks on my lines were evenly spaced, but I was restricted to lines at a 90˚ angle, never mind circles.

We can draw many other curves if we start with lines at other angles marked at even intervals.
We can draw even more curves if we start with a circle marked at even intervals.

Once I had discovered some of the amazing curves I could produce, I was determined to explore curve stitching more fully this second time around. But how to draw my templates? I still didn’t want to create my templates by drawing angled lines or circles by hand. I wanted something more precise than that. The solution I came up with is to draw my templates on canvas using JavaScript.

A canvas is a standard HTML element that we can add to a web page and then draw onto using JavaScript. As a programmer, I wanted to make the process of setting up a template as easy as possible. I started by writing the constructor function for a line in a template:

var Line = function(x0, y0, x1, y1, markCount, transform) {
this.points = [];
this.transform = transform;
for (var i = 0; i < markCount; i++) {
var x = x0 + (x1 - x0) * i / (markCount - 1);
var y = y0 + (y1 - y0) * i / (markCount - 1);
this.points.push({x: x, y: y});
}
}

Now, to create a new line in a template, all I have to do is type:

var lineA = new Line(0,0, 0,400, 21);

This automatically creates a new instance of a line from (0, 0) to (0, 400) with 21 marks on it. The coordinates of the 21 marks are stored in the line’s points array. The line also has a method called drawTemplate() that I can call to draw the line and its 21 marks on the canvas:

this.drawTemplate = function() {
var p = this.points;
ctx.save();
if (this.transform !== undefined) this.transform();
ctx.beginPath();
ctx.moveTo(p[0].x, p[0].y);
ctx.lineTo(p[p.length - 1].x, p[p.length - 1].y);
ctx.stroke();
for (var i = 0; i < p.length; i++) {
ctx.beginPath();
ctx.arc(p[i].x, p[i].y, 5, 0, 2 * Math.PI, false);
ctx.fill();
}
ctx.restore();
};

With this infrastructure in place, I can create the template for a 3-pointed star in a few lines of code and with very little math:

var a = 2 * Math.PI / 3;
var lineA = new Line(0,0, 0,400, 21);
var lineB = new Line(0,0, 0,400, 21, function() {ctx.rotate(a)});
var lineC = new Line(0,0, 0,400, 21, function() {ctx.rotate(2*a)});
lineA.drawTemplate();
lineB.drawTemplate();
lineC.drawTemplate();
A 3-pointed star

You may have noticed that I’m not actually calculating the coordinates for the endpoints of the two angled lines in the template. In fact, all three lines start at (0, 0) and end at (0, 400)! Instead of using trigonometry to position the endpoints, I’m actually rotating the coordinate system of the canvas by 120˚ to draw lineB and 240˚ to draw lineC. The only math I had to do is divide 360˚ (2 * Math.PI) by three to get the angle.

Once I had an easy way to draw my templates, I immediately challenged myself to take my infrastructure one step further and draw the curve. That was surprisingly straightforward:

for (var i = 0; i < markCount; i++) {
ctx.beginPath();
lineA.moveTo(i);
lineB.lineTo(markCount - 1 - i);
lineB.moveTo(i);
lineC.lineTo(markCount - 1 - i);
lineC.moveTo(i);
lineA.lineTo(markCount - 1 - i);
ctx.stroke();
}

To draw lines connecting the marks on one line to the corresponding marks on another line, I added two new methods to the line constructor: moveTo() and lineTo(). On the surface, these two methods are very simple:

this.moveTo = function(i) {
ctx.save();
if (this.transform !== undefined) this.transform();
ctx.moveTo(this.points[i].x, this.points[i].y);
ctx.restore();
};

If the endpoints of the line are defined in a transformed coordinate system, we apply the transformation and then either move or draw a line to the point stored in the line’s points array at the index i. However, if we think about that for a moment, we realize that, when drawing lines between marks, we are starting the line in one coordinate system (say the coordinate system for lineA) and ending it in a completely different coordinate system (say the coordinate system for lineB). That’s crazy!

Use this program to draw your own star.

Transformations are one of the things I love best about drawing on canvas. While many of us remember transformations as strange and convoluted operations from math class (don’t get me started on matrices!), they can be surprisingly practical in real life. In all of the drawings you see on this page, I only had to do a bit of basic trigonometry for the flower drawings because it was far easier to use transformations.

Computational thinking

If I get a chance to play around with curve stitching on the computer some more, I want to add two more tools to my infrastructure. First, I want to be able to create time-lapse drawings where you can see the curve take shape as more and more lines are added in. I’m fairly sure I have a good strategy for implementing that feature in my head. Second, I want to be able to color in my drawings. That feature is going to take a lot of thinking to work out. I imagine I’ll color in a few drawings manually at first, and then slowly try to generalize from there. This is the same process I used to generalize drawing curves like stars, flowers, rings, epicycloids, and nautiluses.

Use this program to draw your own flower.

For me, this has been a great exercise in computational thinking. There are parts of the curve stitching process I enjoy. I find drawing the lines connecting the marks to be very zen, but I find trying to create the templates stressful and irritating. Since a computer is far better at drawing angled lines and circles, and then dividing them into equal intervals, than I am, it makes sense to turn that part of the process over to the computer.

When I then went ahead and programmed the computer to draw the lines that form the actual curve (the part of the process I enjoy doing for myself), it was because I wanted to see if I could do it. Could I apply the full set of skills and understanding I had acquired over the past few years to automate the drawing process efficiently? Taking on the challenge and proving I could do it (even learning one new thing along the way) was deeply satisfying.

Growing as a programmer

My first introduction to programming was the computer science course all freshman at Carnegie Mellon had to take. It used Pascal and one of the last concepts we learned were linked lists. From there, I learned BASIC and a few scripting languages, such as ActionScript. One of my favorite tools was mTropolis.

About six years ago, when I started Vertical Learning Labs, I shifted most of my focus to web development and started learning JavaScript. I also started learning Objective-C about two years ago, which was a real challenge since I have zero background in C or any of its variants and am entirely self-taught.

Use this program to draw your own ring.

There are a few skills and concepts which I used to build out my curve-stitching infrastructure that I want to highlight. I started using objects with methods about two years ago—and only because I wanted to put several simulations on the same web page and I didn’t want variable and function names to collide. Storing a simulation in an object, along with its variables and functions, was my way of implementing namespaces!

I distinctly remember the first time writing a function that accepted another function as an argument. I wanted to create a drawToppings() function that would draw pepperoni if I passed it the drawPepperoni() function and green peppers if I passed it the drawGreenPepper() function. That was a year ago. I don’t remember why I started using constructors to create objects, but it also happened about a year ago.

Finally, I used apply() for the first time a few months ago when I wanted to pass an array of values into Math.min() and Math.max(), and I used bind() for the first time in preparing this article. I had hoped to use a closure instead for the first time (a closure seems more elegant), but I couldn’t get it to work right away and I’m on a tight deadline.

Inspiring and supporting programmers

Why am I telling you this? It sounds like I acquired new skills as I needed them, but that’s not the case. Creating the drawToppings() function was not the first time it would have helped to pass one function into another. It’s just that, in the past, I would have either settled for an inferior solution or taken my program in a different direction to avoid the problem. Being aware of callback functions, seeing other people use them, and needing them myself was not enough for me to dig in and actually learn how to use one. It took a couple of years before I felt comfortable enough to try.

As much as I’ve learned over the past few years, I would have learned a lot more a lot faster if I had been part of a community of programmers and not just learning on my own. We need people to inspire, guide, and support us as we stretch and take risks. It’s with that goal in mind that I’ve been partnering with Jared Cosulich of The Puzzle School in developing Drawing In Code, a set of resources for learning JavaScript by drawing on canvas. My hope is we will create the conditions for a variety of learners (both kids and adults) to learn how to program and apply computational thinking.

Use this program to draw your own epicycloid.

Would I have wanted to program a computer to draw curves with straight lines back when I was my niece’s age? In hindsight, I would have if I thought I could, but I didn’t because I thought I couldn’t. That’s not right. I don’t want my niece censoring her interests based on what she thinks she can or cannot do. We also need to stop censoring what we make available to kids. I was at the Thinking about Thinking about Seymour Papert symposium yesterday and Alan Kay spoke about the importance of introducing kids to powerful ideas. Transformations are a powerful idea. Self-initializing objects are another powerful idea. I believe kids can understand and would eagerly apply these ideas if we sowed the right seeds.

Seymour Papert used to say that the only way to learn about learning is by learning ourselves. He was a risk-taker and a true lifelong learner. I can’t be an effective educator unless I’m learning, too. That’s another powerful idea.

Use this program to draw your own nautilus.

--

--

David Ng
Vertical Learning

Founder and Chief Learning Officer of Vertical Learning Labs