Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 71, Gosper Flowsnake: a still of the l-system / hex substitution plate as the atlas renders it, in the fractals accent.

PL. 71  ·  FRACTALS / L-SYSTEM / HEX SUBSTITUTION

Gosper Flowsnake

William Gosper, 1973 · via Martin Gardner

OPEN THE LIVE PLATE ▸

DEFINITION

A → A−B−−B+A++AA+B−
B → +A−BB−−B−A++A+B
turn 60° · both symbols step forward

NOTES

Gosper’s space-filling curve on the hexagonal lattice, which Gardner introduced to the world as the "flowsnake" — a spoonerism of snowflake — and the name stuck. Seven copies of the path tile a hexagonal island that is itself one cell of the next scale up, so the curve fills the plane the way hilbert’s fills the square — but with a boundary that is fractal rather than straight, the Gosper island, of dimension log 3 / log √7 ≈ 1.129. One rewrite per generation multiplies the path by seven.

PROVENANCE

Origin
R. W. Gosper, 1973; published to a wide audience in M. Gardner, "In which ‘monster’ curves force redefinition of the word ‘curve’", Mathematical Games, Scientific American 235(6), December 1976 — later reprinted as "Mandelbrot’s Fractals" in Penrose Tiles to Trapdoor Ciphers. Mandelbrot renamed the curve Peano–Gosper and the tile the Gosper island in Fractals, 1977
Standing
Public domain — a curve construction
Constants
Generations are locked from regenerate: each multiplies the path sevenfold, which is a cost dial, not a look dial

HOUDINI · VEX

The same published mathematics as a Detail Wrangle body. Paste it into a Wrangle with Run Over set to Detail; every constant is the published value plus a tweak channel, so Create Spare Parameters gives a slider that starts where the paper does.

// FORMA — PL. 71 · GOSPER FLOWSNAKE — William Gosper, 1973 · via Martin Gardner
//   A → A−B−−B+A++AA+B−
//   B → +A−BB−−B−A++A+B
//   turn 60° · both symbols step forward
// Paste into a Detail Wrangle (Run Over: Detail), no inputs needed.
// Written from the published mathematics, not adapted from any code.
// Constants arrive at their published values. Press the node's Create
// Spare Parameters button and every tweak becomes a slider — starting
// at 0, the published figure, and moving in the constant's own units.
// https://forma-gen.com/#plate=gosper

float p_gens    = 4 + chf('gens_tweak');          // generations · live 2 .. 4
float p_bearing = 18 + chf('bearing_tweak');      // bearing (deg) · live 0 .. 120

// The plate's own colour: FORMA's FRACTALS accent as a cosine ramp,
// brightest near t = 0 and t = 1, near-black around t = 0.5.
vector forma_ramp(float t){
  return set(
    0.46 + 0.5 * cos(6.28318530718 * (t + 0)),
    0.1389 + 0.151 * cos(6.28318530718 * (t + 0.05)),
    0.1912 + 0.2078 * cos(6.28318530718 * (t + 0.1)));
}

// A → A−B−−B+A++AA+B−, B → +A−BB−−B−A++A+B, both symbols stepping forward,
// every turn 60°. The grammar is held as integer tokens (0 A, 1 B, 2 +, 3 −)
// — four symbols need no string machinery. The plate folds a per-plate grid
// phase into the bearing; a single wrangle takes the published bearing
// alone. Steps are unit length — scale to taste.
int forma_ruleA[] = {0, 3, 1, 3, 3, 1, 2, 0, 2, 2, 0, 0, 2, 1, 3};
int forma_ruleB[] = {2, 0, 3, 1, 1, 3, 3, 1, 3, 0, 2, 2, 0, 2, 1};

int gens = int(rint(p_gens));
int seq[] = {0};                       // the axiom, A
for (int g = 0; g < gens; g++){
    int next[];
    foreach (int sym; seq){
        if (sym == 0)      foreach (int r; forma_ruleA) push(next, r);
        else if (sym == 1) foreach (int r; forma_ruleB) push(next, r);
        else               push(next, sym);
    }
    seq = next;
}

