#Triangle Flowers

The goal here is to use simple geometry to create visually stimulating patterns. The link to this demo is found here.

The source code is found on the Github repo.

Modeling Triangles

Prototype

We model each "flower" as a ring of isoceles triangles. Each triangle has a height, side length (the non-identical side), and rotation.

Here's our triangle prototype:

Triangle = function(x, y, altitude, sidelength, color, rotation) {
    this.x = x;
    this.y = y;
    this.altitude = altitude;
    this.sidelength = sidelength; 
    this.color = color;
    this.rotation = rotation;
    this.lifetime=0;
    this.clicking = true;
    //each differential is a funciton of lifetime and clicking
    this.da = (a, b) => 0;
    this.ds = (a, b) => 0;
    this.dr = (a, b) => 0;
    this.dead = false;

    this.kill = function() {
        this.dead = true;
        this.da = (a, b) => (-1);
        this.ds = (a, b) => (-1);
    }
}

We start off by setting a number of basic attributes. (x, y) is the origin of the triangle. From this point, rotate by rotation radians, then go altitude pixels out and sidelength/2 pixels across. lifetime tracks the number of frames since the creation of the triangle. clicking tracks if the triangle is part of the currently held down group of triangles. We'll get to the other functions later.

This is our triangle drawing function:

drawTri = function(tri) {
    ctx.fillStyle = tri.color; // set fill color

    ctx.save(); // save canvas state
    ctx.translate(tri.x, tri.y); // start drawing from here
    ctx.rotate(tri.rotation); // start facing this direction
    ctx.beginPath();
    // make a triangle between these three points
    ctx.moveTo(0, 0);
    ctx.lineTo(tri.sidelength/2, tri.altitude);
    ctx.lineTo(-tri.sidelength/2, tri.altitude);
    ctx.lineTo(0, 0);
    ctx.fill(); // draw the tri
    ctx.restore(); // restore canvas state
}

Animation

This demo really just uses one basic concept of animation and movement: the derivative. Each primary drawing attribute (height, side length, and rotation) has a corresponding derivative function. Each derivative is a function of triangle lifetime and a boolean clicking variable. clicking is just true on mousedown and false after mouseup. In other words, each attribute is a piecewise function of lifetime, getting bigger until the mouse is released, at which point it integrates the derivative function.

At some point in our draw function, we run:

for(let i=0; i<triangles.length; i++) {
    tri.altitude+=tri.da(tri.lifetime, tri.clicking);
    tri.sidelength+=tri.ds(tri.lifetime, tri.clicking);
    tri.rotation+=tri.dr(tri.lifetime, tri.clicking);
}

The diversity in the triangles of the program just result from defining a variety of derivative functions.

drs = [
(scale) => ((time, click) => { 
    // the "click" variable makes the triangle expand while the mouse is held down at init
    if(click) return scale*(0.01);
    else return scale*(0.02+0.01*Math.sin(0.1*time));
}),
...
];
// the set of all possible altitude derivatives
das = [
(scale, scale2) => ((time, click) => {
    if(click) return 2*scale2;
    else return scale*(1*Math.sin(0.05*time));
}),
...
];
// the set of all possible side length derivatives
dss = [
(scale, scale2) => ((time, click) => {
    if(click) return 1*scale2;
    else return scale*(1.5*Math.sin(0.05*time));
}),
(scale, scale2) => ((time, click) => {
    if(click) return 0.1*scale2;
    else return scale*(1.5*Math.sin(0.05*time));
}),
...
];

Each function in the drs, das, dss arrays is a generator for a derivative function. They each classify a type of derivative. For instance, drs[0] classifies a type of trigonometric derivative. To obtain a usable derivative function, we call drs[0](scale) and use the return value. Modeling the function this way allows us to generate a wide variety of derivative functions by using random scale parameters.

Life Cycle

We don't want the triangles to stay around forever. To manage them, we use a lifetime variable that tracks how long they've been around for. We add the following attribute and function to the prototype:

function Triangle(...) {
    ...
    this.dead = false;
    this.kill = function() {
        this.dead = true;
        this.da = (a, b) => (-1);
        this.ds = (a, b) => (-1);
    }
}

Then, in the animation loop:

if(tri.lifetime==tri.maxLife) {
    triangles[i].kill();
}
if(tri.dead && tri.altitude<5 ) {
    triangles.splice(i, 1);
}

The kill function initiates the death process by making the size derivatives -1, so the triangle shrinks. Once the triangle has reached below a certain size, it is spliced from the array and destroyed.

Program Architecture

As with most interactive demos, we're going to have an initialization, an animation loop, and an event handler.

Initialization

Here's the initialization code:

triangles = [];

Yep, it turns out there isn't really any initialization needed in this demo. However, for the purposes of usability and to make the demo more interesting, I add some triangles that are immediately spawned at the end of their life cycle on initialization. This should give an idea of what the demo does and encourage people to click around.

