Created
January 25, 2021 02:50
-
-
Save cwillmor/32ff5a907e743958cd525ff2cd2e860a to your computer and use it in GitHub Desktop.
ells.pde (L-tiling clock in processing)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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