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.
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)
Browse more articles?
← Splats Experiments tagged p5 (32/85) Teleportation effect →
|← Experiments tagged tiling (1/6) Checkerboard animation 1 →
← Splats Experiments on this site (32/85) Teleportation effect →