GLC with external tools

With the release of the image sequence export feature, I thought I’d play around a bit with making different animations with the frame outputs to see what quality and file size I could come up with.

I started with an existing animation and exported an animated gif from that. Then exported an image sequence from the same animation and used ImageMagick to create another animated gif, and ffmpeg to create an mp4 video. Here are the results.

First, the GLC gif output:

GLC Direct Output - 473kb

That comes in at 473kb and looks decent.

Then the ImageMagick version.

ImageMagick Output - 326kb

That’s 326kb. Looks about the same to me, but significantly smaller. Actually, my initial attempts were horrible. The IM version came in larger than GLC’s output and had all kinds of horrible artifacts. Some research found some command line options that cleaned it up and optimized it nicely. Here’s the command I used to create this image:

convert -delay 3.33 -loop 0 -fuzz 2% -layers Optimize *.png isogrid_im.gif

Note, delay is in terms of 100ths of a second, fuzz cleans up the quantization and -layers Optimize brought the file size way down.

Now that I look at it closer, there’s still a bit of artifacting going on. Raising the fuzz level a bit would probably handle that though.

Onto ffmpeg.

That’s down to a whopping 50kb. But it does look a bit fuzzy. I’m not even going to begin to explain the ffmpeg command line I used, because I hardly understand it myself.

ffmpeg.exe -framerate 30 -i isogrid_%0
4d.png -c:v libx264 -r 30 -pix_fmt yuv420p isogrid.mp4

Here’s the page I gleaned that info from: https://trac.ffmpeg.org/wiki/Create%20a%20video%20slideshow%20from%20images

There are probably options that would improve the quality at the expense of some file size. Another interesting concept is the idea of including audio in the mp4. or creating multiple animations and stitching them together in a longer movie. Of course, you could just pull all the files into some video editor and do the same thing that way.

I was going to try gifsicle as well, but that requires individual gifs as input. Won’t take pngs.

Anyway, there’s some things to try. I’d love to hear any additional tools or techniques you come up with.

I had to turn off comments here. 10,000 to 1 spam to comment ratio. But tweet with the tag #gifloopcoder and I’ll see it.

Version 1.0.3: Isobox

Late last night I released Version 1.0.3 of GIF Loop Coder. The only major change is the addition of a new shape: the Isobox. This is simply an isometric box. OK, technically, it’s a dimetric box.

You can give it a screen x and y, a size and an ‘h’ for height. The size property is the width from one corner of the box to the other. The h is how far up the box extends from its base. So an h of greater than 0 will create a box of some kind, an h of 0 will create a tile, and a negative h will actually create a hole!

boxes

The colors are specified by the properties colorTop, colorLeft and colorRight. A great way to assign colors to these is with the color.hsv method. Use the same hue and saturation for all three, with varying values. Generally, the lightest value will be on the top and two darker values on the left and right, although you can change this up to create different lighting effects.


colorTop: color.hsv(90, 1, 1),
colorRight: color.hsv(90, 1, 0.75),
colorLeft: color.hsv(90, 1, 0.5),

colorboxes

Of course, all properties are animatable. Here, I’m animating the h and colors.

animcube

This is a great case for the color.animHSV method.


colorTop: color.animHSV(0, 60, 1, 1, 1, 1),
colorRight: color.animHSV(0, 60, 1, 1, 0.75, 0.75),
colorLeft: color.animHSV(0, 60, 1, 1, 0.5, 0.5),

The following code is extracted and simplified from the isometricgrid.js file in the examples folder, and shows how to make a grid of boxes.

    var tileWidth = 60,
        tileHeight = tileWidth / 2;
    
    for(var y = 0; y < 5; y++) {
        for(var x = 0; x < 5; x++) {
            var xpos = width / 2 + (x - y) * tileWidth / 2,
                ypos = 100 + (x + y) * tileHeight / 2,
                hue = 30 + Math.random() * 30;
            list.addIsobox({
                x: xpos,
                y: ypos,
                size: tileWidth,
                h: 40,
                colorTop: color.hsv(hue, 1, 1),        
                colorLeft: color.hsv(hue, 1, 0.5),
                colorRight: color.hsv(hue, 1, 0.75),
            });
        }
    }

