Today, you're going to make a cherry blossom tree with Javascript.
- object oriented modeling in Javascript
- immediate mode graphics with canvas
- turtle graphics
Let's model a cherry blossom tree from the ground up.
- A tree has a trunk.
- The trunk has length and thickness.
- The trunk grows at some angle to the ground.
- As we follow the trunk upwards, it splits into branches.
- Branches also have length and thickness.
- Branches split off at some angle to their parent branch.
- Some branches have flowers.
Trees also have a root system, of course, but let's not worry about that for now. Branches are also sometimes kindof twisty, but let's also not worry about that for the moment.
Notice a similarity:
- The trunk has height, thickness, angle, and children (which are
Branch
es) - Each
Branch
has length, thickness, angle, and children (alsoBranch
es)
So let's simplify our model and say that the trunk is a Branch
. It still has
length, it just so happens that it's pointing mostly upwards.
Let's also simplify our model and say that each Branch
terminates where it
splits into children. Sometimes the tree has what appears to be one long branch
with several sprouts:
But we'll model that as several Branch
instances:
Write an index.html
with a <canvas>
tag and a <code>
tag in it.
Both should fill the entire window.
Finally, create sakura.js
and link it to the document with a <script>
tag. Also link in
turtle.js
, which we'll use later.
Let's write two classes for the model: Sakura
and Branch
.
Sakura
:
- has a
trunk
instance variable, which is aBranch
. - has a
tick
method—as in the sound a clock makes—which will get called frequently to give the tree a chance to grow and, eventually, draw itself. Thetick
function:- requests another animation frame using
requestAnimationFrame
- and calls
tick
on the tree'strunk
.
- requests another animation frame using
Branch
:
- has
length
,thickness
,angle
, andchildren
instance variables, - has a
tree
instance variable, which is its parentSakura
, - has a
tick
method, which:- grows the branch by a random amount,
- sprouts new children 0.5% of the time,
- and then calls
tick
on all its children.
requestAnimationFrame(func)
asks the browser to call func
the next time a frame is needed. This
is the preferred way to do Javascript animations in modern browsers.
When the browser calls your function from requestAnimationFrame
, this
will be undefined
.
You can fix this by binding this.tick
to this
your Sakura
constructor:
function Sakura() {
// ...
this.tick = this.tick.bind(this);
this.tick(); // start animating
}
Sakura.prototype.tick = function() {
requestAnimationFrame(this.tick);
// ...
};
This line:
this.tick = this.tick.bind(this);
Is a little enigma for you to ponder.
Finally, add some driver code in your HTML page that constructs a Sakura
.
If you put in some console.log
statements, you should be able to open your HTML file and
see that stuff is happening. That's not the greatest test, so let's print some more structured
output.
Write a toString
method for your Sakura
and Branch
classes. Branch
's toString
should
return information about the branch and all its children, recursively.
Next, have the Sakura constructor take a DOM node as an argument. Write a debugPrint
method on Sakura
that sets the textContent
of that node to the value returned
from toString
.
Call debugPrint
from tick
.
Finally, pass a DOM node to Sakura
in your driver code. Give the <code>
tag an id
and find it with
document.getElementById
.
(You could use jQuery, but you won't be doing any other DOM manipulation, so it seems excessive).
When you run your page, you should see some information about all your tree's branches.
Your output might be more readable if you set the CSS property
white-space: pre
for the <code>
element and put newlines ('\n'
) after each branch's output.
If you let your tab run long enough, your computer will start feeling sluggish, and then your tab will crash.
This is because the Sakura grows without bound, and since your branches replicate like bunnies, it's easy to overwhelm your computer.
Fix this by having a maxBranches
instance variable on Sakura
and keeping track of how many
Branch
es have been created. Stop sprouting branches if you have more than a million of them.
To draw our tree, we're going to draw one line for each Branch
. The line's width will be
defined by the Branch
's thickness
, its length will be defined by the Branch
's length,
and its position on the screen will be determined by its angle
with respect to its parent,
and the position of its parent.
Add another argument to Sakura
. This argument will be a
<canvas>
element.
You'll need to set the width
and the height
of the canvas element to be the
canvas
element's clientWidth
and clientHeight
. This is kindof dumb, but yes, you
have to do it.
Also, have your Sakura
get a canvas drawing context with canvas.getContext('2d')
and
save it in a ctx
instance variable. A drawing context is an object with a whole bunch
of drawing functions on it. When you call them, they draw stuff to the canvas.
Add a draw
method to both your Branch
and Sakura
classes, and call it from both their
tick
s.
Sakura
's draw
should clear the screen. That's all.
Branch
's draw
should draw a line on the canvas for the branch. Here's how to draw
a line with a canvas context:
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);
ctx.lineWidth = widthInPixels;
ctx.stroke();
x0
, y0
, x1
, y1
, and widthInPixels
are all values you specify. We know how wide
we want the Branch
to be, but how do we figure out its coordinates?
We can use a turtle to help us.
What's a turtle?
To figure out where to draw the lines this, we're going to use turtle graphics.
Turtle graphics work like this; imagine there's a turtle at point (100, 100) on the canvas:
var t = new Turtle();
t.pos = [100, 100];
t.pos; // -> [100, 100]
t.x; // -> 100
t.y; // -> 100
This doesn't draw anything. In fact, the turtle never draws anything, she can just move around and report her position.
The turtle starts facing straight up, towards the top of the screen:
t.bearing; // -> [0, -1]
[0, -1]
means that if you ask the turtle to move one step, she'll move by 0
pixels
horizontally and -1
pixel vertically.
You don't need to use bearing
directly.
You can just ask the turtle to go forward, and say how far:
t.fwd(5);
t.pos; // -> [100, 95]
Your turtle has now moved five pixels up the screen. Yay! You can also tell the turtle to turn.
t.turnRight(90);
t.fwd(10);
t.pos; // -> [110, 95]
But you can tell the turtle to turn by any angle:
t.turnLeft(30);
t.fwd(5);
t.pos; // -> [114.3301270189222, 92.5]
And that's how we'll figure out the endpoints of the branches on our tree.
One last thing a Turtle
can do is create a copy of itself. It does this with spawn
:
var babyTurtle = t.spawn();
babyTurtle.pos; // -> [114.3301270189222, 92.5]
This will be useful to you.
You'll want to create a new Turtle
for each frame, position it where you want
the base of the tree, and then pass the turtle down from each branch to its children.
You may find that your tree doesn't look exactly how you want. It's randomly generated,
of course, so it may never look exactly how you want, but you can guide how the tree
grows by changing the constants in your Branch
's tick
method. Parameters like
this are known as "tuning parameters"—numbers you can tweak until a program works how you
want it to. If your branches are too stocky, add a bit less thickness
on each frame. If your
tree is growing too thick with branches, make it less likely that a branch will produce a child.
- Given our data model, how might you create the appearance of longer, twistier branches?
- What if a branch's thickness didn't start off as 0?
- Can you replicate different styles of growth?
- What if we wanted the trunk to have special rules governing its growth? How might we architect that?
Just as every cherry tree is beautiful and unique, I leave the implementation of blossoms up to you.
Our tree is the expression of two simple rules: (1) branches grow, and (2) branches branch. It's a fractal—an image that contains instances of itself. In tuning your tree, you might notice that since the rules are applied recursively, slight changes in the rules can produce profound changes in the final result. In this regard, you and your sakura are the same.
Our tree represents a recursive structure as a collection of objects. You might be interested in other ways of representing fractals, such as Lindenmayer systems and iterated function systems.