JavaScript Turtle Graphics

Polygon Animation

Introduction

This tutorial will take you through the basics of drawing with turtle graphics through the creation of the motion of stars as seen from a space craft exceeding the speed of light. This is a journey, but like all journeys, it has a series of small steps.

In preparation for the journey, it is assumed that you already have some knowledge of JavaScript and Turtle Graphics. If not, you may want to read or review the following pages before starting here:

On this journey we will visit various turtle graphics functions, creating JavaScript functions to do new things, repeating mechanisms to do the same thing over and over, using coordinate systems to plot things on the screen, using JavaScript objects to simplify parameter passing and processing, and finally animating some polygons.

Another way is to take advantage of the loop control mechanisms that are part of JavaScript. Since this is "real" JavaScript programming, it is the preferred way to go. JavaScript has many looping mechanisms from which to choose including: a while loop, a do...while loop and a for loop. All of these require the following be done:

Coloring the Polygon or Spikey

In the tutorial you learned how to change the color and width of the stroke. We will review that and add a fill function to fill a shape with a color. To change the color of the turtle's pen use use the color() function with a color as in the following example:

function spikey (n, revs, size) { var i = 0 while (i < n) { write (i) forward (size) right (revs*360/n) i = i + 1 } } function demo() { reset() // just clear the screen and home the turtle color("red") spikey(5, 2, 50) }

To change the width of the pen stroke, we use the width function with the width expressed as a number of pixels. A pixel is the size of smallest piece of the display, a dot or maybe a period.

function spikey (n, revs, size) { var i = 0 while (i < n) { write (i) forward (size) right (revs*360/n) i = i + 1 } } function demo () { reset() color("blue") width(5) spikey(5, 2, 50) }

We can even fill in the interior of the polygons or spikeys by using the fill directives of turtle graphics. This directive is a little different than others we have used in that it has to bracket a series of moves with a beginShape() and a fillShape() functions. This lets the program know the boundaries of the shape to fill. Of course the movements between the beginShape() and fillShape() functions should start and end at the same point on the canvas.

So let's give it a try:

function spikey (n, revs, size) { var i = 0 while (i < n) { write (i) forward (size) right (revs*360/n) i = i + 1 } } function demo () { reset() color("black") beginShape() spikey(5, 2, 50) fillShape("red") }

The stroke color, the color left by the turtle's pen, and the fill color can be different.

You may have noticed that the fill color goes over the top of pen stroke and it overlaps about half of the stroke line. To make the width of the stroke as you intended it, you need to re-stroke the graphic to get it on top of the fill.

function spikey (n, revs, size) { var i = 0 while (i < n) { write (i) forward (size) right (revs*360/n) i = i + 1 } } function demo () { reset() width(1) color("white") beginShape() spikey(5, 2, 50) fillShape("blue") width(2) color("white") spikey(5, 2, 50) }

Positioning the Polygon or Spikey

We want to start with the spikey() function that was discussed in the Turtle Graphics Tutorial. We don't have to always start at the center of the canvas. We can start anywhere on the canvas. We can use the basic turtle moves like the following code to move the figure:

function spikey (n, revs, size) { var i = 0 while (i < n) { write (i) forward (size) right (revs*360/n) i = i + 1 } } function demo() { reset() forward (100) right(90) forward( 100) left(90) spikey( 5, 2, 40) }

That leaves a line of where the turtle traveled. We can prevent the turtle from marking by having it lift the pen as in the following code.

function spikey (n, revs, size) { var i = 0 while (i < n) { write (i) forward (size) right (revs*360/n) i = i + 1 } } function demo() { reset() penup() forward (100) right(90) forward( 100) left(90) pendown() spikey( 5, 2, 40) }

But it still isn't centered about a specific point. It would be nice if it were centered because spikies of different sizes could be place concentrically and the points of the spike can be reoriented without appearing to move the spikey, sort of like the spokes of a bicycle wheel. To center the two spikies takes a little geometry and trigonometry. So if you don't understand the following calculations, don't worry, this will be the only time that the math will be this difficult in this tutorial. Consulting the figure on the right we find that:

These calculations can be used to center a spikey. The size of the spikey can now be the radius of a circle that would go around the spikey. The code for a spikey that uses a central point and a radius instead of a side to determine size in the following code. Math.cos() is the cosine function from the JavaScript Math library. The beauty of the cosine function is that it expresses the ratio of the side adjacent to the angle to the hypotenuse (the long side of the right triagle as a function of the angle. Since the JavaScript implementation of the cosine function uses radians instead of degrees, the function degToRad() is used to make the proper conversion. There are 2 pi radians in a circle and there are 360 in the same circle. So this conversion finds the fraction of a circle (number of degrees divided by the number of degrees in a cirles), and multiples that fraction by the number of radian in a circle (2 pi).

This also uses the goto() function to move the turtle to an x, y position on the canvas Cartesian coordinate system. It also uses the setheading() function to orient the spikey. Although the spikey will be drawn with a point straight up, it could be set to any angle.

function spikey ( points, revs, radius, x, y) { penup() goto(x, y) setheading(0) forward(radius) var turnAngle = 360 * revs/points var angleA = ( 180 - turnAngle)/2 var stroke = 2 * radius * Math.cos( degToRad( angleA)) right( 180 - angleA) pendown() for( var i = 0; i < points; i = i + 1) { //> forward( stroke) right( turnAngle) } } function demo () { reset() hideTurtle() for (rad = 40; rad <=100; rad = rad + 10) { spikey( 5, 2, rad, -10, 10) } }

Since there are a lot of changes to the spikey() function all at once, let's stop to explain what is going on. First up, the spikey function has different parameters. The points and revs parameters have the same purpose as before, that is to limit the scope of the variables to within the spikey function. The side parameter has been replaced with a radius parameter. Both specify the size of the spikey, but in different ways. radius specifies the radius of a circle that would go around all of the points of the spikey. So spikeys with different number of points or different number of revs would still be the same size as each other. The new parameters x and y specify the center point of the spikey.

As you can see when the code runs, there are seven perfectly centered and aligned five-pointed stars.

Using Named Parameters and Arguments

The spikey() function has three parameters: number of points, number of revolutions and length of side. But even with just three parameters, it starts to get confusing as to what the order of the parameters are. JavaScript has a mechanism to help with that, although it is not direct. To review, JavaScript has objects. For instance a car object may be

car = { make: "Volkswagen", model: "bug", year: 1967, color: "red" }

The make, model, year and color are attributes of the car. You can have other cars with other attributes. To access an attribute you use the dot notation with the object.attribute as in:

car1 = { make: "Volkswagen", model: "bug", year: 1967, color: "red" } car2 = { make: "Ford", model: "Mustang", year: 2008, color: "blue" } paint = car1.color // paint is red paint = car2.color // paint is blue

A function can use an object as its only parameter and then break out the individual attributes. When the function is invoked, one must use the object notation in the argument. So taking the familiar, by now, spikey() function and changing it for object notation:

function spikey ( args ) { for( var i=0; i < args.points; i = i+1) { //> forward (args.side) right( 360*args.revs/args.points) } } function demo() { reset(); spikey({points:5, revs:2, side:50}) left(180) spikey({side:50, revs:2, points:5}) // order doesn't matter. }

This was for the old simple spikey function. Let's say you want a spikey function that does what a spikey does, but now you want to add a starting point, a starting angle, a stroke width and color and a fill color. That's a lot to remember just in itself, but then you also have to remember the parameter order. Naming the parameters reduces that complexity.

This function also know whether to re-stroke the spikey or not. This is a case where a simple conditional provides some functionality and saves the user of the function having to call it twice.

function spikey ( args ) { goto( args.x, args.y) setheading( args.heading) forward( args.radius) var turnAngle = 360 * args.revs/args.points var angleA = (180 - turnAngle)/2 var stroke = 2 * args.radius * Math.cos(degToRad(angleA)) right(180-angleA) pendown() color( args.color) width( args.width) beginShape() for( var i = 0; i < args.points; i = i + 1) { //> forward( stroke) right( turnAngle) } fillShape( args.fill) } function demo () { reset() hideTurtle() spikey({ points:5, revs:2, radius:15, x:0, y:0, heading:0, color:"red", width:3, fill:"white" }) spikey({ x:0, y:30, radius: 10, heading:180, side:50, points:5, revs:2, color:"blue", fill:"yellow" }) spikey({ x:30, y:10, radius: 10, heading:180-72, side:50, points:5, revs:2, color:"blue", fill:"yellow" }) spikey({ x:20, y:-28, radius: 10, heading:-36, side:50, points:5, revs:2, stroke:"blue", fill:"yellow" }) spikey({ x:-20, y:-28, radius: 10, heading:36, side:50, points:5, revs:2, stroke:"blue", fill:"yellow" }) spikey({ x:-30, y:10, radius: 10, heading:180-72, side:50, points:5, revs:2, color:"blue", fill:"yellow" }) }

Default Parameters

So now we don't need to worry about the order of the parameters, but what happens if we leave one out. As it stands, if an attribute is not supplied, JavaScript will stop with an error because that attribute is undefined. 'undefined' is a special value any variable can have. The easiest thing to do is to set all undefined attributes with a default value as shown below. Note that even arguments which probably should have been supplied are furnished with a default here. It may be a better idea to let an argument like side remain undefined, so that the function is called properly. The following is a code segment to handle default values

// fill out default values for undefined attributes if (args.x === undefined) args.x = 0 if (args.y === undefined) args.y = 0 if (args.radius === undefined) args.radius = 10 // shouldn't default if (args.heading === undefined) args.heading = 0 if (args.color === undefined) args.color = "black" if (args.fill === undefined) args.fill = "white" if (args.width === undefined) args.width = 1 if (args.revs === undefined) args.revs = 1 // garden variety polygon if (args.points === undefined) args.points = 5 // shouldn't default

Polygon Animation

Now we can play around a bit with animation. Animation is done by drawing a series of pictures or frames. Each frame is slightly different than the preceding frame. If the frames are played at the right speed the human eye blurs the frames together to form motion. Let's start with simply twisting a simple star.

function spikey ( args ) { goto( args.x, args.y) setheading( args.heading) forward( args.radius) var turnAngle = 360 * args.revs/args.points var angleA = (180 - turnAngle)/2 var stroke = 2 * args.radius * Math.cos(degToRad(angleA)) right(180-angleA) pendown() color( args.color) width( args.width) beginShape() for( var i = 0; i < args.points; i = i + 1) { //> forward( stroke) right( turnAngle) } fillShape( args.fill) } // GLOBALS var twistAngle // FUNCTIONS function twistStar() { reset() hideTurtle() spikey({ points:5, revs:2, radius:40, x:0, y:0, heading:twistAngle, color:"red", width:3, fill:"white" }) twistAngle = twistAngle + 2 delay( twistStar, 30) } function demo () { reset() hideTurtle() twistAngle = 0 twistStar() }

An improvement can be made to this code. The object defining the star can be made a stand alone definition. This allows the program to be maintained in a more organized fashion: the definitions allow the characteristics of the star to be defined separately from the code that does the action. This code is shown below. Note the two sections of the code: GLOBALS and FUNCTIONS. The variables defined as they are in the GLOBALS section are global and may be accessed by any function. ("// GLOBALS" is a comment that is just a signal to the programmer that this is the beginning of a section.) The name "deltaRotation" is the way that an engineer or physisist would talk about change in rotation.

// GLOBALS var star1 = { points:5, revs:2, radius:40, x:0, y:0, heading:0, color:"red", width:3, fill:"white" } var deltaRotation = 2 // number of degrees of twist between frames var frameDelay = 30 // milliseconds between frames // FUNCTIONS function spikey ( args ) { // the simpler version for brevity goto( args.x, args.y) setheading( args.heading) forward( args.radius) var turnAngle = 360 * args.revs/args.points var angleA = (180 - turnAngle)/2 var stroke = 2 * args.radius * Math.cos(degToRad(angleA)) right(180-angleA) pendown() color( args.color) width( args.width) beginShape() for( var i = 0; i < args.points; i = i + 1) { //> forward( stroke) right( turnAngle) } fillShape( args.fill) } function twistStar() { clear() spikey(star1) star1.heading = star1.heading + deltaRotation delay( twistStar, frameDelay) } function demo () { reset() hideTurtle() twistStar() }

Now let's move the star about. So instead of changing the rotation, we will change its x and y position. These are independent so you can move the star in any direction including backward when using a negative change or delta.

// GLOBALS var star1 = { points:5, revs:2, radius:40, x:0, y:0, heading:0, color:"red", width:3, fill:"white" } var deltaX = 2 // pixels per frame in x direction var deltaY = 2 // pixels per frame in y direction var frameDelay = 30 // milliseconds between frames // FUNCTIONS function spikey ( args ) { // the simpler version for brevity goto( args.x, args.y) setheading( args.heading) forward( args.radius) var turnAngle = 360 * args.revs/args.points var angleA = (180 - turnAngle)/2 var stroke = 2 * args.radius * Math.cos(degToRad(angleA)) right(180-angleA) pendown() color( args.color) width( args.width) beginShape() for( var i = 0; i < args.points; i = i + 1) { //> forward( stroke) right( turnAngle) } fillShape( args.fill) } function moveStar() { clear() spikey(star1) star1.x = star1.x + deltaX star1.y = star1.y + deltaY delay( moveStar, frameDelay) } function demo () { reset() hideTurtle() wrap(false) // fix edge condition moveStar() }

Now let's grow the star over time. We do that by adding a delta to the radius for each frame.

// GLOBALS var star1 = { points:5, revs:2, radius:40, x:0, y:0, heading:0, color:"red", width:3, fill:"white" } var deltaRadius = 2 // pixels per frame in x direction var frameDelay = 30 // milliseconds between frames // FUNCTIONS function spikey ( args ) { // the simpler version for brevity goto( args.x, args.y) setheading( args.heading) forward( args.radius) var turnAngle = 360 * args.revs/args.points var angleA = (180 - turnAngle)/2 var stroke = 2 * args.radius * Math.cos(degToRad(angleA)) right(180-angleA) pendown() color( args.color) width( args.width) beginShape() for( var i = 0; i < args.points; i = i + 1) { //> forward( stroke) right( turnAngle) } fillShape( args.fill) } function growStar() { clear() spikey(star1) star1.radius = star1.radius + deltaRadius delay( growStar, frameDelay) } function demo () { reset() hideTurtle() wrap(false) // fix edge condition growStar() }

Now let's combine all three and move, grow, and twist it at the same time.

// GLOBALS var star1 = { points:5, revs:2, radius:40, x:0, y:0, heading:0, color:"red", width:3, fill:"white" } var deltaX = 2 // change in pixels per frame in x direction var deltaY = 2 // change in pixels per frame in y direction var deltaHeading = 2 // change in heading per frame var deltaRadius = 2 // change in radius per frame var frameDelay = 30 // milliseconds between frames // FUNCTIONS function spikey ( args ) { // the simpler version for brevity goto( args.x, args.y) setheading( args.heading) forward( args.radius) var turnAngle = 360 * args.revs/args.points var angleA = (180 - turnAngle)/2 var stroke = 2 * args.radius * Math.cos(degToRad(angleA)) right(180-angleA) pendown() color( args.color) width( args.width) beginShape() for( var i = 0; i < args.points; i = i + 1) { //> forward( stroke) right( turnAngle) } fillShape( args.fill) } function changeStar() { clear() spikey(star1) star1.x = star1.x + deltaX star1.y = star1.y + deltaY star1.heading = star1.heading + deltaHeading star1.radius = star1.radius + deltaRadius delay( changeStar, frameDelay) } function demo () { reset() hideTurtle() wrap(false) // fix edge condition changeStar() }

This code could be improved a small bit. The deltas can be part of the other attributes of the star. This way if there were more than one star, they would have separate speeds. While we are at it, let's also shorten "delta" to just plain "d." To make it interesting, we will also add a second star.

// GLOBALS var star1 = { points:5, revs:2, radius:40, x:0, y:0, heading:0, color:"red", width:3, fill:"white", dx: 2, dy: 2, dhead: 2, drad: 2 } var star2 = { points:7, revs:3, radius:40, x:-100, y:0, heading:0, color:"black", width:3, fill:"blue", dx: 4, dy: 2, dhead: 6, drad: 3 } var frameDelay = 30 // milliseconds between frames // FUNCTIONS function spikey ( args ) { // the simpler version for brevity goto( args.x, args.y) setheading( args.heading) forward( args.radius) var turnAngle = 360 * args.revs/args.points var angleA = (180 - turnAngle)/2 var stroke = 2 * args.radius * Math.cos(degToRad(angleA)) right(180-angleA) pendown() color( args.color) width( args.width) beginShape() for( var i = 0; i < args.points; i = i + 1) { //> forward( stroke) right( turnAngle) } fillShape( args.fill) } function changeStars() { clear() spikey(star1) star1.x = star1.x + star1.dx star1.y = star1.y + star1.dy star1.heading = star1.heading + star1.dhead star1.radius = star1.radius + star1.drad spikey(star2) star2.x = star2.x + star2.dx star2.y = star2.y + star2.dy star2.heading = star2.heading + star2.dhead star2.radius = star2.radius + star2.drad delay( changeStars, frameDelay) } function demo () { reset() hideTurtle() wrap(false) // fix edge condition changeStars() }

Now let's add acceleration to the mix. Acceleration just changes the speed parameters a bit over time. Acceleration is the feeling you get when speeding up. Negative acceleration or deceleration is the feeling you get when slowing down. Since acceleration is just a change in speed we will abbreviate acceleration as "dd..." We can add acceleration to any speed attribute, so we will add variables ddx, ddx, ddy, ddhead, and dddrad to add acceleration attributes for x, y, heading and radius respectively.

// GLOBALS var star1 = { points:5, revs:2, radius:40, x:0, y:0, heading:0, color:"red", width:3, fill:"white", dx: 2, dy: 2, dhead: 2, drad: 2, ddx: .2, ddy: .2, ddhead: .2, ddrad: .2 } var star2 = { points:7, revs:3, radius:40, x:-100, y:0, heading:0, color:"black", width:3, fill:"blue", dx: 4, dy: 2, dhead: 6, drad: 3, ddx: -.4, ddy: .1, ddhead: -.3, ddrad: -.2 } var frameDelay = 30 // milliseconds between frames // FUNCTIONS function spikey ( args ) { // the simpler version for brevity goto( args.x, args.y) setheading( args.heading) forward( args.radius) var turnAngle = 360 * args.revs/args.points var angleA = (180 - turnAngle)/2 var stroke = 2 * args.radius * Math.cos(degToRad(angleA)) right(180-angleA) pendown() color( args.color) width( args.width) beginShape() for( var i = 0; i < args.points; i = i + 1) { //> forward( stroke) right( turnAngle) } fillShape( args.fill) } function changeStar() { clear() spikey(star1) star1.x = star1.x + star1.dx star1.y = star1.y + star1.dy star1.heading = star1.heading + star1.dhead star1.radius = star1.radius + star1.drad star1.dx = star1.dx + star1.ddx star1.dy = star1.dy + star1.ddy star1.dhead = star1.dhead + star1.ddhead star1.drad = star1.drad + star1.ddrad spikey(star2) star2.x = star2.x + star2.dx star2.y = star2.y + star2.dy star2.heading = star2.heading + star2.dhead star2.radius = star2.radius + star2.drad star2.dx = star2.dx + star2.ddx star2.dy = star2.dy + star2.ddy star2.dhead = star2.dhead + star2.ddhead star2.drad = star2.drad + star2.ddrad delay( changeStar, frameDelay) } function demo () { reset() hideTurtle() wrap(false) // fix edge condition changeStar() }

So we have done motion and acceleration on two stars. Now let's multiple stars. If the stars were defined as an array, the individual processing for each star would be the same (except for the array member number). There should be some variation in the size, color and position of each star. The stars should appear to be moving from the center of the canvas straight toward the edge.

// GLOBALS var frameDelay = 30 // milliseconds between frames var polys = [] var revs = new Array (0,0,0,0,0, 2,2,3,3,4,4,5) // FUNCTIONS function generatePolys () { var x var y var points for (i=0; i < 10; i++) { //> x = random (-100, 100) y = random (-100, 100) points = random(5, 11) polys.push ({ points: points, revs: revs[ points], radius: random( 1,6)/2, color: logoColors[ random(15)], fill: logoColors[ random(15)], width: 1, heading: random(360), x: x, y: y, drad: 1, dhead: 1, dx: Math.sign(x), dy: Math.sign(y) * Math.abs(y/x), ddrad: random(1,6)/20, ddhead: .2, ddx: .4 * Math.sign(x), ddy: .2 * Math.sign(y) * Math.abs(y/x) }) } } function spikey ( args ) { // the simpler version for brevity goto( args.x, args.y) setheading( args.heading) forward( args.radius) var turnAngle = 360 * args.revs/args.points var angleA = (180 - turnAngle)/2 var stroke = 2 * args.radius * Math.cos(degToRad(angleA)) right(180-angleA) pendown() color( args.color) width( args.width) beginShape() for( var i = 0; i < args.points; i = i + 1) { //> forward( stroke) right( turnAngle) } fillShape( args.fill) } function changeStars() { clear() for (i=0; i < 10; i++) { //> spikey(polys[i]) polys[i].x = polys[i].x + polys[i].dx polys[i].y = polys[i].y + polys[i].dy polys[i].heading = polys[i].heading + polys[i].dhead polys[i].radius = polys[i].radius + polys[i].drad polys[i].dx = polys[i].dx + polys[i].ddx polys[i].dy = polys[i].dy + polys[i].ddy polys[i].dhead = polys[i].dhead + polys[i].ddhead polys[i].drad = polys[i].drad + polys[i].ddrad } delay( changeStars, frameDelay) } function demo () { reset() hideTurtle() wrap(false) // fix edge condition generatePolys() changeStars() }

That was cool, but the animation stopped way too soon. We will now add an incubator to create new baby stars, so that the animation lasts longer. This is done by modifying the generatePolys() function to take an argument for the number of stars to generate, and then calling it from the changeStars() function.

// GLOBALS var frameDelay = 30 // milliseconds between frames var polys = [] // FUNCTIONS function generatePolys (count) { var x var y var points console.log(count) for (i=0; i < count; i++) { //> x = random (-100, 100) y = random (-100, 100) points = random(5, 11) polys.push ({ points: 5, revs: 2, radius: random( 1,6)/2, color: logoColors[ random(15)], fill: logoColors[ random(15)], width: 1, heading: random(360), x: x, y: y, drad: 1, dhead: 1, dx: Math.sign(x), dy: Math.sign(y) * Math.abs(y/x), ddrad: random(1,6)/60, ddhead: .2, ddx: .4 * Math.sign(x), ddy: .2 * Math.sign(y) * Math.abs(y/x) }) } } function spikey ( args ) { // the simpler version for brevity goto( args.x, args.y) setheading( args.heading) forward( args.radius) var turnAngle = 360 * args.revs/args.points var angleA = (180 - turnAngle)/2 var stroke = 2 * args.radius * Math.cos(degToRad(angleA)) right(180-angleA) pendown() color( args.color) width( args.width) beginShape() for( var i = 0; i < args.points; i = i + 1) { //> forward( stroke) right( turnAngle) } fillShape( args.fill) } function changeStars() { clear() for (i=0; i < polys.length; i++) { //> spikey(polys[i]) polys[i].x = polys[i].x + polys[i].dx polys[i].y = polys[i].y + polys[i].dy polys[i].heading = polys[i].heading + polys[i].dhead polys[i].radius = polys[i].radius + polys[i].drad polys[i].dx = polys[i].dx + polys[i].ddx polys[i].dy = polys[i].dy + polys[i].ddy polys[i].dhead = polys[i].dhead + polys[i].ddhead polys[i].drad = polys[i].drad + polys[i].ddrad } generatePolys(1) delay( changeStars, frameDelay) } function demo () { reset() hideTurtle() wrap(false) // fix edge condition generatePolys(10) changeStars() }

That works for a while, but it is consuming memory by continually adding to the polys array. Even though stars are no longer on the canvas they are using memory and are being recomputed each frame. The motion will get slower and slower over time.

One way to correct the problem is to delete elements from the polys array when that element is off the canvas. The edges of the canvas are given by the Turtle Graphics functions minX(), maxX(), minY(), and maxY(). But remember its not just when the center of the star passes by the edge. but the extremities of the star. All of the arms of the star are within the radius of the star, so that needs to be added to the dimension of the canvas. Array.splice(start, count) is method for chopping out members of an array. This has two arguments start, the element where the deletion should start, and count, the number of elements to delete. Since this deletes elements and causes the length of the array to change, we need to do this from the end of the array and move toward its beginning.

// GLOBALS var frameDelay = 30 // milliseconds between frames var polys = [] // FUNCTIONS function generatePolys (count) { var x var y var points console.log(count) for (i=0; i < count; i++) { //> x = random (-100, 100) y = random (-100, 100) points = random(5, 11) polys.push ({ points: 5, revs: 2, radius: random( 1,6)/2, color: logoColors[ random(15)], fill: logoColors[ random(15)], width: 1, heading: random(360), x: x, y: y, drad: 1, dhead: 1, dx: Math.sign(x), dy: Math.sign(y) * Math.abs(y/x), ddrad: random(1,6)/60, ddhead: .2, ddx: .4 * Math.sign(x), ddy: .2 * Math.sign(y) * Math.abs(y/x) }) } } function spikey ( args ) { // the simpler version for brevity goto( args.x, args.y) setheading( args.heading) forward( args.radius) var turnAngle = 360 * args.revs/args.points var angleA = (180 - turnAngle)/2 var stroke = 2 * args.radius * Math.cos(degToRad(angleA)) right(180-angleA) pendown() color( args.color) width( args.width) beginShape() for( var i = 0; i < args.points; i = i + 1) { //> forward( stroke) right( turnAngle) } fillShape( args.fill) } function killPolys() { for (i=polys.length-2; i >= 0; i = i - 1) { //> if (polys[i].x -polys[i].radius > maxX() || polys[i].x + polys[i].radius < minX() || polys[i].y - polys[i].radius > maxY() || polys[i].x + polys[i].radius < minX()) { console.log("killing " + i + " " + polys[i].x + " " + polys[i].y + " " + polys.length) polys.splice(i,1) } } } function changeStars() { clear() for (i=0; i < polys.length; i++) { //> spikey(polys[i]) polys[i].x = polys[i].x + polys[i].dx polys[i].y = polys[i].y + polys[i].dy polys[i].heading = polys[i].heading + polys[i].dhead polys[i].radius = polys[i].radius + polys[i].drad polys[i].dx = polys[i].dx + polys[i].ddx polys[i].dy = polys[i].dy + polys[i].ddy polys[i].dhead = polys[i].dhead + polys[i].ddhead polys[i].drad = polys[i].drad + polys[i].ddrad } killPolys() generatePolys(1) delay( changeStars, frameDelay) } function demo () { reset() hideTurtle() wrap(false) // fix edge condition generatePolys(10) changeStars() } Objects, Constructor, and Methods

JavaScript is an Object Oriented language. We have used objects in the previous example, but we have yet to make the code object oriented. This next step creates the objects using a constructor. A constructor is a special function that sets the attributes of an object. It uses the special key word "this" that refers to the object being created by the constructor.

function Spikey ( points, revs, radius) { this.points = points this.revs = revs this.radius = radius )

The constructor is invoked to build an object as in the following:

star = new Spikey( 5, 2, 40)

Objects also have methods. A method is a function that is tied to the object. Other objects can have methods with the same name and an object may have many methods. The following shows the definition of a Spikey Class with a grow() method.

function Spikey ( points, revs, radius) { this.points = points this.revs = revs this.radius = radius this.grow = function (delta) { this.radius = this.radius + delta } )

The method is invoked as in the following code.

star = new Spikey( 5, 2, 40) // construct a new Spikey named "star" star.grow(2) // invoke the grow method on the Spikey named "star"

Another way to add a method to a constructor is to use the "prototype" keyword. The definition using the prototype keyword is as if it were part of the original constructor, so the keyword "this" still refers to the object being created.

function Spikey ( points, revs, radius) { this.points = points this.revs = revs this.radius = radius } Spikey.prototype.grow = function (delta) { this.radius = this.radius + delta }

Rewriting the final animation example using object orientation becomes something like the following code.

// CONSTRUCTORS function Spikey ( ) { // set to default value initially this.points = 5 this.revs = 2 this.radius = 10 this.color = "black" this.fill = "white" this.width = 1 this.heading = 0 this.x = 0 this.y = 0 this.drad = 0 this.dhead = 0 this.dx = 0 this.dy = 0 this.ddrad = 0 this.ddhead = 0 this.ddx = 0 this.ddy = 0 } Spikey.prototype.grow = function () { this.radius = this.radius + this.drad } Spikey.prototype.move = function () { this.x = this.x + this.dx this.y = this.y + this.dy } Spikey.prototype.twist = function () { this.heading = this.heading + this.dhead } Spikey.prototype.accelerate = function () { this.dx = this.dx + this.ddx this.dy = this.dy + this.ddy this.dhead = this.dhead + this.ddhead this.drad = this.drad + this.ddrad } Spikey.prototype.draw = function () { // the simpler version for brevity goto( this.x, this.y) setheading( this.heading) forward( this.radius) var turnAngle = 360 * this.revs/this.points var angleA = (180 - turnAngle)/2 var stroke = 2 * this.radius * Math.cos(degToRad(angleA)) right(180-angleA) pendown() color( this.color) width( this.width) beginShape() for( var i = 0; i < this.points; i = i + 1) { //> forward( stroke) right( turnAngle) } fillShape( this.fill) } // GLOBALS var frameDelay = 30 // milliseconds between frames var polys = [] // FUNCTIONS function generatePolys (count) { var x var y var points console.log(count) for (i=0; i < count; i++) { //> x = random (-100, 100) y = random (-100, 100) points = random(5, 11) polys.push( new Spikey()) var last = polys.length -1 polys[last].points = 5 polys[last].revs = 2 polys[last].radius = random( 1,6)/2 polys[last].color = logoColors[ random(15)] polys[last].fill = logoColors[ random(15)] polys[last].width = 1 polys[last].heading = random(360) polys[last].x = x polys[last].y = y polys[last].drad = 1 polys[last].dhead = 1 polys[last].dx = Math.sign(x) polys[last].dy = Math.sign(y) * Math.abs(y/x) polys[last].ddrad = random(1,6)/60 polys[last].ddhead = .2 polys[last].ddx = .4 * Math.sign(x) polys[last].ddy = .2 * Math.sign(y) * Math.abs(y/x) } } function killPolys() { for (i=polys.length-2; i >= 0; i = i - 1) { //> if (polys[i].x -polys[i].radius > maxX() || polys[i].x + polys[i].radius < minX() || polys[i].y - polys[i].radius > maxY() || polys[i].x + polys[i].radius < minX()) { console.log("killing " + i + " " + polys[i].x + " " + polys[i].y + " " + polys.length) polys.splice(i,1) } } } function changeStars() { clear() for (i=0; i < polys.length; i++) { //> polys[i].draw() polys[i].move() polys[i].twist() polys[i].grow() polys[i].accelerate() } killPolys() generatePolys(1) delay( changeStars, frameDelay) } function demo () { reset() hideTurtle() wrap(false) // fix edge condition generatePolys(10) changeStars() }