cubegrid

Object Variables

There’s a feature that was added to GLC a while back, which I realize I never properly explained.

When you create a shape, you pass in an object that contains various pre-defined properties that define how the shape is drawn. But, you can actually add other properties to that object as well – any property you want. Then, if you have a function defining a property, you can access that custom property by use of the keyword, this. For example:

list.addCircle({
centerX: width / 2,
centerY: height / 2,
x: function(t) {
return this.centerX + Math.cos(t * Math.PI * 2) * 100;
},
y: function(t) {
return this.centerY + Math.sin(t * Math.PI * 2) * 100;
},
radius: 10
});

Here, I’ve defined a centerX and centerY property on the object, and accessed that within the functions that define x and y. That’s a simple example to show how object variables work, but it doesn’t really show off how they can be useful. In this case, centerX and centerY could easily have just been regular variables anywhere in that code. So let’s take another example.


for(var i = 0; i < 6; i++) { var angle = Math.PI * 2 / 6 * i; list.addCircle({ x: width / 2 + Math.cos(angle) * 100, y: height / 2 + Math.sin(angle) * 100, radius: 10 }); }

Here we have a for loop creating six circles. It first creates an angle variable based on the for loop variable, i. It then uses that variable to calculate the x and y position of each circle. This gives us this image:

thistest01

Now let's change that just a bit.


for(var i = 0; i < 6; i++) { var angle = Math.PI * 2 / 6 * i; list.addCircle({ x: function(t) { return width / 2 + Math.cos(angle) * 100; }, y: function(t) { return height / 2 + Math.sin(angle) * 100; }, radius: 10 }); }

I didn't change the formula for x and y at all. I just wrapped them in functions. But when we do this, we get this picture:

thistest02

What happened?

OK, in the first example, we were calculating that formula, width / 2 + Math.cos(angle) * 100, right in the for loop, getting the number that evaluated to, and assigning it to x. And the same for y.

In the second example, we were NOT evaluating that formula in the for loop. It was just going into the function. You have to think of this in terms of time.

First, the for loop runs, creating all the circle objects. Inside of that, the variable, i, is going from 0 up to 5, and the angle variable on each iteration is:


0
1.0471975511965976
2.0943951023931953
3.141592653589793
4.1887902047863905
5.235987755982988

But that angle variable is never actually accessed during the for loop. When the for loop finishes, angle = 5.23598... And that's where it stays. All of this happens before you ever see a single thing on the screen.

Then, well after that for loop has completed, GLC starts looping. It goes through each circle object and when it sees that they have functions for x and y, it calls those functions. When the functions run, they access the current value of angle, which, as we just saw, is now stuck at 5.23598... radians, or 300 degrees. And that's where each circle gets drawn.

This is the exact situation that object variables are designed to solve. We change the code to this:


for(var i = 0; i < 6; i++) { var angle = Math.PI * 2 / 6 * i; list.addCircle({ a: angle, x: function(t) { return width / 2 + Math.cos(this.a) * 100; }, y: function(t) { return height / 2 + Math.sin(this.a) * 100; }, radius: 10 }); }

Here, I've created an a property on the object. This gets assigned the current value of angle inside that for loop. So the first circle gets 0 for a. On the next iteration, angle is 1.047... but that doesn't change the a value for the first circle. The second circle gets 1.047 as its a though. And so on for the rest of the circles. Now each circle has a unique value for a that is locked in. And this brings us back to our original picture.

thistest01

Now of course, at this point I'd go in and add to that formula to make some custom animation using t, such as:


for(var i = 0; i < 6; i++) { var angle = Math.PI * 2 / 6 * i; list.addCircle({ a: angle, x: function(t) { return width / 2 + Math.cos(this.a) * 100 + t * 50; }, y: function(t) { return height / 2 + Math.sin(this.a) * 100 + t * 50; }, radius: 10 }); }

Not the most exciting animation, but hopefully this helps.

New: the Color Module

I’m very excited about this new feature to GLC. To be fair, I was already proud of color management in GLC and did a lot of work on that aspect of it. It can handle pretty much any color string you can throw at it – 3- and 6-digit hex strings, rgb and rgba strings and named CSS color strings. It can also handle 8-digit hex strings, which native canvas cannot handle. And it can animate between any two colors in any of these formats. It does that by breaking down the colors into their component r, g, b and a channels, interpolating between them, and converting the result back into a color string. Not rocket science, but lots of details coded in there to make sure it all works smoothly.

However, I felt there was a need for a bit more flexibility in the area of creating colors. Sometimes I want a random color, or I need to calculate the values of the color channels mathematically, and convert that into a string (ironically, so that it can be converted back into a number to be animated – might be some room for some future optimization there). And, I had a feature request for hsv/hsb colors. So color improvements is something I’ve had on the list for a while.

About a year and a half ago, I’d created a JavaScript library called clrs, which did just about all of what I wanted to be in GLC. I was hoping I could just “require” it in there and have it work, but when I took a closer look I found that there was a lot of stuff in there that was inapplicable, a few things that were missing, and it actually just wasn’t compatible with GLC. But all of that was easily fixable. I ripped out a bunch of stuff, added some stuff, and refactored what was left. And so now we have a GLC color module.

You access the module with glc.color. However, like other GLC features, I’ve added an alias to the template:

color = glc.color;

So now you can just type “color” to get at the methods. Here are all the methods:

- color.rgb(r, g, b)
- color.rgba(r, g, b, a)
- color.gray(shade)
- color.randomRGB()
- color.randomRGB(min, max)
- color.randomGray()
- color.randomGray(min, max)
- color.num(number)
- color.hsv(h, s, v)
- color.animHSV(h, s, v)
- color.randomHSV(minH, maxH, minS, maxS, minV, maxV)

Let’s go through them.

color.rgb(255, 128, 0) does the same thing as "rgb(255, 128, 0)". The big difference is that using the latter, if you calculate one or more of the channel values mathematically, you’ll need to round those values to integers, then insert them into a string, like so:

fillStyle: "rgb(" + Math.round(red) + ", " + Math.round(green) + ", " + Math.round(blue) + ")"

As opposed to:

fillStyle: color.rgb(red, green, blue)

The same with color.rgba

To create a gray color, just pass a single value from 0-255 to the color.gray method:

fillStyle: color.gray(128)

Again, this saves you from rounding and stringifying it.

The random methods can be called with no parameters and create a completely random rgb color or random gray value:

strokeStyle: color.randomGray(),
fillStyle: color.randomRGB()

Or, you can pass in min and max values. For color.randomRGB(min, max), this ensures that each of the component channels will have a value between min and max. So you can create darker random colors:

fillStyle: color.randomRGB(0, 128)

Or lighter ones, or any range in between. The same goes for color.randomGray(min, max)

The last of the rgb methods is color.num(number). This just lets you pass in an integer that will be converted to a color string. This would most often be used with hex-formatted integers, like the following example, but any integer would work. Note, this only works for 24-bit numbers, no alpha.

fillStyle: color.num(0xff8000)

Now we get down to the hsv based functions. color.hsv(h, s, v) does exactly what you’d think it would do – creates a color based on hue, saturation and value (the same as hsb – hue, saturation and brightness). Hue is in terms of a number between 0 and 360 and saturation and value are both percentages in the range of 0 to 1.

fillStyle: color.hsv(30, 1, 1)