function makeDyingBloom(x, y) {
    let side = 30+Math.random()*60;
    let drf = drs[0](1);
    let daf = (side, click) => (-2);
    let dsf = (side, click) => (-2);

    let color = hslStr(Math.random()*360, 80, 50, 0.7);
    for(let i=0; i<10; i++) {
        let newtri = new Triangle(x, y, 150, 150, color, (i/10)*2*Math.PI);
        newtri.dr= drf;
        newtri.da= daf;
        newtri.ds= dsf;
        newtri.maxLife = 30;
        triangles.push(newtri);
    }
}
for(var i=0; i<10; i++) {
    makeDyingBloom(Math.random()*canvas.width,Math.random()*canvas.height);    
}

The above code is pretty straightforward, and just populates the canvas with a bunch of dying blooms at the outset of the program.

Animation Loop

Here's our animation loop:

canvasDraw = function() {
    // match container dimensions to drawing context
    canv.attr('width',$('#canvas').width());
    canv.attr('height',$('#canvas').height());

    // reset background
    ctx.fillStyle = "#112233";
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // handle triangles
    for(let i=0; i<triangles.length; i++) {
        const tri = triangles[i]; // for readability
        drawTri(tri); // draw it onto the canvas

        // now integrate
        tri.altitude+=tri.da(tri.lifetime, tri.clicking);
        tri.sidelength+=tri.ds(tri.lifetime, tri.clicking);
        tri.rotation+=tri.dr(tri.lifetime, tri.clicking);

        // add to the lifetime
        tri.lifetime++;

        // handle lifecycle
        if(tri.lifetime==tri.maxLife) {
            tri.kill();
        }
        if(tri.dead && tri.altitude<5 ) {
            triangles.splice(i, 1);
        }
    }
}

Let's focus in particular on the step where we handle each triangle. For each triangle, we first draw it to the canvas. Then, we integrate our three drawing parameters. Finally, we manage the lifecycle by increasing the lifetime variable, then killing the triangle if the lifetime variable exceeds a certain value.

Event Listener

Here's our mousemove listener code:

mouse = {x:0, y:0}; 
// always update the position of the mouse (in canvas coordinates, not global)
$("#canvas").mousemove(function(event) { 
    mouse.x = event.pageX-$(this).offset().left;
    mouse.y = event.pageY-$(this).offset().top;
});

This is used to track the position of the mouse relative to the canvas.

Here's the click listener, the bulk of our program:

// just an easy helper function: 50% chance to return 1/-1
randomSign = () => (Math.random()>0.5) ? 1 : -1; 

// define click event - we want to make one bloom on each click
$("#canvas").mousedown(function() {
    // total number of petals from 3 to 12
    var petals = Math.floor(Math.random()*10)+3;

    // set up an initial side length and color
    let side = 30+Math.random()*60;
    let color = hslStr(Math.random()*360, 80, 50, 0.7);
    // select a random derivative function for the three drawing variables, also using random scale parameters
    let drf = drs[Math.floor(Math.random()*drs.length)](1*(0.5+Math.random())*randomSign());
    let daf = das[Math.floor(Math.random()*das.length)](1*(0.5+Math.random())*randomSign(), 0.5+Math.random());
    let dsf = dss[Math.floor(Math.random()*dss.length)](1*(0.5+Math.random())*randomSign(), 0.5+Math.random());

    // random (10%) chance to add a pointy one
    if(Math.random()<0.1) {side = 2; dsf = (time, click) => 0; petals = 20+Math.floor(Math.random()*20)}

    // now add the triangles to the array - note that each triangle in each bloom has the same derivative functions
    for(let i=0; i<petals; i++) {
        let newtri = new Triangle(mouse.x, mouse.y, 0, side, color, (i/petals)*2*Math.PI);
        newtri.dr = drf;
        newtri.da = daf;
        newtri.ds = dsf;
        newtri.maxLife = 600;
        triangles.push(newtri);
    }
}).mouseup(function() {
    for(var i=0; i<triangles.length; i++) {
        triangles[i].clicking = false;
    }
});

We start by generating a total number of blooms, from 3 to 12. Then, we set up an initial side length and color: we only generate these two because the rotation doesn't matter (they'll be rotating a lot anyway) and altitude will increase as we hold the mouse down.

Now, we select a random derivative function for each of the three drawing parameters. First, we select a random generator. For instance, drs[Math.floor(Math.random()*drs.length)] returns us not a rotation derivative, but a class of rotation derivatives. Then, we input a random value for the scale parameter, giving us a usable function.

We also add a random chance to create a pointy triangle, which has a very short side length, just for fun.

Finally, we populate the triangles array with all the petals. In each bloom, every petal is identical except for the initial rotation, giving a cohesive effect to each bloom. The initial rotation is just regularly spaced around a circle.

Final Thoughts

Things to do: