Last active
March 25, 2025 22:54
-
-
Save claudiopro/24aacdfdfeb6b8c81bb52eca743e0b6f to your computer and use it in GitHub Desktop.
Fibonacci Spiral with HTML, CSS, and vanilla JS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- @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