Skip to content

Instantly share code, notes, and snippets.

@cwillmor
Created January 25, 2021 02:50
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save cwillmor/32ff5a907e743958cd525ff2cd2e860a to your computer and use it in GitHub Desktop.
Save cwillmor/32ff5a907e743958cd525ff2cd2e860a to your computer and use it in GitHub Desktop.
ells.pde (L-tiling clock in processing)
// ell clock https://twitter.com/cwillmore/status/1353435612636803073
// developed with processing 3.5.4 (processing.org)
// TODO:
// - motion blur
// - ripple update of ells - one only starts rotating when it has room to (<< ... <> ... >>)
static final int DEPTH = 3;
static final int N = 1 << (DEPTH + 1);
static final int FRAME_RATE = 30;
static final float DT = 1 / (float)FRAME_RATE;
static final float STEP_LENGTH = 1;
static final int FRAMES_PER_STEP = int(FRAME_RATE * STEP_LENGTH);
static final boolean SAVE_FRAMES = false;
static final boolean USE_PACMAN_ELLS = false;
// an ell is an L-shaped triomino mounted on an angular spring at the concave vertex at its center. use setOrientation() to change the spring's equilibrium position, getTheta() to get its current angle, and step() to advance the spring simulation.
class Ell {
private int orientation; // 0 = |_, 1 = _|, 2 = `|, 3 = |'
private float theta; // angular position (rad). 0 = |_, pi/2 = _|, etc.
private float targetTheta; // equilibrium angular position (rad)
private float omega; // angular velocity (rad/sec)
Ell() {
orientation = 0;
theta = 0;
targetTheta = 0;
omega = 0;
}
// given current orientation and target theta and a desired new orientation, return a new target theta that minimizes the amount by which the ell has to rotate
private float updateTheta(float theta, int oldOrientation, int newOrientation) {
int diff = newOrientation - oldOrientation;
while (diff < 0) {
diff += 4;
}
while (diff >= 4) {
diff -= 4;
}
switch (diff) {
case 0: return theta;
case 1: return theta + HALF_PI;
case 2: return (random(1) > 0.5) ? (theta + PI) : (theta - PI);
case 3: return theta - HALF_PI;
default: return theta;
}
}
public float getTheta() {
return theta;
}
public void setOrientation(int newOrientation, boolean animate) {
if (animate) {
targetTheta = updateTheta(targetTheta, orientation, newOrientation);
} else {
targetTheta = theta = newOrientation * HALF_PI;
omega = 0;
}
orientation = newOrientation;
}
// tau = I alpha
// tau = k_spring (targetTheta - theta) - k_drag omega
// I alpha = k_spring (targetTheta - theta) - k_drag omega
// alpha = (k_spring / I) (targetTheta - theta) - (k_drag / I) omega
public void step() {
float k_spring = 400; // N.m / rad
float k_drag = 20; // N.m / (rad/s)
// just say ell has moment of rotation I = 1, whatever
float alpha = k_spring * (targetTheta - theta) - k_drag * omega;
omega += alpha * DT;
theta += omega * DT;
}
}
color bgColor;
color ellColor;
Ell[][] ells;
ArrayList<int[]> gapSequence;
int gifFrameCount;
void setup(){
bgColor = color(255);
//bgColor = #C4E3FC;
ellColor = color(0);
//ellColor = #025AA2;
frameRate(FRAME_RATE);
size(500,500);
ells = new Ell[N + 1][N + 1];
setOrientations(0, 0, /*animate*/false);
gapSequence = clockSequence();
gifFrameCount = FRAMES_PER_STEP * gapSequence.size();
}
// return the index of the quadrant that the cell (xGap, yGap) lies in w.r.t. the point (xMid, yMid)
int gapQuadrant(int xMid, int yMid, int xGap, int yGap) {
if (xGap >= xMid) {
if (yGap >= yMid) {
return 0;
} else {
return 3;
}
} else {
if (yGap >= yMid) {
return 1;
} else {
return 2;
}
}
}
// tile the grid with ells such that all squares except (xGap, yGap) are covered. 'animate' controls whether the transition is animated or immediate.
void setOrientations(int xGap, int yGap, boolean animate) {
setOrientations(xGap, yGap, 0, N, 0, N, animate);
}
// tile the subgrid [xLo, xHi) x [yLo, yHi) with ells such that all squares except (xGap, yGap) are covered.
void setOrientations(int xGap, int yGap, int xLo, int xHi, int yLo, int yHi, boolean animate) {
assert xLo <= xGap && xGap < xHi;
assert yLo <= yGap && yGap < yHi;
assert xHi - xLo == yHi - yLo;
int xMid = (xHi + xLo) / 2;
int yMid = (yHi + yLo) / 2;
int quad = gapQuadrant(xMid, yMid, xGap, yGap);
if (ells[xMid][yMid] == null) {
ells[xMid][yMid] = new Ell();
}
ells[xMid][yMid].setOrientation(quad, animate);
if (xHi - xLo <= 2) {
return;
}
// TODO: is there a more compact way to write all this?
if (quad == 0) {
setOrientations(xGap, yGap, xMid, xHi, yMid, yHi, animate);
} else {
setOrientations(xMid, yMid, xMid, xHi, yMid, yHi, animate);
}
if (quad == 1) {
setOrientations(xGap, yGap, xLo, xMid, yMid, yHi, animate);
} else {
setOrientations(xMid - 1, yMid, xLo, xMid, yMid, yHi, animate);
}
if (quad == 2) {
setOrientations(xGap, yGap, xLo, xMid, yLo, yMid, animate);
} else {
setOrientations(xMid - 1, yMid - 1, xLo, xMid, yLo, yMid, animate);
}
if (quad == 3) {
setOrientations(xGap, yGap, xMid, xHi, yLo, yMid, animate);
} else {
setOrientations(xMid, yMid - 1, xMid, xHi, yLo, yMid, animate);
}
}
// draw a 2x2 ell with its concave vertex at the origin, in the L orientation
void drawEll() {
float inset = 0.1;
beginShape();
vertex(-inset, -inset);
vertex(-inset, 1 - inset);
vertex(-1 + inset, 1 - inset);
vertex(-1 + inset, -1 + inset);
vertex(1 - inset, -1 + inset);
vertex(1 - inset, -inset);
endShape(CLOSE);
}
// draw a pacman (3/4 of a circle) with radius 1 and its concave vertex at the origin, in the L orientation
void drawPacmanEll() {
float inset = 0.1;
float c = 0.55192; // magic circle number
float r = 1 - inset;
beginShape();
vertex(0, 0);
vertex(0, r);
bezierVertex(-c * r, r, -r, c * r, -r, 0); // 2nd quadrant arc
bezierVertex(-r, -c * r, -c * r, -r, 0, -r); // 3rd quadrant arc
bezierVertex(c * r, -r, r, -c * r, r, 0); // 4th quadrant arc
endShape(CLOSE);
}
// return a sequence of 'len' randomly chosen cells
ArrayList<int[]> randomSequence(int len) {
ArrayList<int[]> result = new ArrayList();
for (int i = 0; i < len; i++) {
result.add(new int[] {int(random(N)), int(random(N))});
}
return result;
}
// return a sequence of cells that marches clockwise around the perimeter of the subgrid [xLo, xHi) x [yLo, yHi)
ArrayList<int[]> clockSequence(int xLo, int xHi, int yLo, int yHi) {
ArrayList<int[]> result = new ArrayList();
int x, y;
for (x = xLo; x < xHi - 1; x++) {
result.add(new int[] {x, yLo});
}
for (y = yLo; y < yHi - 1; y++) {
result.add(new int[] {xHi - 1, y});
}
for (x = xHi - 1; x > xLo; x--) {
result.add(new int[] {x, yHi - 1});
}
for (y = yHi - 1; y > yLo; y--) {
result.add(new int[] {xLo, y});
}
return result;
}
// return a sequence of cells that marches clockwise around the perimeter of the grid
ArrayList<int[]> clockSequence() {
return clockSequence(0, N, 0, N);
}
// return a sequence of cells that spirals clockwise in toward the middle
ArrayList<int[]> spiralSequence() {
ArrayList<int[]> result = new ArrayList();
for (int margin = 0; margin < N / 2; margin++) {
result.addAll(clockSequence(margin, N - margin, margin, N - margin));
}
return result;
}
void draw(){
background(bgColor);
if (frameCount % FRAMES_PER_STEP == 0) {
int step = frameCount / FRAMES_PER_STEP;
int[] gap = gapSequence.get(step % gapSequence.size());
setOrientations(gap[0], gap[1], /*animate*/true);
}
//<>//
fill(ellColor);
noStroke();
translate(250, 250);
int margin = 30;
scale((500 - 2 * margin) / (N * sqrt(2)));
rotate(PI/4);
translate(-N/2, -N/2);
for (int x = 0; x <= N; x++) {
for (int y = 0; y <= N; y++) {
Ell ell = ells[x][y];
if (ell == null) {
continue;
}
ell.step();
push();
translate(x, y);
rotate(ell.getTheta());
if (USE_PACMAN_ELLS) {
drawPacmanEll();
} else {
drawEll();
}
pop();
}
}
if (SAVE_FRAMES && frameCount > gifFrameCount && frameCount <= 2 * gifFrameCount){
saveFrame("fr#####.png");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment