4.1.2 Dragon Curve
Generate the Heighway dragon — a recursive L/R fold sequence rendered with turtle graphics. One self-similar rule, 2,047 turns, an unmistakable silhouette.
Overview
The Heighway dragon curve is what you get when you fold a paper strip in half repeatedly, always in the same direction, then unfold each crease to exactly 90°. NASA physicist John Heighway first investigated it in 1966, and Martin Gardner popularised it in his Scientific American column the next year [1]. What makes it irresistible: the entire curve, no matter how complex, is encoded as a string of Ls and Rs built by one short recursive rule. Render the string with turtle graphics — move forward, turn left or right — and a self-similar dragon emerges that never crosses itself and tiles the plane.
This lesson sits at the conceptual end of Module 04. Where 4.1.1 used recursion to place pixels, 4.1.2 uses recursion to construct a sequence of instructions and only then renders it. That separation — generation rule vs. interpretation — is the same architectural move you will see again in Mandelbrot iteration (4.1.3) and L-systems (4.3.1).
Learning objectives
- Apply the recursive rule “$D_n$ = previous sequence + $R$ + reversed-and-inverted previous sequence” to build the dragon sequence.
- Render an
L/Rturn sequence on a NumPy canvas using a turtle-graphics interpreter. - Predict the sequence length for any depth using the formula 2 to the power (n+1), minus 1.
- Recognise the dragon curve as a separation of generation (the recursive string) from interpretation (the turtle walk) — the same pattern that drives L-systems.
Quick start — see the dragon emerge
import numpy as np
from PIL import Image
LEFT, UP, RIGHT, DOWN = range(4)
def turn_left(d): return (d + 3) % 4
def turn_right(d): return (d + 1) % 4
def generate_dragon_sequence(initial_turn, depth):
if depth == 0:
return initial_turn
previous = generate_dragon_sequence(initial_turn, depth - 1)
inverted = ''.join('L' if c == 'R' else 'R' for c in previous[::-1])
return previous + 'R' + inverted
# Depth 10 → 2047 turn instructions
sequence = generate_dragon_sequence('R', 10)
print(f"Sequence length: {len(sequence)}")
Core concepts
Concept 1 — Paper-folding as recursion
Fold a paper strip in half once. Unfold it so the crease opens to 90°: one right turn. Fold it twice in the same direction, then unfold each crease to 90°: the sequence is R R L — two right turns then one left turn [2]. The number of creases doubles with every fold, and the pattern is exactly the dragon curve at that depth.
The construction rule that captures this is recursive: take the previous depth’s sequence, append a single R (the brand-new middle fold), then append the reversed and inverted previous sequence. “Inverted” means swap every L with R and vice versa — this captures the fact that, after the new fold, the second half of the paper now points in the opposite direction:
$D_0 = R$, and at depth $n$, $D_n$ is the previous depth’s sequence, then
R, then the previous depth’s sequence reversed and inverted.
Concept 2 — Generating the sequence
The recursive construction translates directly to two short Python functions:
def invert_sequence(s):
"""Swap every L for R and vice versa."""
return ''.join('L' if c == 'R' else 'R' for c in s)
def generate_dragon_sequence(initial_turn, depth):
if depth == 0:
return initial_turn
previous = generate_dragon_sequence(initial_turn, depth - 1)
second_half = invert_sequence(previous[::-1])
return previous + 'R' + second_half
Trace the first few iterations by hand:
| Depth | Sequence | Length |
|---|---|---|
| 0 | R | 1 |
| 1 | R R L | 3 |
| 2 | RRL R RLL | 7 |
| 3 | RRLRRLL R RRLRLLL | 15 |
| 4 | RRLRRLLRRRLRLLL R RRLRRLRRRLLRLLL | 31 |
The bolded middle R is the brand-new fold introduced at each depth — every other character was either inherited from the previous row or is a reversed-inverted copy of it.
Concept 3 — Turtle interpretation
The sequence tells you how to turn. To actually draw anything, an imaginary “turtle” walks across the canvas:
- Forward — move one step in the current direction, drawing pixels along the way.
L— turn 90° counter-clockwise.R— turn 90° clockwise.
Directions are integers 0..3 so rotation is a (d ± 1) % 4 operation. Forward motion is a slice assignment along the matching axis — canvas[y, x:x+s] for a rightward step, and so on:
def move_forward(canvas, position, step_size, direction, color):
x, y = position
if direction == RIGHT:
canvas[y, x:x + step_size] = color
return (x + step_size, y)
elif direction == LEFT:
canvas[y, x - step_size:x] = color
return (x - step_size, y)
elif direction == UP:
canvas[y - step_size:y, x] = color
return (x, y - step_size)
elif direction == DOWN:
canvas[y:y + step_size, x] = color
return (x, y + step_size)
def draw_dragon(canvas, sequence, start, step, direction, color):
pos = start
for turn in sequence:
pos = move_forward(canvas, pos, step, direction, color)
direction = turn_right(direction) if turn == 'R' else turn_left(direction)
move_forward(canvas, pos, step, direction, color)
Exercises
Three exercises in Execute → Modify → Create order: run the depth-10 dragon, sweep parameters, then re-implement the recursive sequence generator from scratch.
Run the dragon
Download and run dragon_curve.py. The full script generates the depth-10 sequence and walks the turtle across an 800×600 canvas.
Reflection questions
- Why does the depth-10 sequence have exactly 2,047 entries?
- Look for the “spine” of bilateral symmetry. Where on the curve does the very first
Rof the sequence fall — at the start, the middle, or the end? - What is the visible difference between depth 6 and depth 10? Where does the new detail live?
Answers
Why 2,047 — each iteration roughly doubles the previous length and adds one new fold: $L_n = 2 L_p + 1$, where $L_p$ is the previous-depth length. Starting from $L_0 = 1$, the recurrence resolves to “$L_n$ = two to the power n+1, minus one”. At depth 10 this gives $2^{11} - 1 = 2047$.
The spine of symmetry — the recursive rule places the brand-new middle R exactly between the inherited half and the inverted-reversed half. Visually, the sequence is symmetric around its middle character (with an L/R swap). The first R you see is at index 0; the spine is at index 1023 of the depth-10 sequence.
Depth 6 vs depth 10 — the silhouette is broadly the same; the new detail lives along the boundary, where extra folds turn smooth edges into nested zigzags. Self-similarity in action: zoom into any branch of depth 10 and you find a depth-6 dragon inside it.
Sweep depth, colour, and direction
Edit your copy of dragon_curve.py to produce three variants. Only change the marked parameters.
Goals
- Depth comparison. Render the same dragon at depths 4, 7, and 10 side-by-side. Keep
step_size = 3constant so you can see the silhouette converging. - Fire dragon. Replace the light-blue colour
(100, 180, 255)with a warm orange-red such as(255, 110, 40). - Rotated dragon. Change
start_directionfromUPtoRIGHT. Predict what happens, then verify.
Goal 1 — what to expect
The depth-4 dragon is a recognisable 31-segment zigzag with its silhouette barely emerging. By depth 7 the dragon shape is unmistakable; by depth 10 the boundary is dense with fine detail. None of the silhouettes drift away from a common bounding box — that is the stability of the recursive construction.
Goal 2 — what to expect
Just a recoloured curve. The dragon shape is decided entirely by the L/R sequence; colour is a render decision applied uniformly to every segment. You could also pass color as a tuple that depends on segment index to get a hue gradient along the dragon’s body.
Goal 3 — what to expect
The entire dragon rotates 90° clockwise. Starting direction is a global rotation: it applies to every segment uniformly, so the shape is preserved and only the orientation changes. You may need to re-tune start_position to keep the curve inside the canvas.
Re-code the recursive generator
Implement the dragon sequence generator from scratch. The turtle/render code is provided; you fill in the recursion.
def invert_sequence(sequence):
"""Swap all L's and R's."""
# TODO 1: return a string with every L replaced by R and every R by L
pass
def generate_dragon_sequence(initial_turn, depth):
"""Build the dragon curve sequence recursively."""
if depth == 0:
# TODO 2: return the base case
pass
# TODO 3: get the previous depth's sequence (recursive call)
previous = None
# TODO 4: build the second half — reverse, then invert
second_half = None
# TODO 5: combine: previous + 'R' (the new fold) + second_half
return None
# Self-test
test = generate_dragon_sequence('R', 3)
assert test == 'RRLRRLLRRRLRLLL', f"Got {test!r}"
print("All good — depth 3 sequence matches the expected dragon string.") Hint 1 — invert_sequence
return ''.join('L' if c == 'R' else 'R' for c in sequence)A list comprehension that walks the string character by character, swapping each.
Hint 2 — base and recursive cases
- Base case —
depth == 0returns the single characterinitial_turn. Without this the recursion never bottoms out. - Recursive case — call
generate_dragon_sequence(initial_turn, depth - 1)to get the previous sequence, then buildprevious + 'R' + invert_sequence(previous[::-1]). The[::-1]reverses,invert_sequenceswaps L/R, and the central'R'is the new fold introduced at this depth.
Complete solution
def invert_sequence(sequence):
return ''.join('L' if c == 'R' else 'R' for c in sequence)
def generate_dragon_sequence(initial_turn, depth):
if depth == 0:
return initial_turn
previous = generate_dragon_sequence(initial_turn, depth - 1)
second_half = invert_sequence(previous[::-1])
return previous + 'R' + second_half
assert generate_dragon_sequence('R', 3) == 'RRLRRLLRRRLRLLL'
Make it your own
- Twin dragons. Draw two dragons from the same start position, one facing
UPand one facingDOWN, each a different colour. They interlock without overlapping — the twin-dragon tiling Knuth analysed [2].
- Animated depth sweep. Loop over depths 1..12 and save each as a frame. Pillow’s
Image.save(..., save_all=True, append_images=...)writes the result as an animated GIF.
- Vector output. Replace
move_forwardwith a PillowImageDraw.line()call (or SVG output) for a smooth, resolution-independent dragon you can print or laser-cut.
Downloads
dragon_curve.py — Exercise 1 reference exercise3_solution.py — Exercise 3 reference dragon.py — minimal alternative implementationSummary
Common pitfalls to avoid
- Forgetting the base case. Without
if depth == 0: return initial_turn, the recursion never terminates and Python raisesRecursionError. - Reversing without inverting (or vice versa). Both operations are needed on the second half. Drop either one and the curve loses its self-similar shape and starts crossing itself.
- Off-canvas drawing. Depth 12 at
step_size = 3exceeds an 800-pixel canvas. Either reducestep_size, increase the canvas, or tunestart_position. - Confusing axis conventions.
move_forwarduses(x, y)but NumPy indexescanvas[y, x]. Mixing these up swaps your dragon’s orientation.
References
- [1] Gardner, M. (1967). Mathematical games: The dragon curve and other animals. Scientific American, 216(3), 124–129.
- [2] Davis, C., & Knuth, D. E. (1970). Number representations and dragon curves — I, II. Journal of Recreational Mathematics, 3(2), 66–81; 3(3), 133–149.
- [3] Crichton, M. (1990). Jurassic Park. Knopf. ISBN 978-0-394-58816-2.
- [4] Mandelbrot, B. B. (1982). The Fractal Geometry of Nature. W. H. Freeman. ISBN 978-0-7167-1186-5.
- [5] Prusinkiewicz, P., & Lindenmayer, A. (1990). The Algorithmic Beauty of Plants. Springer-Verlag. ISBN 978-0-387-97297-8.
- [6] Edgar, G. (2008). Measure, Topology, and Fractal Geometry (2nd ed.). Springer. ISBN 978-0-387-74748-4.
- [7] Harris, C. R., Millman, K. J., van der Walt, S. J., et al. (2020). Array programming with NumPy. Nature, 585, 357–362. doi:10.1038/s41586-020-2649-2