Paved prime spiral

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 .

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)
}