float ang = p_bearing * 3.14159265359 / 180.0;
float sixth = 3.14159265359 / 3.0;
float x = 0.0, y = 0.0;
int steps = 0;
foreach (int sym; seq) if (sym < 2) steps++;
int prim = addprim(0, "polyline");
int pt = addpoint(0, set(0.0, 0.0, 0.0));
setpointattrib(0, "Cd", pt, forma_ramp(0.8));
addvertex(0, prim, pt);
int i = 0;
foreach (int sym; seq){
    if (sym == 2){ ang += sixth; continue; }
    if (sym == 3){ ang -= sixth; continue; }
    x += cos(ang);  y += sin(ang);  i++;
    // canvas y runs down; negated so the island sits as the plate shows it
    pt = addpoint(0, set(x, -y, 0.0));
    // colour sweeps the bright lobe of the ramp along the path
    setpointattrib(0, "Cd", pt, forma_ramp(0.8 + 0.3 * float(i) / float(steps)));
    addvertex(0, prim, pt);
}

AFTER EFFECTS · EXPRESSION

The same published mathematics as a Shape Layer path expression. Paste it onto a Path property; every constant is the published value plus a Slider Control named _tweak, so a bare paste already draws the figure and each slider moves one constant in its own units. Trim Paths is the comet.

// FORMA — PL. 71 · GOSPER FLOWSNAKE — William Gosper, 1973 · via Martin Gardner
//   A → A−B−−B+A++AA+B−
//   B → +A−BB−−B−A++A+B
//   turn 60° · both symbols step forward
// After Effects port — paste onto a Shape Layer's Path property
// (Contents › Shape › Path). Written from the published mathematics, not
// adapted from any code. Constants arrive at their published values; add a
// Slider Control (Effect › Expression Controls) named <k>_tweak and that
// constant moves in its own units, starting at 0 — the published figure.
// The plate's comet and its reveal are Trim Paths; the stroke colour is
// FORMA's FRACTALS accent, #FF4D6A. Animation runs on time.
// https://forma-gen.com/#plate=gosper

// A missing slider reads 0, so a bare paste already draws the figure.
function forma_tweak(n){ try { return effect(n)("Slider"); } catch (e){ return 0; } }
var p_gens    = 4 + forma_tweak("gens_tweak");        // generations · live 2 .. 4
var p_bearing = 18 + forma_tweak("bearing_tweak");    // bearing (deg) · live 0 .. 120

// The frame: the plate's W × H canvas is this comp, with the origin at the
// layer's anchor; canvas y already runs down, as After Effects' does.
var forma_W = thisComp.width, forma_H = thisComp.height, forma_t = time;
var forma_phase = 0.9322764608077705;   // this plate's own fixed phase, as the page has it
function forma_pt(x, y){ return [x - forma_W / 2, y - forma_H / 2]; }

// The Gosper island's flowsnake as an L-system on the hexagonal lattice:
// A → A−B−−B+A++AA+B−, B → +A−BB−−B−A++A+B, every letter a unit step, ± a
// turn of 60°. The bearing joins this plate's own phase so no two plates in a
// grid share an angle. Fitted to 0.88 of the frame; the page grows it over
// 19 s — Trim Paths, End 0 → 100%.
var gens = Math.round(p_gens);
var s = 'A';
for (var g = 0; g < gens; g++)
  s = s.replace(/[AB]/g, function (ch){ return ch === 'A' ? 'A-B--B+A++AA+B-' : '+A-BB--B-A++A+B'; });
var ang = (p_bearing + forma_phase * 60) * Math.PI / 180, x = 0, y = 0;
var raw = [[0, 0]];
for (var i = 0; i < s.length; i++){
  var ch = s.charAt(i);
  if (ch === '+') ang += Math.PI / 3;
  else if (ch === '-') ang -= Math.PI / 3;
  else { x += Math.cos(ang); y += Math.sin(ang); raw.push([x, y]); }
}
var minx = 1e9, maxx = -1e9, miny = 1e9, maxy = -1e9;
for (var j = 0; j < raw.length; j++){
  var q = raw[j];
  if (q[0] < minx) minx = q[0]; if (q[0] > maxx) maxx = q[0];
  if (q[1] < miny) miny = q[1]; if (q[1] > maxy) maxy = q[1];
}
var sc = Math.min(forma_W * 0.88 / ((maxx - minx) || 1), forma_H * 0.88 / ((maxy - miny) || 1));
var ox = (forma_W - (maxx - minx) * sc) / 2 - minx * sc;
var oy = (forma_H - (maxy - miny) * sc) / 2 - miny * sc;
var pts = [];
for (var m = 0; m < raw.length; m++) pts.push(forma_pt(ox + raw[m][0] * sc, oy + raw[m][1] * sc));
createPath(pts, [], [], false);