Ulam Spiral

by Chris Ball

HTML

<svg
	version="1.2"
	width="500"
	height="500"
	viewBox="0 0 500 500"
	xmlns="http://www.w3.org/2000/svg"
	preserveAspectRatio="xMidYMid meet"
	id="canvas"></svg>

CSS

#canvas {
	/* background-image: url(https://empslocal.ex.ac.uk/people/staff/mrwatkin/zeta/ulam.gif); */
    background-repeat: no-repeat;
    background-position: center;
	background-size: contain;
	height: auto;
	width: 100%;
}

JavaScript

function numbersUntil(limit, start = 1) {
	return Array.from({length: limit}, (_, i) => i + start)
}

function isSquare(n) {
	return n > 0 && Math.sqrt(n) % 1 === 0
}

const isPrime = num => {
	for (let i = 2, s = Math.sqrt(num); i <= s; i++) {
		if (num % i === 0) return false
	}
	return num > 1
}

function appendSVGChild(elementType, target, attributes = {}, text = '') {
	const element = document.createElementNS('http://www.w3.org/2000/svg', elementType)
	Object.entries(attributes).map(a => element.setAttribute(a[0],a[1]))

	if (text) {
		const textNode = document.createTextNode(text)
		element.appendChild(textNode)
	}

	target.appendChild(element)

	return element
}

const canvas = document.getElementById('canvas')
const canvasSize = canvas.getBoundingClientRect()
const canvasCentre = {
	x: canvasSize.width / 2,
	y: canvasSize.height / 2
}
const size = 30

function radius(num) {
	return isPrime(num) ? 0.5 : 0
}

class UlamSpiral {

	#points = []
	#currentValue = 1

	constructor(maxSideLength) {
		this.xpos = Math.ceil( maxSideLength / 2 )
		this.ypos = Math.ceil( maxSideLength / 2 )

		for (let currentSquareSize = 1; currentSquareSize <= maxSideLength; currentSquareSize++) {
			// For squares composed of even-numbered sides, the starting position is bottom right.
			if (currentSquareSize % 2 === 0) {
				this.xpos++
				this.drawRight(currentSquareSize)
				this.drawTop(currentSquareSize)
			}
			// For squares composed of odd-numbered sides, the starting position is top left.
			else {
				this.xpos--;
				this.drawLeft(currentSquareSize)
				this.drawBottom(currentSquareSize)
			}
		}
		
		return this.#points
	}

	drawBottom(currentSquareSize) {
		for (let currSideLength = 1; currSideLength < currentSquareSize; currSideLength++) {
			this.xpos++
			this.setPoint(this.#currentValue)
			this.#currentValue++
		}
	}

	drawRight(currentSquareSize) {
		for (let currSideLength = 1; currSideLength <= currentSquareSize; currSideLength++)...