Skip to content

Instantly share code, notes, and snippets.

@claudiopro
Last active March 25, 2025 22:54
Show Gist options
  • Save claudiopro/24aacdfdfeb6b8c81bb52eca743e0b6f to your computer and use it in GitHub Desktop.
Save claudiopro/24aacdfdfeb6b8c81bb52eca743e0b6f to your computer and use it in GitHub Desktop.
Fibonacci Spiral with HTML, CSS, and vanilla JS
<!-- @format -->
<!--
The Fibonacci spiral is an approximation of the golden spiral created by drawing circular
arcs connecting the opposite corners of squares in the Fibonacci tiling, a tiling with
squares whose side lengths are successive Fibonacci numbers: 1, 1, 2, 3, 5, 8, 13, 21...
+-------------------------+----------------------------------------+
| | |
| | |
| | |
| | |
| | |
| | |
| 8 | |
| | |
| | |
| | |
| | 13 |
| | |
| | |
+----------------+-+------+ |
| |1| | |
| +-+ 2 | |
| |1| | |
| 5 +-+------+ |
| | | |
| | 3 | |
| | | |
| | | |
+----------------+--------+----------------------------------------+
-->
<!doctype html>
<html>
<head>
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<title>Fibonacci Spiral</title>
<style type="text/css">
:root {
font-family: Arial, Helvetica, sans-serif;
font-size: 12pt;
--spiral-stroke-width: 4px;
}
#controls {
align-items: center;
display: flex;
gap: 10px;
left: 0;
margin: 20px;
position: fixed;
top: 0;
z-index: 1;
}
.d {
box-sizing: border-box;
left: calc(50% + var(--offset-left, 0px));
overflow: hidden;
position: absolute;
top: calc(50% + var(--offset-top, 0px));
}
.debug .d {
border: 1px solid #000;
}
.d::before {
border: var(--spiral-stroke-width) solid #000;
border-radius: var(--size, 10px);
box-sizing: border-box;
content: '';
display: block;
height: calc(2 * var(--size, 10px));
transform: translate3d(var(--transform-x, 0px), var(--transform-y, -10px), 0);
width: calc(2 * var(--size, 10px));
}
</style>
<script>
const DEFAULT_ITERATIONS = 10;
function fibonacci(n) {
switch (n) {
case 0:
return 1;
case 1:
return 1;
default:
if (!fibonacci.hasOwnProperty('_cache')) {
fibonacci._cache = {};
}
let ret = fibonacci._cache[n];
if (!ret) {
ret = fibonacci(n - 1) + fibonacci(n - 2);
fibonacci._cache[n] = ret;
}
return ret;
}
}
function adjustZoom(el) {
const zoom = el.value;
requestAnimationFrame(() => {
const spiral = document.querySelector('#spiral');
if (spiral) {
const z = Math.max((100 - zoom) / 100, 1e-6);
spiral.style.zoom = z;
spiral.style.setProperty('--spiral-stroke-width', `${4 / z}px`);
}
});
}
function toggleDebug(el) {
document.body.classList[el.checked ? 'add' : 'remove']('debug');
}
function initSpiral(iterations) {
const spiral = document.querySelector('#spiral');
if (!spiral) {
return;
}
let offsetLeft = 0;
let offsetTop = 0;
let transformX = 0;
let transformY = -10;
let prevSize = 0;
let prevPrevSize = 0;
for (let i = 0; i < iterations; i += 1) {
const size = 10 * fibonacci(i);
const div = document.createElement('div');
div.id = `d${i + 1}`;
div.classList.add('d');
div.style.width = `${size}px`;
div.style.height = `${size}px`;
switch (i % 4) {
case 0:
transformX = 0;
transformY = -size;
if (i > 0) {
// FIXME: adjust initial offset to avoid this special case
offsetLeft -= size;
offsetTop -= prevPrevSize;
}
break;
case 1:
transformX = 0;
transformY = 0;
offsetTop -= size;
break;
case 2:
transformX = -size;
transformY = 0;
offsetLeft += prevSize;
break;
case 3:
transformX = -size;
transformY = -size;
offsetLeft -= prevPrevSize;
offsetTop += prevSize;
break;
}
div.style.setProperty('--size', `${size}px`);
div.style.setProperty('--offset-left', `${offsetLeft}px`);
div.style.setProperty('--offset-top', `${offsetTop}px`);
div.style.setProperty('--transform-x', `${transformX}px`);
div.style.setProperty('--transform-y', `${transformY}px`);
spiral.appendChild(div);
[prevPrevSize, prevSize] = [prevSize, size];
}
}
function updateIterations(el) {
const spiral = document.querySelector('#spiral');
if (!spiral) {
return;
}
while (spiral.children.length) {
spiral.removeChild(spiral.children[0]);
}
initSpiral(el.value);
}
window.addEventListener('load', function (e) {
initSpiral(DEFAULT_ITERATIONS);
});
</script>
</head>
<body class="_debug">
<section id="controls">
<label for="f_zoom">Zoom:</label>
<input
id="f_zoom"
type="range"
min="1"
max="99"
value="1"
step="0.1"
onchange="adjustZoom(this)"
oninput="adjustZoom(this)"
list="markers"
/>
<label for="f_iterations">Iterations:</label>
<input
id="f_iterations"
type="number"
min="1"
max="20"
value="10"
onchange="updateIterations(this)"
/>
<label for="f_debug">
<input
id="f_debug"
type="checkbox"
onchange="toggleDebug(this)"
/>
Debug mode</label
>
<datalist id="markers">
<option value="1"></option>
<option value="25"></option>
<option value="50"></option>
<option value="75"></option>
<option value="99"></option>
</datalist>
</section>
<section id="spiral"></section>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment