Skip to content

Natural Knight tiling

In 2025, I was intrigued by the polygons formed out of chess knight moves on an integer grid. I produced a whole series of "knight tilings".

This sketch is a self-assembling crystal of the three smallest knight polygons.

Click to regenerate.

A tiling of small, colorful rhombs

function setup() {
  createCanvas(windowWidth, windowHeight);
  strokeJoin(ROUND)
  mousePressed()
}

let step = 6
let boundary = [] // List of points + edge directions + point's magSq
let polygons = []

function mousePressed() {
  colorMode(RGB)
  background(255);
  stroke(0)
  colorMode(HSB)
  strokeWeight(1)
  boundary = [
    [createVector(0, 0), createVector(1, 2), 0],
    [createVector(1, 2), createVector(-1, -2), 5]
  ]
  loop()
}

function draw() {
  let frameStart = performance.now() 
  for (let i = 0; i < 1000; i ++) {
    if (i%4 == 0 && performance.now() - frameStart > 1000 / 60 - 2) {
      break;
    }
    let polygon = expandBoundary()
    if (!polygon) {
      noLoop()
      return
    }
    let area = polygon.map((_, i, pts) => pts[i].x * pts[(i + 1) % pts.length].y - pts[i].y * pts[(i + 1) % pts.length].x).reduce((a,b) => a + b) / 2
    //fill(random(360), random(0, 100), random(80, 100))
    fill(((random() + area) * 360 / 3 + 32) % 360, 30, 100)

    //circle(edge[0].x * 10 + width / 2, edge[0].y * 10 + height / 2, 10)
    beginShape()
    for (let pt of polygon) {
      vertex(pt.x * step + width / 2, pt.y * step + height / 2)
    }
    endShape(CLOSE)
  }
}

function getMinBoundaryPoint() {
  while(true) {
    let minI = -1
    let minD = Infinity
    for (let i = 0; i < boundary.length; i ++) {
      if (abs(boundary[i][0].x) > width / 2 / step || abs(boundary[i][0].y) > height / 2 / step) {
        continue
      }
      let d = boundary[i][2]
      if (d < minD) {
        minI = i
        minD = d
      }
    }
    if (minI == -1) return []
    let prevI = (minI - 1 + boundary.length) % boundary.length
    let nextI = (minI + 1) % boundary.length
    
    if (boundary.length > 2 && p5.Vector.add(boundary[minI][1], boundary[prevI][1]).magSq() < 1) {
      // Denormalized boundary; remove extra <-> edges
      if (minI > 0) {
        boundary.splice(minI - 1, 2)
      } else {
        boundary.pop()
        boundary.splice(minI, 1)
      }
      continue
    }
    return [minI, prevI, nextI]
  }
}
function rotateMove(move1) {
  return (
    move1.y * 2  == move1.x ? createVector(move1.x, -move1.y) :
    move1.x * 2 == -move1.y ? createVector(-move1.x, move1.y) : 
    move1.y * move1.x < 0 ? createVector(-move1.y, -move1.x) :
    createVector(move1.y, move1.x)
  )
}
function expandBoundary() {
  let [minI, prevI, nextI] = getMinBoundaryPoint()
  if (minI === undefined) return undefined
  
  let move1 = boundary[minI][1]
  
  let move2Max = p5.Vector.mult(boundary[prevI][1], -1)
  let move2 = rotateMove(move1)
  if (p5.Vector.sub(move2, move2Max).magSq() > 1 && random() < 0.5) {
    move2 = rotateMove(move2)
  }
  // if (p5.Vector.sub(move2, move2Max).magSq() > 1 && random() < 0.5) {
  //   move2 = rotateMove(move2)
  // }
  let move2Neg = p5.Vector.mult(move2, -1)
  
  let p0 = boundary[minI][0]
  let p1 = p5.Vector.add(p0, move2)
  let p2 = p5.Vector.add(p1, move1)
  let p3 = p5.Vector.add(p2, move2Neg)
  
  boundary.splice(minI, 1, [p0, move2, p0.magSq()], [p1, move1, p1.magSq()], [p2, move2Neg, p2.magSq()])
  
  return [boundary[minI][0], p1, p2, p3]
}

function debugBoundary() {
  push()
  noFill()
  stroke("red")
  let [minI, prevI, nextI] = getMinBoundaryPoint()
  for (let i = 0; i < boundary.length; i ++) {
    if (minI == i) stroke("blue")
    else if (prevI == i) stroke("green")
    else stroke("red")
    let [pt, edge] = boundary[i]
    line(pt.x * 10 + width / 2, pt.y * 10 + height / 2, (pt.x + edge.x / 2) * 10 + width / 2, (pt.y + edge.y / 2) * 10 + height / 2)
  }
  pop()
}

(Originally seen at https://editor.p5js.org/bojidar-bg/sketches/OFgChcuFx and https://mastodon.social/@bojidar_bg/114420922582844249)

Experiments tagged p5 (32/85)

|← Experiments tagged tiling (1/6)

Experiments on this site (32/85)