Custom Code Experiment •
Dragon Curve
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dragon Curve</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #05050f;
overflow: hidden;
width: 100vw;
height: 100vh;
font-family: 'Space Mono', monospace;
cursor: crosshair;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
#ui {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none;
}
#title {
position: fixed;
top: 32px;
left: 40px;
pointer-events: none;
}
#title h1 {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.35em;
text-transform: uppercase;
color: rgba(180, 200, 255, 0.9);
}
#title p {
font-size: 10px;
letter-spacing: 0.2em;
color: rgba(100, 130, 200, 0.55);
margin-top: 4px;
}
#depth-ctrl {
position: fixed;
bottom: 32px;
left: 40px;
pointer-events: all;
display: flex;
flex-direction: column;
gap: 8px;
}
#depth-ctrl label {
font-size: 10px;
letter-spacing: 0.2em;
color: rgba(100, 130, 200, 0.6);
text-transform: uppercase;
}
#depth-ctrl .controls {
display: flex;
align-items: center;
gap: 12px;
}
#depth-ctrl button {
width: 28px;
height: 28px;
background: transparent;
border: 1px solid rgba(100, 140, 255, 0.3);
color: rgba(160, 190, 255, 0.8);
font-family: 'Space Mono', monospace;
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.2s, background 0.2s;
line-height: 1;
}
#depth-ctrl button:hover {
border-color: rgba(100, 160, 255, 0.8);
background: rgba(100, 140, 255, 0.1);
}
#depth-val {
font-size: 18px;
font-weight: 700;
color: rgba(180, 210, 255, 0.9);
min-width: 24px;
text-align: center;
}
#status {
position: fixed;
bottom: 32px;
right: 40px;
text-align: right;
pointer-events: none;
}
#status p {
font-size: 10px;
letter-spacing: 0.18em;
color: rgba(100, 130, 200, 0.45);
text-transform: uppercase;
line-height: 1.9;
}
#status span {
color: rgba(150, 180, 255, 0.7);
}
#hover-hint {
position: fixed;
top: 32px;
right: 40px;
font-size: 10px;
letter-spacing: 0.2em;
color: rgba(100, 130, 200, 0.45);
text-transform: uppercase;
pointer-events: none;
transition: opacity 0.6s;
}
.dot {
position: fixed;
width: 4px;
height: 4px;
border-radius: 50%;
background: rgba(120, 160, 255, 0.4);
pointer-events: none;
}
.dot:nth-child(1) { top: 32px; left: 34px; }
.dot:nth-child(2) { bottom: 32px; left: 34px; }
.dot:nth-child(3) { bottom: 32px; right: 34px; }
.dot:nth-child(4) { top: 32px; right: 34px; }
</style>
</head>
<body>
<canvas id="cv"></canvas>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div id="title">
<h1>Dragon Curve</h1>
<p>Heighway fractal — paper folding sequence</p>
</div>
<div id="hover-hint">Move mouse to shift spectrum</div>
<div id="depth-ctrl">
<label>Depth</label>
<div class="controls">
<button id="btn-dec">−</button>
<span id="depth-val">14</span>
<button id="btn-inc">+</button>
</div>
</div>
<div id="status">
<p>Segments <span id="seg-count">—</span></p>
<p>Iterations <span id="iter-count">—</span></p>
</div>
<script>
(function(){
const canvas = document.getElementById('cv');
const ctx = canvas.getContext('2d');
let W, H;
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
draw();
}
let depth = 14;
let mouseNX = 0.5, mouseNY = 0.5, hovered = false;
let animFrame = null;
function buildDragon(d) {
let turns = [1];
for (let i = 1; i < d; i++) {
const len = turns.length;
turns.push(1);
for (let j = len - 1; j >= 0; j--) {
turns.push(1 - turns[j]);
}
}
return turns;
}
function computePoints(turns) {
const stepMap = [[1,0],[0,1],[-1,0],[0,-1]];
let px = 0, py = 0, facing = 0;
const pts = [{x: px, y: py}];
for (const t of turns) {
facing = t === 1 ? (facing + 1) % 4 : (facing + 3) % 4;
px += stepMap[facing][0];
py += stepMap[facing][1];
pts.push({x: px, y: py});
}
return pts;
}
function scalePoints(pts) {
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
for (const p of pts) {
if (p.x < minX) minX = p.x;
if (p.x > maxX) maxX = p.x;
if (p.y < minY) minY = p.y;
if (p.y > maxY) maxY = p.y;
}
const pad = 0.1;
const rangeX = maxX - minX;
const rangeY = maxY - minY;
const scaleX = W * (1 - 2 * pad) / rangeX;
const scaleY = H * (1 - 2 * pad) / rangeY;
const scale = Math.min(scaleX, scaleY);
const ox = (W - rangeX * scale) / 2 - minX * scale;
const oy = (H - rangeY * scale) / 2 - minY * scale;
return pts.map(p => ({ x: p.x * scale + ox, y: p.y * scale + oy }));
}
let scaledPts = [];
function rebuild() {
const turns = buildDragon(depth);
const raw = computePoints(turns);
scaledPts = scalePoints(raw);
document.getElementById('seg-count').textContent = (scaledPts.length - 1).toLocaleString();
document.getElementById('iter-count').textContent = Math.pow(2, depth).toLocaleString();
document.getElementById('depth-val').textContent = depth;
draw();
}
function hsl(h, s, l) {
h = ((h % 360) + 360) % 360;
s = Math.max(0, Math.min(100, s));
l = Math.max(0, Math.min(100, l));
const sl = s / 100, ll = l / 100;
const a = sl * Math.min(ll, 1 - ll);
const f = n => {
const k = (n + h / 30) % 12;
return ll - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)));
};
const r = Math.round(f(0) * 255);
const g = Math.round(f(8) * 255);
const b = Math.round(f(4) * 255);
return `rgb(${r},${g},${b})`;
}
function getColor(t, hovered, mx, my) {
if (!hovered) {
return hsl(210 + t * 150, 70, 38 + t * 22);
} else {
const h = (mx * 300 + t * 280 + my * 80) % 360;
return hsl(h, 88 + my * 12, 38 + t * 30);
}
}
function draw() {
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#05050f';
ctx.fillRect(0, 0, W, H);
const n = scaledPts.length - 1;
if (n < 1) return;
const lw = hovered ? 1.3 : 0.95;
ctx.lineWidth = lw;
const BATCH = Math.max(1, Math.floor(n / 1200));
for (let i = 0; i < n; i += BATCH) {
const t = i / n;
ctx.beginPath();
ctx.moveTo(scaledPts[i].x, scaledPts[i].y);
const end = Math.min(i + BATCH, n);
ctx.lineTo(scaledPts[end].x, scaledPts[end].y);
ctx.strokeStyle = getColor(t, hovered, mouseNX, mouseNY);
ctx.stroke();
}
}
window.addEventListener('resize', resize);
canvas.addEventListener('mousemove', e => {
mouseNX = e.clientX / W;
mouseNY = e.clientY / H;
if (!hovered) {
hovered = true;
document.getElementById('hover-hint').style.opacity = '0';
}
if (animFrame) cancelAnimationFrame(animFrame);
animFrame = requestAnimationFrame(draw);
});
canvas.addEventListener('mouseleave', () => {
hovered = false;
document.getElementById('hover-hint').style.opacity = '1';
draw();
});
document.getElementById('btn-inc').addEventListener('click', () => {
if (depth < 18) { depth++; rebuild(); }
});
document.getElementById('btn-dec').addEventListener('click', () => {
if (depth > 1) { depth--; rebuild(); }
});
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
rebuild();
})();
</script>
</body>
</html>