Search code examples
p5.jsgenerative-art

Adding colors to Hitomezashi stitch pattern in p5.js


I have recreated this procedural drawing from this Numberphile episode https://www.youtube.com/watch?v=JbfhzlMk2eY&ab_channel=Numberphile.

It generates lines which are either offset or not depending on some random sequence true and false, to generate these interesting patterns.

My next goal is now to be able to play with the color of these patterns, for example making one cluster darker depending on how big the cluster is, I really don't have a clue how to go on from here so any kind of help would be much appreciated.

enter image description here

function setup() {
  var canvas = createCanvas(1600,800);
  // Move the canvas so it’s inside our <div id="sketch-holder">.
  canvas.parent('sketch-holder');
  background(255, 0, 200);
  
}


var _horzRows = 17*2;
var _horzCols = 17*2;
var _vertRows = 8*2;
var _vertCols = 34*2;

var rows = [1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0];
var cols = [1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0];

var x = 0;
var y = 0;
var xOffset = 0;
var yOffset = 0;

function horzGrid(l, t, stitchLength) {
  for (var j = 0; j < _horzCols; j++) {
    for (var k = 0; k < _horzRows; k++) {
      x = l + j*(stitchLength*2); // stitch + skip
      y = t + k*(stitchLength);
      if (rows[k] == 1) {
        xOffset = stitchLength;
      } else {
        xOffset = 0;
      }
      line(x+xOffset, y, x+xOffset + stitchLength, y);
    }
  }
}

function vertGrid(l, t, stitchLength) {
  for (var m = 0; m < _vertCols; m++) {
    for (var n = 0; n < _vertRows; n++) {
      x = l + m*(stitchLength);
      y = t + n*(stitchLength*2); // stitch + skip
      if (cols[m] == 1) {
        yOffset = stitchLength;
      } else {
        yOffset = 0;
      }
      line(x, y+yOffset, x, y+yOffset + stitchLength);
    }
  }
}


function draw() {
  horzGrid(30, 40, 25);
  vertGrid(30, 40, 25);
  size(920, 480);
  background(255);
  strokeWeight(2);
  
}

Solution

  • Following up on my comment, you can do this by creating a graph data structure.

    Each cell has properties u, d, l and r to denote whether the cell has a wall along that side of its perimeter. If it doesn't have a wall, then consider the cells connected by the missing wall to be neighbors. Otherwise, they're not neighbors.

    This creates a graph structure, where each vertex is a cell and there are edges to all neighbor cells.

    The next step is to randomly populate the links between cells using the Hitomezashi algorithm described in the video.

    Finally, with the graph data structure in place, run a flood fill from each cell to determine the size of its connected component. The size of the component will determine its color, which is then passed into a second flood fill that assigns the colors for each group of connected cells.

    Finally, iterate and draw all of the cells.

    Note that I haven't had time to DRY this code out, so it's rather messy. The recursion is also unsafe on large grids, so consider this a proof of concept.

    const w = document.documentElement.clientWidth;
    const h = document.documentElement.clientHeight;
    
    const gridSize = 10;
    const grid = [...Array(~~(h / gridSize) + 1)].map(() =>
      [...Array(~~(w / gridSize) + 1)].map(() => ({
        u: false, d: false, l: false, r: false,
      }))
    );
    
    const addHitomezashi = grid => {
      for (let i = 0; i < grid.length; i++) {
        const offset = ~~random(2);
    
        for (let j = offset; j < grid[i].length; j += 2) {
          grid[i][j].d = true;
    
          if (grid[i+1]) {
            grid[i+1][j].u = true;
          }
        }
      }
    
      for (let j = 0; j < grid[0].length; j++) {
        const offset = ~~random(2);
    
        for (let i = offset; i < grid.length; i += 2) {
          grid[i][j].r = true;
    
          if (grid[i][j+1]) {
            grid[i][j+1].l = true;
          }
        }
      }
    };
    
    const colorHitomezashi = grid => {
      const visited = new Set();
      const getSize = (x, y) => {
        if (x < 0 || y < 0 || 
            y >= grid.length || x >= grid[y].length ||
            visited.has(`${x} ${y}`)) {
          return 0;
        }
    
        let size = 0;
        visited.add(`${x} ${y}`);
    
        if (!grid[y][x].u) {
          size = max(size, getSize(x, y - 1));
        }
        if (!grid[y][x].d) {
          size = max(size, getSize(x, y + 1));
        }
        if (!grid[y][x].l) {
          size = max(size, getSize(x - 1, y));
        }
        if (!grid[y][x].r) {
          size = max(size, getSize(x + 1, y));
        }
    
        return size + 1;
      };
    
      const floodFill = (x, y, color) => {
        if (x < 0 || y < 0 ||
            y >= grid.length || x >= grid[y].length ||
            grid[y][x].color !== undefined) {
          return 0;
        }
    
        grid[y][x].color = color;
    
        if (!grid[y][x].u) {
          floodFill(x, y - 1, color);
        }
        if (!grid[y][x].d) {
          floodFill(x, y + 1, color);
        }
        if (!grid[y][x].l) {
          floodFill(x - 1, y, color);
        }
        if (!grid[y][x].r) {
          floodFill(x + 1, y, color);
        }
      };
    
      for (let i = 0; i < grid.length; i++) {
        for (let j = 0; j < grid[i].length; j++) {
          const color = 180 - getSize(j, i);
          floodFill(j, i, color);
        }
      }
    };
    
    function setup() {
      createCanvas(w, h);
      noLoop();
      addHitomezashi(grid);
      colorHitomezashi(grid);
    }
    
    function draw() {
      for (let i = 0; i < grid.length; i++) {
        for (let j = 0; j < grid[i].length; j++) {
          const y = i * gridSize + 0.5;
          const x = j * gridSize + 0.5;
          
          fill(grid[i][j].color);
          noStroke();
          rect(x, y, gridSize + 1, gridSize + 1);
          stroke(0);
    
          if (grid[i][j].u) {
            line(x, y, x + gridSize, y);
          }
          if (grid[i][j].d) {
            line(x, y + gridSize, x + gridSize, y + gridSize);
          }
          if (grid[i][j].l) {
            line(x, y, x, y + gridSize);
          }
          if (grid[i][j].r) {
            line(x + gridSize, y, x + gridSize, y + gridSize);
          }
        }
      }
    }
    body {
      margin: 0;
    }
    canvas {
      border: 1px solid black;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.js"></script>

    Next step: implement this on the isometric grid described in the video!