An important thing to note here is that this function takes your h, s and v values and immediately converts them to an rgb string. So, if you expect to animate between hsv values directly, you’ll be disappointed. Take this example:

function onGLC(glc) {
    glc.loop();
    glc.size(200, 200);
    glc.setDuration(5);
    glc.setMode("single");
    glc.setEasing(false);
    var list = glc.renderList,
        width = glc.w,
        height = glc.h,
        color = glc.color;

    list.addCircle({
        x: 100,
        y: 100,
        radius: 100,
        fillStyle: [color.hsv(0, 1, 1), color.hsv(360, 1, 1)]
    })
}

We’re animating the hue value from 0 to 360, so you might expect to see the circle go through all the colors of the spectrum. But instead, we get:

hsv1

The problem is color.hsv(0, 1, 1) and color.hsv(1, 1, 1) both convert into "#ff0000". So when GLC goes to animate them, that's what it animates between. The hue value is completely lost in the translation.

To satisfy that problem, there's another hsv function called color.animHSV(startH, endH, startS, endS, startV, endV). This allows you to specify a start and end value for h, s and v. These will be preserved across an animation, so you can actually animate across the spectrum. Here's that last example fixed to show this in use:

function onGLC(glc) {
    glc.loop();
    glc.size(200, 200);
    glc.setDuration(5);
    glc.setMode("single");
    glc.setEasing(false);
    var list = glc.renderList,
        width = glc.w,
        height = glc.h,
        color = glc.color;

    list.addCircle({
        x: 100,
        y: 100,
        radius: 100,
        fillStyle: color.animHSV(0, 360, 1, 1, 1, 1)
    });
}       

With this, hue, saturation, and value will be recalculated and interpolated on every frame, and you'll get an animation across the whole spectrum.

hsv2

Here, you'll notice I've set the duration to 5, the mode to bounce, and turned off easing, allowing you to see the transition more clearly.

Of course, you can use the same function to animate between a smaller portion of the spectrum:

function onGLC(glc) {
    glc.loop();
    glc.size(200, 200);
    var list = glc.renderList,
        width = glc.w,
        height = glc.h,
        color = glc.color;

    list.addCircle({
        x: 100,
        y: 100,
        radius: 100,
        fillStyle: color.animHSV(20, 60, 1, 1, 1, 1)
    })
}

Here, we are animating between hues of 20 (orangey-red) and 60 (yellowish).

hsv3

Of course, you can also animate with saturation and value as well.

And here's a more advanced use of hsv, to show what's possible:

function onGLC(glc) {
    glc.loop();
    var list = glc.renderList,
        width = glc.w,
        height = glc.h,
        color = glc.color;

    var res = 25;
    for(var y = 0; y < height; y += res) { 
        for(var x = 0; x < width; x += res) { 
            var dx = width / 2 - x - res / 2,
                dy = height / 2 - y - res / 2,
                dist = Math.sqrt(dx * dx + dy * dy);
            list.addCircle({
                translationX: x,
                translationY: y,
                x: res / 2,
                y: res / 2,
                radius: res / 2,
                fillStyle: color.animHSV(20, 60, 1, 1, 1, 1),
                phase: dist * 0.005
            })
        }
    }
}       

Here, we're creating a grid of 25-pixel wide circles, using the same hue range as before. I'm also calculating a dist variable, which is the distance from the center of each circle to the center of the canvas. This is then used to affect the phase value, causing the circles to cycle through their colors at different offsets based on how far from the center they are.

hsv4

Pretty neat! It would be much more difficult to make a nice animation between color values like that using just rgb.

Finally, there is color.randomHSV(minH, maxH, minS, maxS, minV, maxV). This requires minimum and maximum values for each of h, s and v. If you want a fixed value for any one of these, just pass in the same value for min and max. This can create some really nice random, yet coordinated color palettes.

