Replacement technique with 2D grid of objects (Processing)
2021-09-04
The replacement technique was already explained and illustrated in the previous tutorial. This one shows both how to use it with objects that have random parameters, and also with a 2D grid structure: something I have used a lot.
I chose this gif as example for this tutorial:
In the gif the positions of the tiles on one axis (x-axis) never change, and it moves along another axis (y-axis). We’re going to use a 2D grid of tiles of size 70 x 4. The idea is that every object (tile) will take the place 4 tile later along y-axis at the end of the loop.
First let’s make the grid of tiles and show them.
Here is some code to draw that:
float t; | |
float L; | |
int numberOfFrames = 100; | |
int ny = 4; | |
int nx = 70; | |
class Tile | |
{ | |
int xIndex; | |
int yIndex; | |
Tile(int i,int j) | |
{ | |
xIndex = i; | |
yIndex = j; | |
} | |
void show() | |
{ | |
push(); | |
translate(L*(xIndex-nx/2),L*yIndex,-L/2); | |
fill(0); | |
stroke(255); | |
strokeWeight(1.5); | |
box(0.95*L,0.95*L,0.1*L); // a bit of space between tiles, and small z length | |
pop(); | |
} | |
}; | |
Tile [][] array = new Tile[nx][ny]; | |
void setup(){ | |
size(600,600,P3D); | |
randomSeed(12345); | |
L = 1.0*width/15; | |
for(int i=0;i<nx;i++) | |
for(int j=0;j<ny;j++) array[i][j] = new Tile(i,j); | |
} | |
void draw(){ | |
//t = 1.0*mouseX/width; // to control time with mouse | |
t = 1.0*(frameCount-1)/numberOfFrames; | |
background(0); | |
push(); | |
translate(width/2,height/2); | |
rotateX(0.85); | |
rotateZ(0.4); | |
for(int i=0;i<nx;i++) | |
for(int j=0;j<ny;j++) array[i][j].show(); | |
pop(); | |
if(frameCount<=numberOfFrames) | |
{ | |
saveFrame("fr###.gif"); | |
} | |
if(frameCount==numberOfFrames) | |
{ | |
stop(); | |
} | |
} |
There’s already the setup to render the animation although the tiles don’t move yet. I also already rotated the view as it is at the end. I made gifs at 50 fps (delay=2). L defines the size of the tiles.
So far it looks like this:
Now let’s use replacement technique with our grid taking the next grid position when time is increased by 1.
In Tile class:
void show(float p) | |
{ | |
push(); | |
translate(L*(xIndex-nx/2),L*yIndex,-L/2); | |
translate(0,-p*L*ny); | |
fill(0); | |
stroke(255); | |
strokeWeight(1.5); | |
box(0.95*L,0.95*L,0.1*L); | |
pop(); | |
} | |
int K = 10; | |
void show() | |
{ | |
for(int i=-K;i<=K;i++) | |
{ | |
float p = i+t; | |
show(p); | |
} | |
} |
We get something like this:
It does not look amazing but we’ve got a nice structure behind and a large part of the work is done.
Let’s introduce a delay member in the Tile class that will determine when a tile starts to flip:
class Tile | |
{ | |
int xIndex; | |
int yIndex; | |
float delay; | |
Tile(int i,int j) | |
{ | |
xIndex = i; | |
yIndex = j; | |
delay = 0; | |
} | |
void show(float p) | |
{ | |
push(); | |
translate(L*(xIndex-nx/2),L*yIndex,-L/2); | |
translate(0,-p*L*ny); | |
float changer = constrain(map(p-delay,0,2.5,0,1),0,1); | |
float xRot = PI+PI*changer; | |
// starts being rotated by PI (starts flipped to hide digits) | |
// and ends being rotated by TWO_PI | |
fill(0); | |
stroke(255); | |
strokeWeight(1.5); | |
rotateX(xRot); | |
box(0.95*L,0.95*L,0.1*L); | |
pop(); | |
} | |
int K = 10; | |
void show() | |
{ | |
for(int i=-K;i<=K;i++) | |
{ | |
float p = i+t; | |
show(p); | |
} | |
} | |
}; |
The variable changer
is 0 before p=delay, then its value increases to 1 until p=delay+2.5, then its value stays at 1.
So this is used to rotate the tiles around X axis. The result looks like this:
Let’s try to have a delay that changes according to the y index so that the 4x70 grid structure is less apparent.
Tile(int i,int j) | |
{ | |
xIndex = i; | |
yIndex = j; | |
delay = 1.0*yIndex/ny; | |
} |
Maybe I can make this formula logical by saying that when y and yIndex increase, we want the tile to move later, so with more delay.
The fix looks like this:
Now let’s add some gaussian noise to the delay of each tile to get something that looks more complex (and by the way the delay is a bit decreased with a constant).
Tile(int i,int j) | |
{ | |
xIndex = i; | |
yIndex = j; | |
delay = 0.5*randomGaussian() + 1.0*yIndex/ny - 0.3; | |
} |
To make this a bit more exciting we can use easing functions, you can find some on this page.
float easeOutElastic(float x) | |
{ | |
float c4 = (2*PI)/3; | |
if(x<=0) return 0; | |
if(x>=1) return 1; | |
return pow(2, -10 * x) * sin((x * 10 - 0.75) * c4) + 1; | |
} | |
// in-out easing of strength g | |
float ease(float p, float g) { | |
if (p < 0.5) | |
return 0.5 * pow(2*p, g); | |
else | |
return 1 - 0.5 * pow(2*(1 - p), g); | |
} |
Easing functions basically distort values between 0 and 1 and can be useful to get nice transitions. Here it’s directly applied to the changer
variable.
void show(float p) | |
{ | |
push(); | |
translate(L*(xIndex-nx/2),L*yIndex,-L/2); | |
translate(0,-p*L*ny); | |
float changer = constrain(map(p-delay,0,2.5,0,1),0,1); | |
changer = ease(changer,1.4); // classic in-out easing with stength 1.4 | |
float elasticChanger = easeOutElastic(changer); // then elastic easing | |
float xRot = PI+PI*elasticChanger; | |
fill(0); | |
stroke(255); | |
strokeWeight(1.5); | |
rotateX(xRot); | |
box(0.95*L,0.95*L,0.1*L); | |
pop(); | |
} |
Then I’ve done something with colors:
void show(float p) | |
{ | |
push(); | |
translate(L*(xIndex-nx/2),L*yIndex,-L/2); | |
translate(0,-p*L*ny); | |
float changer = constrain(map(p-delay,0,2.5,0,1),0,1); | |
changer = ease(changer,1.4); // classic in-out easing with stength 1.4 | |
float elasticChanger = easeOutElastic(changer); // then elastic easing | |
float xRot = PI+PI*elasticChanger; | |
fill(0); | |
stroke(255*(1-changer)); // <--- change here | |
strokeWeight(1.5); | |
rotateX(xRot); | |
box(0.95*L,0.95*L,0.1*L); | |
pop(); | |
} |
(note: changer
variable, and not elasticChanger
, is used to change colors. That’s why I use two different variables)
Here is the result:
Because we can do more random stuff for each object than the gaussian noise, I thought about using different digits. You could also give a random color to each tile, random different easing, random size/shape, etc…
The Tile class with the digits (very few changes):
class Tile | |
{ | |
int xIndex; | |
int yIndex; | |
float delay; | |
int digit = floor(random(10)); | |
Tile(int i,int j) | |
{ | |
xIndex = i; | |
yIndex = j; | |
delay = 0.5*randomGaussian() + 1.0*yIndex/ny - 0.3; | |
} | |
void show(float p) | |
{ | |
push(); | |
translate(L*(xIndex-nx/2),L*yIndex,-L/2); | |
translate(0,-p*L*ny); | |
float changer = constrain(map(p-delay,0,2.5,0,1),0,1); | |
changer = ease(changer,1.4); // classic in-out easing with stength 1.4 | |
float elasticChanger = easeOutElastic(changer); // then elastic easing | |
float xRot = PI+PI*elasticChanger; | |
fill(0); | |
stroke(255*(1-changer)); | |
strokeWeight(1.5); | |
rotateX(xRot); | |
box(0.95*L,0.95*L,0.1*L); | |
translate(0,0,0.1*L+0.5); // small z translate before drawing digit | |
stroke(255); | |
fill(255); | |
textSize(25); | |
text(digit,-L/6,L/6); | |
pop(); | |
} | |
int K = 10; | |
void show() | |
{ | |
for(int i=-K;i<=K;i++) | |
{ | |
float p = i+t; | |
show(p); | |
} | |
} | |
}; |
One thing I sometimes do is to have something in each object that changes with 1D noise and the “p” variable in this code (and with a different seed for each object).
Important: The “changer
” thing that is used here to trigger something is not necessary at all for 2D grid replacement loops: objects are more generally changing with the parameter p
.
The grid is a quite abstract structure, it doesn’t have to look like as much as a grid as here, and I use the same technique to make some tunnel gifs.
Thanks for reading and I hope you found this interesting or helpful.
Here is the entire code for the above finished gif of this tutorial:
float t; | |
float L; | |
int numberOfFrames = 100; | |
int ny = 4; | |
int nx = 70; | |
float easeOutElastic(float x) | |
{ | |
float c4 = (2*PI)/3; | |
if(x<=0) return 0; | |
if(x>=1) return 1; | |
return pow(2, -10 * x) * sin((x * 10 - 0.75) * c4) + 1; | |
} | |
float ease(float p, float g) { | |
if (p < 0.5) | |
return 0.5 * pow(2*p, g); | |
else | |
return 1 - 0.5 * pow(2*(1 - p), g); | |
} | |
class Tile | |
{ | |
int xIndex; | |
int yIndex; | |
float delay; | |
int digit = floor(random(10)); | |
Tile(int i,int j) | |
{ | |
xIndex = i; | |
yIndex = j; | |
delay = 0.5*randomGaussian() + 1.0*yIndex/ny - 0.3; | |
} | |
void show(float p) | |
{ | |
push(); | |
translate(L*(xIndex-nx/2),L*yIndex,-L/2); | |
translate(0,-p*L*ny); | |
float changer = constrain(map(p-delay,0,2.5,0,1),0,1); | |
changer = ease(changer,1.4); // classic in-out easing with stength 1.4 | |
float elasticChanger = easeOutElastic(changer); // then elastic easing | |
float xRot = PI+PI*elasticChanger; | |
fill(0); | |
stroke(255*(1-changer)); | |
strokeWeight(1.5); | |
rotateX(xRot); | |
box(0.95*L,0.95*L,0.1*L); | |
translate(0,0,0.1*L+0.5); // small z translate before drawing digit | |
stroke(255); | |
fill(255); | |
textSize(25); | |
text(digit,-L/6,L/6); | |
pop(); | |
} | |
int K = 10; | |
void show() | |
{ | |
for(int i=-K;i<=K;i++) | |
{ | |
float p = i+t; | |
show(p); | |
} | |
} | |
}; | |
Tile [][] array = new Tile[nx][ny]; | |
void setup(){ | |
size(600,600,P3D); | |
randomSeed(12345); | |
L = 1.0*width/15; | |
for(int i=0;i<nx;i++) | |
for(int j=0;j<ny;j++) array[i][j] = new Tile(i,j); | |
} | |
void draw(){ | |
//t = 1.0*mouseX/width; // to control time with mouse | |
t = 1.0*(frameCount-1)/numberOfFrames; | |
background(0); | |
push(); | |
translate(width/2,height/2); | |
rotateX(0.85); | |
rotateZ(0.4); | |
for(int i=0;i<nx;i++) | |
for(int j=0;j<ny;j++) array[i][j].show(); | |
pop(); | |
if(frameCount<=numberOfFrames) | |
{ | |
saveFrame("fr###.gif"); | |
} | |
if(frameCount==numberOfFrames) | |
{ | |
stop(); | |
} | |
} |
Here’s a pretty similar gif I made:
Also this one: