#Rectangles

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 Rectangles

Each triangle stores a position, width and height, color, and rotation. These are all used for drawing.

Each triangle also stores a velocity and acceleration, which are integrated to find the position.

Finally, the lifetime variable represents how long the rectangle can exist for. Once this value reaches 0, the rectangle is destroyed.

function Rectangle(x, y, width, height, color, rotation) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height; 
    this.color = color;
    this.rotation = rotation;
    this.lifetime = 100;
    this.vx = 0;
    this.vy = 0;
    this.ax = 0;
    this.ay = 0;
}

We use the following draw function, utilizing transformations to draw rectangles at angles.

function drawRect(rect) {
    ctx.fillStyle = rect.color;

    ctx.save(); // save canvas transformation

    ctx.translate(rect.x, rect.y); // move canvas origin to rect origin
    ctx.rotate(rect.rotation);

    //draw the rect
    ctx.beginPath();
    ctx.rect(-rect.width*0.5, -rect.height*0.5, rect.width, rect.height);
    ctx.fill();
    ctx.strokeStyle="white";
    ctx.stroke();

    ctx.restore(); // restore canvas transformation
}

We create a makeRect function to add rectangles to the canvas.

function hslStr(h, s, l, a) {
    return "hsla("+h+","+s+"%,"+l+"%,"+a+")";
}
...
function makeRect(x, y, rectarr, isRandom) {
    // slowly cycle through hues over time, also keep rotating
    var myColor = hslStr((0.1*getTime()) % 360, 100, 80, 0.5);
    var myRotation = (getTime()*0.002) % (2*Math.PI);

    // a boolean flag that tells us to override the time-based color and use a random color instead
    if(isRandom) {
        myColor = hslStr(Math.random()* 360, 100, 80, 0.5);
    }

    // make the rectangle
    rectarr.push(new Rectangle(x, y, 80, 30, myColor, myRotation));

    // if we've made too many rectangles, get rid of one
    if(rectarr.length>100) {
        rectarr.splice(0, 1);
    }
}

The only odd thing about this function is the use of rectarr in the arguments. Since we want to be able to spawn sources of "rain," we need to have multiple arrays of rectangles. rectarr carries a reference, or pointer, to the particular array that makeRect is pushing to.

Program Architecture

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

Event Handlers

First, we use the following mousemove handler to keep track of the mouse position relative to the canvas, using jQuery to calculate offset.

We also use this function to draw rects that follow the mouse, giving a snake-like effect.

var mouse = {x:0, y:0};
$("#canvas").mousemove(function(event) {
    mouse.x = event.pageX-$(this).offset().left;
    mouse.y = event.pageY-$(this).offset().top;

    makeRect(mouse.x, mouse.y, rects);
});

On click, we want to add a source of "rain" that continuously generates rectangles.

$("#canvas").click(function() {
    rain.push({
        o: {x:mouse.x, y:mouse.y}, // origin
        rects: rects.slice(), // make a copy of the current rect array
        direction: Math.floor(Math.random()*4) // go in a random direction
    });

    for(let i=0; i<rain.length; i++) {
        rain[i].direction=(rain[i].direction+1) % 4;
    }
    if(rain.length>6) rain.splice(0, 1); // have at max 6 rains at a time
});

Animation Loop

We handle the animation loop in 3 parts. First, we update the state. Then, we manage the rect animations. Finally, we manage the rain animations.

In the first part, we track the frames passed since the start of the demo, manage the canvas dimensions, and clear the canvas.

function reDraw() {
    framecount++;

    canv.attr('width',$('body').width());
    canv.attr('height',$('body').height());

    ctx.fillStyle = "#222244";
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    if(framecount % 2 == 0 && started) makeRect(mouse.x, mouse.y, rects);
    for(let i=0; i<rain.length; i++) {
        makeRect(rain[i].o.x, rain[i].o.y, rain[i].rects)
    }
    ...
}

The framecount % 2 line just spawns a rectangle at the mouse position every other frame. We spawn rectangles every other frame, and also on mouse move. This way, more rectangles are spawned if the mouse moves than if it stays still. started is just a boolean flag to indicate whether or not the demo has started - if this is not tracked, then the mouse will initialize in the upper left corner at (0, 0).

Here's the step for managing animating individual rectangles:

function reDraw() {
    ...
    for(let i=0; i<rects.length; i++) {
        drawRect(rects[i]);

        //each rect shrinks a little bit over time (but not too small)
        if(rects[i].width  > 30) rects[i].width*=0.99;
        if(rects[i].height > 1) rects[i].height*=0.99;

        // integrate acceleration and velocity
        rects[i].x+=rects[i].vx; rects[i].y+=rects[i].vy;
        rects[i].vx+=rects[i].ax; rects[i].vy+=rects[i].ay;

        // destroy it if it's too old
        rects[i].lifetime--;
        if(rects[i].lifetime==0) {
          rects.splice(i, 1);
        }

    }
    ...
}

Finally, here's the step for managing each "rain."

function reDraw() {
    ...
    for(let i=0; i<rain.length; i++) {
        const currArr = rain[i].rects;
        for(let j=0; j<currArr.length; j++) {
            drawRect(currArr[j]);

            // integrate velocity and acceleration
            currArr[j].x+=currArr[j].vx; currArr[j].y+=currArr[j].vy;
            currArr[j].vx+=currArr[j].ax; currArr[j].vy+=currArr[j].ay;

            // make it fall in the direction it's supposed to fall in
            if(rain[i].direction == 0) {
                currArr[j].ax= 0;
                currArr[j].ay=Math.random();
            } else if(rain[i].direction == 1) {
                currArr[j].ay=0;
                currArr[j].ax=-Math.random();
            } else if(rain[i].direction == 2){
                currArr[j].ax=0;
                currArr[j].ay=-Math.random();
            } else {
                currArr[j].ay=0;
                currArr[j].ax= Math.random();
            }
        }
    }
}

The rain is really just a constant stream of rectangles that fall in a certain direction. In order to make the falling more interesting and not so uniform, the acceleration is set to Math.random() each frame rather than a constant value.

Initialization

We spawn a bunch of rectangles that quickly disappear, in order to encourage the user to interact with the document.

for(let i=0; i<100; i++) {
  makeRect(Math.random()*canvas.width, Math.random()*canvas.height, rects, true);
}

Final Thoughts

In an upgraded version of this project, I'd like to make each click spawn a snake of rectangles that wanders around the screen.