function onGLC(glc) {
    glc.loop();
    var list = glc.renderList,
        width = glc.w,
        height = glc.h,
        color = glc.color;

    var res = 50;
    for(var y = 0; y < height; y += res) { 
        for(var x = 0; x < width; x += res) { 
            list.addCircle({
                translationX: x,
                translationY: y,
                x: res / 2,
                y: res / 2,
                radius: res / 2,
                fillStyle: color.randomHSV(90, 180, 1, 1, 1, 1),
            })
        }
    }
}       

hsv5

Here, the hue is random between 90 and 180, while both saturation and value are 1.

Or, you can keep the hue the same, and randomize the saturation and/or value, giving you some monochromatic type palettes.

function onGLC(glc) {
    glc.loop();
    var list = glc.renderList,
        width = glc.w,
        height = glc.h,
        color = glc.color;

    var res = 50;
    for(var y = 0; y < height; y += res) { 
        for(var x = 0; x < width; x += res) { 
            list.addCircle({
                translationX: x,
                translationY: y,
                x: res / 2,
                y: res / 2,
                radius: res / 2,
                fillStyle: color.randomHSV(240, 240, 0, 1, 0.5, 1),
            })
        }
    }
}       

hsv6

So, a whole lot of new stuff there. Hope you find it useful.

Update 11/24/15:

Per a request, there are now versions of the hsv functions that include an alpha channel.

fillStyle: color.hsva(h, s, v, a)

and

fillStyle: color.animHSVA(startH, endH, startS, endS, startV, endV, startA, endA)

Single Mode

The default mode of GLC is “bounce”. This causes the animated properties of your objects to go from their starting values to their ending values, and then back again. All nice and smooth. Pretty hard to create a non-smoothly-looping animation with that default setup.

But once you switch over to “single” mode, things become more difficult. Most simple animations will look horrible because they’ll animate from one state to another, and then jump back to the initial state. Very jarring. But there are some tricks to making decent single mode animations, and once you get it, I’d say these animations are even superior to ones created in bounce mode.

The biggest single point to understand, is that for each object in the render list, what’s on screen at the start of the animation point has to match what’s on screen in that same location at the end of the animation.

Now at first glance, that seems impossible. You’re changing something from state A to state B. How could it be at state A when it ends at state B? Well, one example would just be to rotate something 360 degrees. Like this:

list.addRect({
    x: 100,
    y: 100,
    w: 100,
    h: 100,
    rotation: [0, 360]
});

single01

Since 360 degrees is the same as 0 degrees, we’re in the same state, and we have a smoothly running single mode animation.

Actually, because we have a square there, we can even rotate it to any multiple of 90 degrees and still have it wind up in the same visible state it starts at. It won’t technically BE in the same state, because 90 isn’t 0, but what you see on screen in that location will be the same.

list.addRect({
    x: 100,
    y: 100,
    w: 100,
    h: 100,
    rotation: [0, 90]
});

single02

But there are other tricks. Consider this: if an object starts in some state where you can’t see it, and ends in some other state where you can’t see it, that’s the same too. So we could have it start and end off screen.

list.addRect({
    x: [-50, 250],
    y: 100,
    w: 100,
    h: 100
});

single03

But off screen isn’t the only way something can be invisible. We could set the alpha down to zero or change the alpha channel of the color down to zero, or simply have the color change to match the background. In all of these cases, you won’t be able to see the object.

list.addRect({
    x: [-50, 100],
    y: 100,
    w: 100,
    h: 100,
    globalAlpha: [1, 0]
});

single04

In this last example, the square starts off invisible, because it’s off screen to the left. It ends invisible because its alpha is 0. So it’s all nice and smooth. You could reverse this and have it start off with zero alpha, and fade in as it moves off screen.

Or, you could reduce the size of the object to zero.

list.addRect({
    x: [-50, 100],
    y: 100,
    w: [100, 0],
    h: [100, 0],
});

single05

How about hiding it by moving it behind something?

list.addRect({
    x: [-50, 100],
    y: 100,
    w: 50,
    h: 50
});
list.addRect({
    x: 100,
    y: 100,
    w: 75,
    h: 200
});

single06

Here, there’s another rect in the middle of the screen. The first rect is hiding behind it so you don’t notice it snapping out of existence and returning to the starting point.

This last one is key because it gives us the starting point to the most powerful method of single mode animations – multiple objects. In this example, we have one object start where the other one ends.

list.addRect({
    x: [-50, 100],
    y: 100,
    w: 50,
    h: 50
});
list.addRect({
    x: [100, 250],
    y: 100,
    w: 50,
    h: 50
});

single07

Here we have the first square starting off screen to the left and moving to a position of 100, 100. And we have another square starting at 100, 100 and moving off screen to the right. At the end of the animation the first square winks out of existence and jumps back to its starting position on the left. At the same instant, the second square jumps back into the middle of the screen. But because these two things match up, you never see the change.

Now because we have easing on, it does stop there briefly in the middle of the screen. But that’s only due to the easing. If we turn off easing, we have a nice smooth transition.

single08

Realize that it doesn’t matter what the first square does or looks like before it reaches its end point. And it doesn’t matter what that second square changes into or where it goes after that initial state. As long as A’s end matches B’s start, we’re golden.

list.addRect({
    x: [-50, 100],
    y: 100,
    w: [20, 50],
    h: [200, 50],
    fillStyle: ["red", "black"]
});
list.addRect({
    x: [100, 250],
    y: [100, -50],
    w: 50,
    h: 50,
    fillStyle: ["black", "green"],
    rotation: [0, 360]
});

single09

Here, the first square starts out as a red rectangle. The second one turns green as it spins out through the top right corner. But at that transition point, they are the same, so it’s smooth.

Just like juggling, once you get two objects down, you can move to three.

list.addRect({
    x: [-50, 100],
    y: 100,
    w: [20, 50],
    h: [200, 50],
    fillStyle: ["red", "black"]
});
list.addRect({
    x: [100, 250],
    y: [100, 50],
    w: 50,
    h: 50,
    fillStyle: ["black", "green"],
    rotation: [0, 360]
});
list.addRect({
    x: 250,
    y: [50, 150],
    w: [50, 0],
    h: [50, 0],
    fillStyle: "green",
    globalAlpha: [1, 0]
});

single10

Here the second rectangle doesn’t go off screen, but it joins a third one that moves down as it shrinks and fades away.

Now we have two merge points where one object is swapping over with another. And we can chain as many as we want this way. But the first and last objects need to start and end invisible. Unless… they merge with each other.

list.addRect({
    x: [50, 150],
    y: 50,
    w: 50,
    h: 50,
});
list.addRect({
    x: 150,
    y: [50, 150],
    w: 50,
    h: 50,
});
list.addRect({
    x: [150, 50],
    y: 150,
    w: 50,
    h: 50,
});
list.addRect({
    x: 50,
    y: [150, 50],
    w: 50,
    h: 50,
});

single11

Here we have four squares. The first one moves left to right, where it joins with the second one, which moves down. This meets with the third, which moves back to the left, meeting the fourth that moves up. The fourth square ends where the first starts, so everything joins together.

And we can do whatever we want with any of these shapes as long as the end of one matches the start of the next one, all the way around.

list.addRect({
    x: [50, 150],
    y: 50,
    w: 50,
    h: 50,
    fillStyle: ["red", "blue"]
});
list.addRect({
    x: 150,
    y: [50, 150],
    w: 50,
    h: 50,
    fillStyle: ["blue", "green"],
    rotation: [0, 45]
});
list.addRect({
    x: [150, 50],
    y: 150,
    w: [50, 25],
    h: [50, 75],
    fillStyle: ["green", "orange"],
    rotation: [45, 360]
});
list.addRect({
    x: 50,
    y: [150, 50],
    w: [25, 50],
    h: [75, 50],
    fillStyle: ["orange", "red"]
});

single12

So, it takes a bit more work, thinking and planning than bounce mode, but the results are well worth it.

Max Colors and File Size

Two new features today, and you can see them both right here:

1.1

(Click to enlarge that.)

First, in the control panel, you’ll see a new slider below duration and fps. It’s Max Colors. Up to now, gifs were encoded with 256 colors. This was hard coded in one of the gif encoding modules that GLC uses. I didn’t write these, but did tweak them a bit to work with the system. They are mostly over my head, but I did figure out where this value was being set and went in there with a machete and hacked out a code trail to it and exposed that with a slider. The end result is that you can specify the maximum number of colors your final animated gif will have, anywhere from 2 to 256.

Note, sometimes those sliders can be difficult to get to land on an exact number. But once one is in focus, you can tweak it by single values using the cursor left and right keys on your keyboard.

Limiting the number of colors in the gif can have a big impact on the size of your output. Often at no visible loss of quality. Realize that changing this slider will not affect what you see in the Canvas panel AT ALL. Also, if you generate a gif, the slider won’t change that gif you’re looking at either. You’ll have to re-generate a new gif.

The other change, very much related to the first, is down at the bottom of the output panel. GLC will now estimate the gif size before you even save it. In my tests on Windows anyway, this estimation has actually been spot on. Different file systems may or may not make a difference, so I’ll leave the word “estimated” in there.

The combo of these two changes makes it possible to tweak your animation for size even more than you could with just fps and duration controls, and see the results before you even save the file. This can be vital if you’re trying to generate gifs for Tumblr, for example, which has a 2MB limit.

Creating a grid layout

Sometimes you want to create a bunch of objects laid out in a grid. There are a few ways to do this. Here’s the easiest way I found.

We’ll start with the basic template. I’ll add a size variable and a double for loop that goes from 0 to height on the y-axis and 0 to width on x.

function onGLC(glc) {
    glc.loop();
    // glc.playOnce();
    // glc.size(400, 400);
    // glc.setDuration(5);
    // glc.setFPS(20);
    // glc.setMode("single");
    // glc.setEasing(false);
    var list = glc.renderList,
        width = glc.w,
        height = glc.h;

        var size = 50;
        for(var y = 0; y < height; y += size) {
        for(var x = 0; x < width; x += size) {

        }
    }
}


The size variable represents the size of one cell in the grid. Here, I’m making square cells, but you could have a different variable for each axis and make non-square cells as well.

The loop starts x and y at 0 and increments them by size each time. Now we can add an object.

var size = 50;
for(var y = 0; y < height; y += size) {
    for(var x = 0; x < width; x += size) {
        list.addCircle({
            translationX: x,
            translationY: y,
        });
    }
}

Using translationX and translationY moves the origin of the canvas to the top left of each grid cell on each iteration. Now you can just draw your object assuming that it’s in a size x size rectangle. No fancy math needed to calculate the x and y positions. Here, I’ll center the circle within that rectangle. And animate it’s radius to a max of size / 2

var size = 50;
for(var y = 0; y < height; y += size) {
    for(var x = 0; x < width; x += size) {
        list.addCircle({
            translationX: x,
            translationY: y,
            x: size / 2,
            y: size / 2,
            radius: [size / 4, size / 2]
        });
    }
}

Here’s what this gives us.

grid

Pretty simple. No fancy math needed. Now we can make things a bit more interesting by messing with the phase property.

var size = 50;
for(var y = 0; y < height; y += size) {
    for(var x = 0; x < width; x += size) {
        list.addCircle({
            translationX: x,
            translationY: y,
            x: size / 2,
            y: size / 2,
            radius: [size / 4, size / 2],
            phase: x / width + y / height
        });
    }
}


gridphase

And then, simply by changing the size variable to a smaller value, like 20, and no other changes, we get this:

gridphase2