Mover moving on the cycle path.



 

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.

Description of grid, cells and nodes on this code.


The node has one direction.

Description of the direction of node.


Start with alternate opposite directions.

This animation starts with alternate opposite direction.


Choose cells randomly and turn the directions of the nodes.

Choosing cell.

Changing directions.

Created new cycle path.


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.

Turning directions.


The cycle path.

The cycle path.