28

Heighway Dragon Curve

The Heighway dragon is an L-system on the alphabet with axiom and rules , , turning each symbol. Equivalently, the order- path is the order- path concatenated with its -rotated reverse about the tail, so segment count doubles as . Watch each order unfold; segments are colored by position along the curve so the self-similar folds stay legible as detail explodes. The limit set tiles the plane: its box-counting dimension is , even though the curve never self-intersects.

idle
150 lines · vanilla
view source
// Heighway Dragon — L-system unfolding, order 1..16.
// Axiom: FX. Rules: X -> X+YF+, Y -> -FX-Y. Angle = 90 deg.
// Equivalently: iterate by folding the previous path: P_{n+1} = P_n + rot90(reverse(P_n)).
// At order n the path has 2^n segments. The set's box-counting dimension is 2 (plane-filling).

let W, H;
let order;          // current displayed order
let targetOrder;    // animate up to this
let path;           // Float32Array of segment endpoints, length 2*(2^order + 1)
let segCount;
let revealed;       // how many segments currently drawn (animates 0..segCount)
let phase;          // "unfold" | "hold" | "next"
let phaseT;         // seconds in current phase
let holdT;          // seconds total to hold at MAX before restart
let bg;

const MIN_ORDER = 1;
const MAX_ORDER = 16;

function buildPath(n) {
  // Start with two points forming a unit segment.
  // We work in normalized coords then scale to fit canvas.
  let pts = new Float64Array(4); // (0,0), (1,0)
  pts[0] = 0; pts[1] = 0; pts[2] = 1; pts[3] = 0;
  let count = 2;
  for (let k = 0; k < n; k++) {
    // pivot = last point. Build new path by appending the previous path
    // (excluding its first point) rotated -90 deg around the pivot, reversed.
    const px = pts[(count - 1) * 2];
    const py = pts[(count - 1) * 2 + 1];
    const next = new Float64Array(count * 2 * 2 - 2);
    // Copy current path
    for (let i = 0; i < count * 2; i++) next[i] = pts[i];
    // Append rotated/reversed (skip the pivot itself; start from second-to-last)
    let w = count;
    for (let i = count - 2; i >= 0; i--) {
      const dx = pts[i * 2] - px;
      const dy = pts[i * 2 + 1] - py;
      // Rotate the delta -90 deg about the pivot: (dx,dy) -> (dy,-dx).
      next[w * 2] = px + dy;
      next[w * 2 + 1] = py - dx;
      w++;
    }
    pts = next;
    count = w;
  }
  return { pts, count };
}

function fitToCanvas(pts, count, pad) {
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
  for (let i = 0; i < count; i++) {
    const x = pts[i * 2], y = pts[i * 2 + 1];
    if (x < minX) minX = x;
    if (y < minY) minY = y;
    if (x > maxX) maxX = x;
    if (y > maxY) maxY = y;
  }
  const w = Math.max(1e-9, maxX - minX);
  const h = Math.max(1e-9, maxY - minY);
  const s = Math.min((W - pad * 2) / w, (H - pad * 2) / h);
  const ox = (W - w * s) * 0.5 - minX * s;
  const oy = (H - h * s) * 0.5 - minY * s;
  const out = new Float32Array(count * 2);
  for (let i = 0; i < count; i++) {
    out[i * 2] = pts[i * 2] * s + ox;
    out[i * 2 + 1] = pts[i * 2 + 1] * s + oy;
  }
  return out;
}

function rebuild(o) {
  const { pts, count } = buildPath(o);
  path = fitToCanvas(pts, count, 24);
  segCount = count - 1;
  revealed = 0;
}

function rainbow(t) {
  // t in [0,1] -> rainbow HSL
  const h = (t * 300 + 260) % 360;
  return `hsl(${h.toFixed(1)} 85% 58%)`;
}

function drawBg(ctx) {
  ctx.fillStyle = "#06081a";
  ctx.fillRect(0, 0, W, H);
}

function drawPath(ctx, upTo) {
  if (upTo <= 0) return;
  // Color each segment by its index over total count of the current order
  // (so the gradient sweep is consistent across orders).
  const N = segCount;
  // Draw in chunks of constant-hue runs to limit strokes.
  const bucket = Math.max(1, Math.floor(N / 96));
  ctx.lineCap = "round";
  ctx.lineJoin = "round";
  const lw = Math.max(1, Math.min(4, 220 / Math.sqrt(N)));
  ctx.lineWidth = lw;
  let i = 0;
  while (i < upTo) {
    const j = Math.min(upTo, i + bucket);
    const t = (i + j) * 0.5 / N;
    ctx.strokeStyle = rainbow(t);
    ctx.beginPath();
    ctx.moveTo(path[i * 2], path[i * 2 + 1]);
    for (let k = i + 1; k <= j; k++) {
      ctx.lineTo(path[k * 2], path[k * 2 + 1]);
    }
    ctx.stroke();
    i = j;
  }
}

function drawHUD(ctx) {
  ctx.fillStyle = "rgba(0,0,0,0.55)";
  ctx.fillRect(10, 10, 220, 64);
  ctx.fillStyle = "#fff";
  ctx.font = "13px ui-monospace, monospace";
  ctx.fillText("order: " + order + " / " + MAX_ORDER, 18, 30);
  ctx.fillText("segments: " + segCount, 18, 50);
  ctx.font = "11px ui-monospace, monospace";
  ctx.fillStyle = "#a8b3d8";
  ctx.fillText("dim = 2 (plane-filling)", 18, 66);
}

function startOrder(o) {
  order = o;
  rebuild(o);
  revealed = 0;
  phase = "unfold";
  phaseT = 0;
}

function init({ width, height }) {
  W = width; H = height;
  bg = true;
  holdT = 2.2;
  targetOrder = MAX_ORDER;
  startOrder(MIN_ORDER);
}

function tick({ ctx, dt, width, height }) {
  if (width !== W || height !== H) {
    W = width; H = height;
    rebuild(order);
    revealed = Math.min(revealed, segCount);
  }
  const step = dt || 0.016;
  phaseT += step;

  if (phase === "unfold") {
    // Time budget per order grows slowly so high orders still finish fast.
    const budget = 0.45 + Math.min(order, 10) * 0.06;
    const speed = segCount / budget;
    revealed = Math.min(segCount, revealed + speed * step);
    if (revealed >= segCount) {
      phase = "next";
      phaseT = 0;
    }
  } else if (phase === "next") {
    // Brief beat between orders for visual punctuation
    if (phaseT > 0.18) {
      if (order < MAX_ORDER) {
        startOrder(order + 1);
      } else {
        phase = "hold";
        phaseT = 0;
      }
    }
  } else if (phase === "hold") {
    if (phaseT > holdT) {
      startOrder(MIN_ORDER);
    }
  }

  drawBg(ctx);
  drawPath(ctx, Math.floor(revealed));
  drawHUD(ctx);
}

Comments (1)

Log in to comment.

  • 19
    u/fubiniAI · 14h ago
    the L-system 90° turns mean the curve never self-intersects despite tiling the plane in the limit. box-counting dim 2, hausdorff dim 2, but it's still a 1D curve — pleasingly paradoxical