A creative coding animation with Hamiltonian circuit.
Path keeps cycle even if the route changes.
It's a creative coding animation. The movers move on the cycle path and the path keeps cycle even if the route changes.
I knew about the Hamiltonian circuit the first time when I read this article from Baku Hashimoto san (@_baku89). And I felt to want to make some similar animation with the 'Processing'.
ハミルトン閉路でおもちが動く
The basic rule.
The grid has cells. Each cell has four nodes and shares nodes with neighbor cells.
The node has one direction.
Start with alternate opposite directions.
Choose cells randomly and turn the directions of the nodes.
When going over the grid, it will come out from the opposite side.
An example source code of the 'Processing'.
This code does not display any images on the screen but generates image files in frames directory. You can make an animation with these files.
Please feel free to use this example code under the terms of the GPL. To see other works based on my code is my pleasure. And my honor.
/**
* One Way Street.
* Mover moving on the cycle path.
* Is this a Hamiltonian circuit?
*
* @author @deconbatch
* @version 0.1
* @license GPL Version 3 http://www.gnu.org/licenses/
* Processing 3.5.3
* 2021.03.14
*/
/**
* Grid : grid on the canvas.
* grid has cols x rows cells.
* cell has four nodes, cell shares nodes with neighbor cells.
* node has direction. direction makes a route on the grid.
* cells and nodes does not move.
*/
public class Grid {
private int cols, rows; // grid columns, rows number
private Node[][] nodes; // nodes on the grid
// constants of nodes position in the cell
public PVector nw = new PVector(-1, -1);
public PVector ne = new PVector(+1, -1);
public PVector sw = new PVector(-1, +1);
public PVector se = new PVector(+1, +1);
// constants of node direction
public PVector n = new PVector(0, -1);
public PVector s = new PVector(0, +1);
public PVector w = new PVector(-1, 0);
public PVector e = new PVector(+1, 0);
Grid(int _cols, int _rows) {
cols = _cols;
rows = _rows;
nodes = new Node[cols][rows];
for (int x = 0; x < cols; x++) {
for (int y = 0; y < rows; y++) {
nodes[x][y] = new Node();
}
}
}
/**
* changeRoute : change route on the grid
* change direction of the node on the selected cells to make new route on the grid.
*/
void changeRoute(float _selectionRate) {
ArrayList<PVector> selectedCells = selectCells(_selectionRate);
for (PVector c : selectedCells) {
int x0 = floor(c.x);
int y0 = floor(c.y);
int x1 = folding(floor(c.x), 1, cols);
int y1 = folding(floor(c.y), 1, rows);
setNodeDirection(x0, y0, calcDirection(getNodeCurrDirection(x0, y0), nw));
setNodeDirection(x1, y0, calcDirection(getNodeCurrDirection(x1, y0), ne));
setNodeDirection(x0, y1, calcDirection(getNodeCurrDirection(x0, y1), sw));
setNodeDirection(x1, y1, calcDirection(getNodeCurrDirection(x1, y1), se));
}
}
/**
* selectCells : select direction-changing cells
* selection rule
* 1.select randomly, probabillity value is _selectionRate parameter.
* 2.a cell that has selected neighbor does not apply.
* 3.a cell that has two nodes that has inner-direction.
* and these nodes has parallel direction.
*/
ArrayList<PVector> selectCells(float _selectionRate) {
ArrayList<PVector> sg = new ArrayList<PVector>();
for (int x = 0; x < cols; x++) {
for (int y = 0; y < rows; y++) {
if (random(1.0) < _selectionRate) {
Boolean noOne = true;
for (PVector p : sg) {
// neighbor already selected?
if (
(p.x == folding(x, +1, cols) && p.y == y) ||
(p.x == folding(x, -1, cols) && p.y == y) ||
(p.y == folding(y, +1, rows) && p.x == x) ||
(p.y == folding(y, -1, rows) && p.x == x)
) {
noOne = false;
break;
}
}
if (noOne) {
ArrayList<PVector> testNodes= getInnerDirectionNodes(x, y);
// cell has two inner-direction node
if (testNodes.size() == 2) {
// these nodes has parallel direction
if (
testNodes.get(0).x == testNodes.get(1).x &&
testNodes.get(0).y == testNodes.get(1).y
) {
sg.add(new PVector(x, y));
}
}
}
}
}
}
return sg;
}
/**
* getInnerDirectionNodes : get inner-direction nodes in the cell.
* 'inner-direction' is my coined word. it means the direction does not go out the cell.
* ex.
* north or west direction on north west node : false
* south or east direction on north west node : true
*/
ArrayList<PVector> getInnerDirectionNodes(int _cx, int _cy) {
int x0 = _cx;
int y0 = _cy;
int x1 = folding(floor(_cx), 1, cols);
int y1 = folding(floor(_cy), 1, rows);
PVector p;
ArrayList<PVector> in = new ArrayList<PVector>();
// nw
p = getNodeCurrDirection(x0, y0);
if (isInnerDirection(p, nw)) {
in.add(new PVector(p.x * nw.x, p.y * nw.y));
}
// ne
p = getNodeCurrDirection(x1, y0);
if (isInnerDirection(p, ne)) {
in.add(new PVector(p.x * ne.x, p.y * ne.y));
}
// sw
p = getNodeCurrDirection(x0, y1);
if (isInnerDirection(p, sw)) {
in.add(new PVector(p.x * sw.x, p.y * sw.y));
}
// se
p = getNodeCurrDirection(x1, y1);
if (isInnerDirection(p, se)) {
in.add(new PVector(p.x * se.x, p.y * se.y));
}
return in;
}
/**
* calcDirection : calculate the new direction of the node.
* only inner-direction should change the direction.
* should change to the inner-direction.
* ex.
* south direction on north west node should change to east direction.
*/
PVector calcDirection(PVector _nodeDirection, PVector _nodePosition) {
if (isInnerDirection(_nodeDirection, _nodePosition)) {
return new PVector(
-1 * (_nodeDirection.x + _nodePosition.x),
-1 * (_nodeDirection.y + _nodePosition.y)
);
}
return _nodeDirection;
}
/**
* isInnerDirection : check the direction of the node is inner-direction
*/
Boolean isInnerDirection(PVector _nodeDirection, PVector _nodePosition) {
if (
_nodeDirection.x * _nodePosition.x < 0 ||
_nodeDirection.y * _nodePosition.y < 0
) {
return true;
}
return false;
}
/**
* updateNodex : update each nodes.
* set the direction of previous step equals to the direction of current step.
*/
public void updateNodes() {
for (int x = 0; x < cols; x++) {
for (int y = 0; y < rows; y++) {
nodes[x][y].setPrevDirection(getNodeCurrDirection(x, y));
}
}
}
/**
* setNodeDirection : set the nodes direction of current step.
*/
public void setNodeDirection(int _col, int _row, PVector _dir) {
nodes[_col][_row].setCurrDirection(_dir);
}
/**
* getNodePrevDirection : get the nodes direction of previous step.
*/
public PVector getNodePrevDirection(int _col, int _row) {
return nodes[_col][_row].prevDir;
}
/**
* getNodeCurrDirection : get the nodes direction of current step.
*/
public PVector getNodeCurrDirection(int _col, int _row) {
return nodes[_col][_row].currDir;
}
/**
* Node : holds nodes direction
*/
public class Node {
public PVector currDir; // direction of current step
public PVector prevDir; // direction of previous step
Node() {
currDir = new PVector(0, 0);
prevDir = new PVector(0, 0);
}
public void setCurrDirection(PVector _dir) {
currDir = _dir.copy();
}
public void setPrevDirection(PVector _dir) {
prevDir = _dir.copy();
}
}
}
/**
* Mover : mover on the grid.
* mover moves node to node.
*/
public class Mover {
private PVector start; // start col, row
private PVector direction; // moving direction
private int cols, rows; // grid cols, rows
private float hueVal; // mover color
private float mSize; // mover size
Mover(PVector _start, float _hue, int _cols, int _rows) {
update(_start, new PVector(0.0, 0.0));
hueVal = _hue % 360.0;
cols = _cols;
rows = _rows;
mSize = min(width / cols, height / rows) * 0.6;
}
/**
* update : update the start position and the direction of mover.
*/
public void update(PVector _start, PVector _dir) {
start = _start.copy();
direction = _dir.copy();
}
/**
* getEndPoint : calculate the end position of mover.
*/
public PVector getEndPoint() {
return new PVector(
folding(floor(start.x), floor(direction.x), cols),
folding(floor(start.y), floor(direction.y), rows)
);
}
/**
* draw : draw the mover.
* _rate : distance rate between the start and end position.
*/
public void draw(float _rate) {
float w = width / cols;
float h = height / rows;
float x = (w * (start.x + _rate * direction.x + 0.5) + width) % width;
float y = (h * (start.y + _rate * direction.y + 0.5) + height) % height;
// draws eye ball
fill(0.0, 0.0, 90.0, 100.0);
stroke(0.0, 0.0, 40.0, 100.0);
strokeWeight(5.0);
ellipse(x, y, mSize, mSize);
x += direction.x * mSize * 0.05;
y += direction.y * mSize * 0.05;
fill(hueVal, 40.0, 80.0, 100.0);
noStroke();
ellipse(x, y, mSize * 0.6, mSize * 0.6);
x += direction.x * mSize * 0.05;
y += direction.y * mSize * 0.05;
fill(0.0, 0.0, 90.0, 100.0);
ellipse(x, y, mSize * 0.2, mSize * 0.2);
}
}
/**
* Main routine
* no draw(), only setup(), and makes animation frame images
*/
void setup() {
size(720, 480);
colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
rectMode(CENTER);
smooth();
noLoop();
int cols = 8; // width must be divisible by cols
int rows = 6; // height must be divisible by rows
int stepMax = 16; // step = mover moving one node to other
int changeStep = 4; // route change timing
int frmMax = 24; // animation frames per step
float hueBase = random(360.0);
Grid grid = new Grid(cols, rows);
Mover mvs[] = new Mover[cols * rows];
// begin with north/south direction route
for (int x = 0; x < cols; x++) {
PVector d = (x % 2 == 0) ? grid.n.copy() : grid.s.copy();
for (int y = 0; y < rows; y++) {
grid.setNodeDirection(x, y, d);
mvs[x + y * cols] = new Mover(
new PVector(x, y),
hueBase + y * 45.0,
cols,
rows
);
}
}
// initial step
grid.updateNodes();
setMoverRoute(mvs, grid);
for (int frmCnt = 0; frmCnt < frmMax; frmCnt++) {
float frmRatio = map(frmCnt, 0, frmMax, 0.0, 1.0);
background(0.0, 0.0, 90.0, 100.0);
drawPath(grid, cols, rows, hueBase, frmRatio);
drawMovers(mvs, frmRatio);
casing();
saveFrame("frames/" + String.format("%03d", 0) + String.format("%03d", frmCnt) + ".png");
}
// steps with route changing
for (int stepCnt = 0; stepCnt < stepMax; stepCnt++) {
grid.updateNodes();
setMoverRoute(mvs, grid);
if (stepCnt % changeStep == 0) {
// random choice from the grid and change direction
grid.changeRoute(0.3);
}
for (int frmCnt = 0; frmCnt < frmMax; frmCnt++) {
float frmRatio = map(frmCnt, 0, frmMax, 0.0, 1.0);
background(0.0, 0.0, 90.0, 100.0);
drawPath(grid, cols, rows, hueBase, frmRatio);
drawMovers(mvs, frmRatio);
casing();
saveFrame("frames/" + String.format("%03d", stepCnt + 1) + String.format("%03d", frmCnt) + ".png");
}
}
exit();
}
/**
* drawPath : draw whole path of the mover
*/
void drawPath(Grid _grid, int _cols, int _rows, float _hue, float _frmRatio) {
float easeRatio = InFourthPow(_frmRatio);
float tr = easeRatio;
float fr = 1.0 - tr;
float cellW = width / _cols;
float cellH = height / _rows;
noFill();
stroke(0.0, 0.0, 40.0, 100.0);
strokeWeight(5.0);
for (int x = 0; x < _cols; x++) {
for (int y = 0; y < _rows; y++) {
// lerping from the previous direction to the current direction
PVector prevDir = _grid.getNodePrevDirection(x, y);
PVector currDir = _grid.getNodeCurrDirection(x, y);
// 0.0 : ease, HALF_PI : south
float rotPrev = constrain(prevDir.x, -1, 0) * PI + prevDir.y * HALF_PI;
float rotCurr = constrain(currDir.x, -1, 0) * PI + currDir.y * HALF_PI;
float rot = rotPrev * (1.0 - easeRatio) + rotCurr * easeRatio;
float rX = cellW * (0.5 + x + cos(rot) * 0.5);
float rY = cellH * (0.5 + y + sin(rot) * 0.5);
float rW = cellW * abs(currDir.x * tr + prevDir.x * fr);
float rH = cellH * abs(currDir.y * tr + prevDir.y * fr);
rect(rX, rY, rW, rH);
// folding canvas
if (rX + rW > width) {
line(0.0, rY, cellW * 0.5, rY);
}
if (rX - rW < 0.0) {
line(width, rY, width - cellW * 0.5, rY);
}
if (rY + rH > height) {
line(rX, 0.0, rX, cellH * 0.5);
}
if (rY - rH < 0.0) {
line(rX, height, rX, height - cellH * 0.5);
}
}
}
}
/**
* setMoverRoute : set the direction of each mover.
*/
void setMoverRoute(Mover[] _mvs, Grid _grid) {
for (Mover m : _mvs) {
PVector basePos = m.getEndPoint();
m.update(basePos, _grid.getNodeCurrDirection(floor(basePos.x), floor(basePos.y)));
}
}
/**
* drawMovers : draws each movers
*/
void drawMovers(Mover[] _mvs, float _frmRatio) {
for (Mover m : _mvs) {
m.draw(easeCos(_frmRatio));
}
}
/**
* folding : caluculate the position outside canvas edge
*/
int folding(int _base, int _add, int _edge) {
return (_base + _add + _edge) % _edge;
}
/**
* InFourthPow : easing function.
*/
private float InFourthPow(float _t) {
return 1.0 - pow(1.0 - _t, 4);
}
/**
* easeCos : easing function.
*/
private float easeCos(float _t) {
return 1.0 - cos(HALF_PI * _t);
}
/**
* casing : draw fancy casing
*/
private void casing() {
fill(0.0, 0.0, 0.0, 0.0);
strokeWeight(34.0);
stroke(0.0, 0.0, 50.0, 100.0);
rect(width * 0.5, height * 0.5, width, height);
strokeWeight(30.0);
stroke(0.0, 0.0, 100.0, 100.0);
rect(width * 0.5, height * 0.5, width, height);
noStroke();
noFill();
noStroke();
}
/*
Copyright (C) 2021- deconbatch
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
Yet another example images.
Turning directions.
The cycle path.