This Archimedian spiral highlights the patterns made by prime numbers when plotted radially.
The spiral is split into segments, and each segment represents a positive number from 1 to 10,000 radiating out from the centre. The segments are aligned such that each square number (1, 4, 9, 25…) has its starting edge positioned along the horizontal axis, moving right from the spiral’s centre. For each prime number, the corresponding segment is coloured white.
Several subspirals featuring densely packed prime numbers are visible. It’s also interesting to note that for all n > 2, n² − 1 is also not prime, which can be seen from the empty band above those representing the squares.
The code to render this diagram is available .
Get occasional updates about new fonts, designs and other interesting things.
import primes from "./primes.asset.mjs"
const tau = 2 * Math.PI
// Create an array filled with numbers `a` up to but not including `b`
const count = (a, b) => Array.from({ length: b - a }, (e, i) => a + i)
// Number of consecutive rings in the (Archimedian) spiral
const rings = 100
// Filter function to find edges by looking for first occurance of `n`
const edges = (a, i, list) => list.find((b) => a.n === b.n) === a
export default async (canvas, isNode) => {
let palette
if (isNode) {
palette = (await import("../_lib/palette.asset.mjs")).default
} else {
canvas.style.background = ""
canvas.style.width = canvas.style.height = "100%"
canvas.width = canvas.height = canvas.width * window.devicePixelRatio
palette = (await import("../../_lib/palette.asset.mjs")).default
}
const ctx = canvas.getContext("2d")
const radius = canvas.width / 2
const gap = radius / (rings + 1)
const spiral = []
// Calculate all points needed to draw a smooth spiral
count(1, rings).forEach((i) => {
// At each full turn, the integer is the square of the ring number
const a = Math.pow(i - 1, 2)
const b = Math.pow(i, 2)
count(a, b).forEach((n) => {
// As the spiral spreads outwards, the line becomes
// less curved and fewer intermediary points are needed
const steps = Math.max(40 - n / 6, 2)
count(0, steps).forEach((i) => {
const k = Math.sqrt(n + i / (steps - 1))
const r = gap * (k + 0.5)
const t = k * tau
const x = r * Math.cos(t)
const y = r * Math.sin(t)
spiral.push({ x, y, t, n })
})
})
})
// Calculate positions from the center of the canvas.
ctx.save()
ctx.translate(radius, radius)
// Draw the spiral
ctx.beginPath()
spiral.forEach(({ x, y }) => ctx.lineTo(x, y))
ctx.strokeStyle = palette.dark
ctx.lineCap = "square"
ctx.lineWidth = gap / 2
ctx.stroke()
// Draw a perpendicular line at each integer point along the spiral
ctx.beginPath()
spiral
.filter(edges)
.slice(1)
.concat(spiral.slice(-1))
.forEach(({ x, y, t }) => {
ctx.save()
ctx.translate(x, y)
ctx.rotate(t)
ctx.moveTo(0, 0)
ctx.lineTo(-gap, 0)
ctx.restore()
})
ctx.lineCap = "butt"
ctx.stroke()
// Fill in each segment that matches the pattern by drawing the
// line segment offset from the original spiral
ctx.beginPath()
spiral
.filter(edges)
.filter((p) => primes.includes(p.n))
.forEach((p) => {
const a = spiral.indexOf(p)
const b = spiral.findIndex((s) => s.n === p.n + 1)
spiral.slice(a, b).forEach((p, i) => {
const dx = -(gap / 2) * Math.cos(p.t)
const dy = -(gap / 2) * Math.sin(p.t)
ctx[i === 0 ? "moveTo" : "lineTo"](p.x + dx, p.y + dy)
})
})
ctx.globalCompositeOperation = "destination-over"
ctx.strokeStyle = palette.canvas
ctx.stroke()
ctx.restore()
ctx.globalCompositeOperation = "destination-over"
ctx.fillStyle = palette.highlight
ctx.fillRect(0, 0, canvas.width, canvas.height)
}