Chapter 14 of Coding Curves
This is the last planned chapter of this series. I might add another one here or there in the future if I find a new interesting curve to write about. There were also a couple of topics on my original list which I decided to hold back. I might change my mind about them someday. Any future additions will be added to the index.
For this “final” installment, I thought I’d cover a few random curves that probably wouldn’t be worth a full chapter in themselves. And I thought it would be good to kind of walk through the process I actually take when I go to code up some formula I discover.
The Cannabis Curve
Wolfram Mathworld is a great place to find interesting formulas by the way. If you want to dig up more 2d curves, the section on Plane Curves is a great place to dive in. But there’s all kinds of other stuff on the site too.
I’m not trying to make any statement by choosing the cannabis curve. I just thought it was pretty cool that you could draw something complex like that with relatively simple math.
So we get this as a formula:
OK, it’s a bit long, but it’s just multiplication, addition and some sines and cosines. We can do this.
This is defined as a polar curve, which means rather than defining x, y values, we’ll be dealing with an angle and a radius. We have a function, r(θ)
, where θ is the Greek letter, theta. This usually represents an angle. And we can guess that r
stands for radius. So we have a function where we pass in an angle and get a radius.
With an angle and a radius, we can easily get an x, y point to draw a segment to. This might look sort of like this:
for (t = 0; t < 2 * PI; t += 0.01) {
radius = r(t)
x = cos(t) * radius
y = sin(t) * radius
lineTo(x, y)
}
stroke()
We use t
to get the radius
and then radius
and t
to get the next point to draw a line to.
Practically speaking though, I’ll never use that r(θ)
function anywhere but in this for loop, so I’ll just hard code it all right there.
The only other thing we see here that isn’t a number or a bracket or a trig formula or θ, is the variable a
. We’re multiplying the whole rest of the formula by a
to get the final radius for a given t
, so it seems like a
will just represent the overall radius we want this curve to be drawn at. So a
will probably be a good parameter to pass into our cannabis function, and I’ll probably rename that parameter radius
for clarity’s sake. We’ll also probably want a center x, y point to locate the curve, so we’ll make those parameters too (xc
and yc
for x and y center).
We come up with something like this for starters
function cannabis(xc, yc, radius) {
for (t = 0; t < 2 * PI; t += 0.01) {
r = radius * ... // that whole formula. we'll get to it.
x = cos(t) * r
y = sin(t) * r
lineTo(xc + x, yc + y)
}
closePath()
}
Now we just need to code up all the stuff that comes after a
. This is really pretty simple now that we’ve figured out how it’s all going to fit together. For the fractional constants, I’ll just use decimals: 0.1 instead of 1/10 and 0.9 instead of 9/10. Let’s go!
function cannabis(xc, yc, radius) {
for (t = 0; t < 2 * PI; t += 0.01) {
r = radius * (1 + 0.9 * cos(8 * t)) * (1 + 0.1 * cos(24 * t)) * (0.9 + 0.1 * cos(200 * t)) * (1 + sin(t))
x = cos(t) * r
y = sin(t) * r
lineTo(xc + x, yc + y)
}
closePath()
}
Now I’ll try to run this by putting up something like so:
canvas(600, 600)
cannabis(300, 300, 140)
stroke()
That gives me this image:
Ah, OK. This tells us a few things.
First, this formula is using Cartesian coordinates and I’m using upside-down screen coordinates. So I’ll have to flip the y-axis. No problem.
Next, the center is where all the “leaves” join. So after flipping, I can probably set the center more towards the bottom of the canvas.
Finally, I guessed that 140 would be a good value for radius, as it would keep it well within the 600×600 size of the canvas. In fact, I expected it would only be about half the size of the canvas. But we actually go well beyond the canvas edge for the larger leaves. We could correct that in the code, maybe multiplying radius
by some fraction to bring the largest leaf down to the radius the user passed in. I’m going to skip that part and just pass in a smaller value, but feel free to do what you want with the function.
Here’s my final version:
function cannabis(xc, yc, radius) {
for (t = 0; t < 2 * PI; t += 0.01) {
r = radius * (1 + 0.9 * cos(8 * t)) * (1 + 0.1 * cos(24 * t)) * (0.9 + 0.1 * cos(200 * t)) * (1 + sin(t))
x = cos(t) * r
y = sin(t) * r
lineTo(xc + x, yc - y)
}
closePath()
}
All I did was change the lineTo
function to use yc - y
instead of yc + y
.
Then when I call it, I just change the parameters a bit (with a bit of trial and error to get it just right):
canvas(600, 600)
cannabis(300, 520, 120)
stroke()
There we go!
Side note. I seriously considered changing the size of the canvas so that the yc
parameter could be 420. You’ll either get that or you won’t. 🙂
Of course, now I’m curious about what that formula is doing. There’s basically four parts to it after the a
, each in parentheses – three with cosines, one with sine. The first one has a hard coded 8 in there.
... (1 + 0.9 * cos(8 * t)) ...
Since there are seven visible leaves there, I’m guessing those are related – there’s probably actually eight leaves, but the bottom one is too small to see. I’ll change that 8 to a 12…
Yup, theory validated. Eleven leaves plus an invisible one.
In the second section, the 24 is a bit less obvious.
... (1 + 0.1 * cos(24 * t)) ...
If I revert back to original and then change the 24 to 0, I get very rounded leaves.
Doubling 24 to 48 gives us:
It’s kind of making three levels for each leaf. Let’s put it back to 24 and change the multiplier:
... (0.7 + 0.3 * cos(24 * t)) ...
Again, we see three levels, which make sense, as 24 = 8 * 3. So this section is using a very small multiplier, 0.1, to make a subtle change to each leaf – making it just a bit less round. Cool. We’ll revert that and look at the next one.
... (0.9 + 0.1 * cos(200 * t)) ...
The 200 there makes me guess it’s creating all the jagged edges. If I change it to 100, it’s less jagged.
But now I’m seeing some blockiness. I can try increasing the resolution by changing the for loop increment from 0.01 to 0.005:
Mmmm… smooth.
I’ll revert that and look at the final sine block.
... (1 + sin(t))
My guess was that this was affecting the orientation of the curve. I thought that if I removed that section, the leaf would be sideways. But I was wrong. Here’s what happens if I remove that section and set yc
back to 300, the center of the canvas:
That was a pleasant surprise! And it gives me a lot of ideas of new curves I can create from this. Also, this makes that missing eighth leaf visible!
A Heart Curve
Again, Wolfram Mathworld to the rescue. As you can see, there is no single formula to draw a heart shaped curve. This page shows eight different ones. Personally I like the last one in the second row.
Rather than a polar formula like last time (and some of these other examples), this just has us computing the x and y directly. But we’re still going to loop a t
value from 0 to 2 * PI.
For the y part of the formula, there are four different calculations. It’s not entirely clear how these calculations are supposed to be combined, but if you look further down in the text, you find that you’re supposed to subtract them. I’m also a bit concerned that we have so many hard coded numbers in there and no way to change the final size of the heart. But I’m sure we can figure that out.
Once again, this is really a pretty straightforward formula, so let’s jump in and code it up.
function heart(xc, yc) {
for (t = 0; t < 2 * PI; t += 0.01) {
x = 16 * pow(sin(t), 3)
y = 13 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(4 * t)
lineTo(xc + x, yc + y)
}
closePath()
}
We can run this like so:
canvas(600, 600)
heart(300, 300)
stroke()
And we’ll get:
Generally right, but we need to flip it like before, and we need to allow for changing the size. Right now it’s about 32 pixels wide. That’s the hard coded 16, times 2.
For flipping we can again just say yc - y
.
For size, let’s first get rid of all of those hard-coded numbers by dividing them all by 16.
x = pow(sin(t), 3)
y = 0.8125 * cos(t) - 0.3125 * cos(2 * t) - 0.125 * cos(3 * t) - 0.0625 * cos(4 * t)
If we left it like that, we’d get a heart that would be two pixels wide (1 * 2). Now we can add a size
parameter and multiply both values by that.
function heart(xc, yc, size) {
for (t = 0; t < 2 * PI; t += 0.01) {
x = size * pow(sin(t), 3)
y = size * (0.8125 * cos(t) - 0.3125 * cos(2 * t) - 0.125 * cos(3 * t) - 0.0625 * cos(4 * t))
lineTo(xc + x, yc - y)
}
closePath()
}
Now we call it like so:
canvas(600, 600)
heart(300, 300, 280)
stroke()
And get:
Not too difficult.
I’m not going to dive in to all the different ways you can mess with this, but just try changing the various constants in the formula and see what happens. Can you make it better? Come up with something completely different?
An Egg
I first looked into drawing egg shapes a couple of years ago. This post showed the result, but none of the thought process behind it: https://www.bit-101.com/blog/2021/06/how-to-draw-an-egg/
The page I got my formula from was here: http://www.mathematische-basteleien.de/eggcurves.htm
There are actually a truckload of different egg formulas on that page. Like the heart curve, I was surprised that there was no single standard egg curve formula.
But I homed in on the one on that page under the section “From the Oval to the Egg Shape”. This takes a general formula for an oval or ellipse and alters the “y radius” based on the current value at each point. If x is to the right of center, it will make the y value a little bit bigger. If x is to the left, it will make the y value a bit smaller. That seemed sensible.
So we’ll start with an ellipse formula. We covered that back in Chapter 3.
function ellipse(x, y, rx, ry) {
res = 4.0 / max(rx, ry)
for (t = 0; t < 2 * PI; t += res) {
lineTo(x + cos(t) * rx, y + sin(t) * ry)
}
closePath()
}
That’s nice and concise, but I’m gonna break it up a bit so we can mess with the raw x and y coords right out of the trig functions before translating and scaling them. I’m also going to ignore the res
variable and just hard code the 0.01 in there. Just for simplicity and clarity. Keep it if you want.
function egg(xc, yc, rx, ry) {
for (t = 0; t < 2 * PI; t += 0.01) {
x = cos(t)
y = sin(t)
lineTo(xc + x * rx, yc + y * ry)
}
closePath()
}
This will just draw an ellipse, but let’s make sure it works with the changes.
canvas(600, 600)
egg(300, 300, 280, 190)
stroke()
Yup, that’s an ellipse. Where did I get the 280 and 190 from? Well, 280 is a bit less than half of the width of the canvas, so that’s the rx
. And I wanted ry
to be somewhat less than that. It was just trial and error and 190 looked about right.
Now let’s make this ellipse into an egg. The article gives three formulas:
t1(x) = 1 + 0.2 * x
t2(x) = 1 / (1 - 0.2 * x)
t3(x) = e^(0.2 * x)
Those t
functions just give us something we need to multiply the y
value by. I’m not going to make new functions though. I’m just going to do the multiplication right in the for loop. Let’s try the first one…
function egg(xc, yc, rx, ry) {
for (t = 0; t < 2 * PI; t += 0.01) {
x = cos(t)
y = sin(t)
y *= (1 + 0.2 * x)
lineTo(xc + x * rx, yc + y * ry)
}
closePath()
}
Woo! An egg!
Now we can mess with that a bit. The 0.2 value there is probably going to be the best source of experimentation. Let’s try 0.3.
OK, it got a bit pointy. How about 0.5?
Even pointier. So we know where that’s going. Let’s go down. To 0.1.
That’s barely distinguishable from the original ellipse. Which makes sense, because if that value was 0, than that line would do nothing and we’d be back to an ellipse. Let’s go back to 0.2, which probably did look the most egg-like, and try changing the yr
value we’re passing in. We’ve been using 190. Here’s 220:
A nice fat egg. And 150:
I’m gonna stand by my choice of 190, but some minor tweaks might work better. Go for it. Let’s try the other formulas. We’ll go back to the ry
of 190 first. Then change that line 5 to:
y *= 1 / (1 - 0.2*x)
This gives us:
And then the third formula:
y *= exp(0.2 * x)
Remember from Chapter 5 that most math libraries have an exp
function that is e
raised to a given power. That’s what we are doing here. The result of that:
These all look suspiciously the same, so I drew them all at once, red, green and blue…
Yeah, those are all virtually the same. Maybe a pixel off here and there. Looking back at the original web site, they are talking about a different ellipse formula and multiplying y2 by that value:
The equation of the ellipse e.g. x²/9+y²/4=1 change to x²/9+y²/4*t(x)=1.
This is also hard coding 9 and 4 as divisors. If you unsquare those you get 3 and 2. And it just so happens that 2/3 of 280 is 186ish. So we pretty much agreed on my choice of 190 for ry
!
At any rate, we have a formula that draws a fairly convincing egg no matter which algorithm we use. So I’m going to leave it there. Here I was literally writing the article as I was writing the code, so you really got to see my process, along with my glossing over of details in the source article. But it all came out just fine!
Summary
Hopefully this gives you some insight on how to find formulas in various places and convert them into code that draws something interesting – if you haven’t already done stuff like this.
And that wraps up this series on coding curves. At least for now. I have another series in mind though, so watch this space!
Mentions