PL. 121 · CURVES / GROUP / STRAND DIAGRAM
Braid Group Word
Emil Artin, 1925 · closure after J. W. Alexander, 1923
OPEN THE LIVE PLATE ▸DEFINITION
Bₙ = ⟨σ₁ … σₙ₋₁ | σᵢσⱼ = σⱼσᵢ for |i − j| ≥ 2, σᵢσᵢ₊₁σᵢ = σᵢ₊₁σᵢσᵢ₊₁⟩ w = a word in σᵢ and σᵢ⁻¹, one crossing per letter closure ŵ: level k at the end of w rejoined to level k at its start
NOTES
Take n strands hanging side by side and cross them one adjacent pair at a time. Emil Artin showed in 1925 that what comes out is a group. The generator σᵢ exchanges whatever sits in positions i and i+1, with the strand arriving from position i passing in front; the inverse is the same exchange with the other strand in front; and every braid on n strands is a word in those n − 1 letters. Two relations then say everything else. Generators whose indices differ by two or more commute, because crossings that share no strand slide past one another freely. And σᵢ σᵢ₊₁ σᵢ = σᵢ₊₁ σᵢ σᵢ₊₁, which is the third Reidemeister move written as algebra: a strand can be pulled across a crossing. Every crossing here keeps a real gap in the strand that goes underneath, and that gap is the entire content of the picture. Take it away and the drawing records only which positions were exchanged, which is a permutation, and every braid sitting over the same permutation looks identical. The gap is what separates a braid from its shadow. The diagram runs around a ring rather than down a page, and that closes it: the far end of every level meets its own near end. So the figure is not only the word but the link the word presents. J. W. Alexander proved in 1923 that every link in space can be put in this form, as a closed braid running around an axis, and the axis is the faint circle at the centre, seen end on, that each strand winds once around. Separate closed curves are separate components of that link, one colour each, and there are exactly as many of them as the permutation has cycles. The word drawn never sets a letter beside its own inverse, so nothing cancels on sight. That does not make it the shortest word for its braid: the second relation can shorten a word with nothing obvious to cancel, and deciding whether two words describe the same braid is the word problem, solved in the 1925 paper by making the group act on a free group. This plate does not solve it, and claims nothing about the word beyond what it draws.
PROVENANCE
- Origin
- E. Artin, "Theorie der Zöpfe", Abhandlungen aus dem Mathematischen Seminar der Universität Hamburg 4, 1925, 47–72. Verified on Crossref against title, author, year and venue, and beyond the four required fields on volume, issue and page range as well (doi:10.1007/BF02950718). The presentation drawn here — the generators, the far-commutation relation and the braid relation — is the one set out in that paper
- Restated in English
- E. Artin, "Theory of Braids", Annals of Mathematics 48(1), 1947, 101–126, which put the group in front of an English readership and rewrote the 1925 treatment of the word problem. Verified on Crossref against title, author, year, venue, volume and issue (doi:10.2307/1969218). The link above points at the 1925 paper because that is the origin; a plate gets one source identifier and it goes to the first statement
- The closure
- J. W. Alexander, "A Lemma on Systems of Knotted Curves", Proceedings of the National Academy of Sciences 9(3), 1923, 93–95 — every link in space is the closure of some braid. Verified on Crossref against title, author, year, venue, volume, issue and pages (doi:10.1073/pnas.9.3.93). It is the reason the diagram is a ring: closing the word costs nothing once it is drawn around one, and what the plate then shows is a link diagram rather than only a group element
- Standing
- Public domain — a group presentation published in 1925 and a lemma published in 1923. No patent is possible on either, and none was ever claimed
- Conventions, declared
- σᵢ puts the strand arriving from position i in front. The opposite convention is equally common in the literature and mirrors every crossing in the figure; nothing else changes. Levels are counted outward from the innermost circle, and the word is read clockwise from a start angle taken from PHASE, the per-plate constant this atlas hashes out of the specimen id. The generator index runs 1 to n − 1 in the presentation above and 0 to n − 2 in the code
- Why the diagram is a ring
- The classical figure runs down a page, and that was measured first and rejected. A linear diagram in a square frame has to fit m crossings along one axis and n strands across the other, so the arc a strand has to swap across shrinks as (n − 1) / m and the two strands at a crossing meet at 2·arctan(π m / 2(n − 1)) folded into the acute range: measured 24.0 degrees at 12 crossings on 5 strands, 16.1 degrees at 18 on 5, and 8.1 degrees at 18 on 3 — which is not a crossing anybody can read, and the crossing is the plate. A ring supplies an along-the-word extent of 2π·0.30R against a radial span of at most 0.28R, a ratio of 6.7 inside the same square, which is the aspect a braid diagram actually has. Alexander then makes the closure free rather than an extra device
- Checked, not just plotted
- The rendered geometry was checked against the theory rather than eyeballed. Reading the drawn samples back and joining every lap to whichever lap begins where it ends, the number of closed curves the figure actually forms equals the number of cycles of the permutation the word induces — in all 1,404 declared constant combinations and in all 5,000 random tuple and seed pairs tested, with no exception — and the join lands within 8.0e-14 px, so the figure is closed rather than closed-looking. Separately, applying the two relations as rewrites to 20,000 random words fired 17,241 times and left the permutation, the cycle count and the exponent sum untouched every time, which is what the relations say and is an independent check that the convention above is implemented as written rather than merely stated
- Constants
- Nothing is locked, and that is measured rather than assumed. The radial span is derived, not declared: it is chosen so the interchange arc a crossing needs always fits inside the angular cell that crossing owns, which pins the crossing angle to exactly what the tightness slider asks. Measured from the sampled geometry across all 1,404 declared combinations, the crossing angle stays between 64.9 and 89.1 degrees, the strand separation never falls below 10.00 CSS px at a 300 px card, and the over/under gap runs 2.40 to 10.08 px against a 1.00 px stroke. A long word on few strands therefore draws as a thinner ring rather than as a shallower crossing, and no combination of the three sliders reaches dead ground
- Source
- doi:10.1007/BF02950718
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. 121 · BRAID GROUP WORD — Emil Artin, 1925 · closure after J. W. Alexander, 1923
// Bₙ = ⟨σ₁ … σₙ₋₁ | σᵢσⱼ = σⱼσᵢ for |i − j| ≥ 2, σᵢσᵢ₊₁σᵢ = σᵢ₊₁σᵢσᵢ₊₁⟩
// w = a word in σᵢ and σᵢ⁻¹, one crossing per letter
// closure ŵ: level k at the end of w rejoined to level k at its start
// 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=braid
float p_strands = 5 + chf('strands_tweak'); // strands n · live 3 .. 8
float p_word = 12 + chf('word_tweak'); // word length · live 6 .. 18
float p_tight = 1 + chf('tight_tweak'); // crossing tightness · live 0.75 .. 1.6
// The plate's own colour: FORMA's CURVES 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.11 + 0.1196 * cos(6.28318530718 * (t + 0)),
0.46 + 0.5 * cos(6.28318530718 * (t + 0.05)),
0.2453 + 0.2667 * cos(6.28318530718 * (t + 0.1)));
}
// A word in the Artin generators, drawn as the strand diagram of its own
// closure. Built the same way braid.js builds it: a freely reduced random
// word in sigma_1 .. sigma_{n-1}, walked once to carry who sits at which
// level and who passes behind whom, then laid around a ring so the far end
// of every level meets its own near end — which is the braid closure, and
// is why one lap of a strand runs on into the lap of whichever strand
// started where this one finished. A closed curve of the figure is one
// cycle of the permutation the word induces (Alexander, 1923), and gets
// its own colour, exactly as on the plate.
//
// Every crossing keeps a real break in the strand that goes underneath.
// That break is the whole content of a strand diagram: without it the
// drawing records only which positions were exchanged, which is the
// permutation, and every braid over the same permutation looks alike. It
// is emitted here as a genuine discontinuity — the polyline ends and a new
// one begins — rather than as a coincident overlap, so the geometry stays
// honest under any downstream resample or sweep.
//
// Not ported: the faint whole-closure underlay and the HILITE head that
// marks the newest crossing. Both are devices of the animated reveal, not
// part of the finished figure — the same call pappus.vex and ford.vex make
// about their own underlays. The axis circle IS ported: it is the axis of
// the closed braid seen end on, and every strand winds once around it.
//
// Deterministic, and different from the page by design. On the page the
// word comes from seeded(11), which reads the atlas regenerate seed; here
// it comes from random(counted seed) on the letter index, so a cook is the
// same braid every time but not the braid a given atlas seed shows. The
// mathematics, the conventions and the geometry are identical.
//
// Sampling is denser than the plate. The plate spends its samples where
// the curvature is, because it has a frame budget; a cook does not, so the
// interchange gets 16 intervals and each straight run 12 wherever they
// fall. Nothing else about the curve changes.
//
// Nothing is waived: all three published constants reach the body.
float forma_TAU = 6.28318530718;
float forma_HALF = 1.57079632679;
float forma_R = 2.0;
// PHASE on the page is a per-plate constant hashed from the specimen id;
// here it only rotates the ring, and a cook has no grid of sibling plates
// to desynchronise from. Baked to the value the id "braid" actually
// produces, using the same expression the draw function uses, so the cook
// and the page start the word at the same place.
float forma_th0 = 0.02455494599416852 * forma_TAU - forma_HALF;
int forma_SA = 12; // intervals across one straight run
int forma_SX = 16; // intervals across one interchange
int n = int(rint(p_strands));
int m = int(rint(p_word));
float tg = p_tight;
// ---- the word -------------------------------------------------------
// sigma_i exchanges whatever sits in positions i and i+1, the strand
// arriving from position i passing in front; the inverse is the same
// exchange with the other strand in front. Index 0 .. n-2 here. A letter
// that would immediately undo the one before it is rejected and redrawn,
// because sigma_i sigma_i^-1 is the empty word and drawing it puts down a
// crossing that is not in the braid. That makes the word freely reduced.
// It does NOT make it shortest in B_n: the braid relation can shorten a
// word with nothing obvious to cancel, and deciding that is the word
// problem, which Artin solved and this port does not attempt.
int idx[], sgn[];
for (int j = 0; j < m; j++){
int i = 0, e = 1;
for (int attempt = 0; attempt < 16; attempt++){
float ra = random(float(j) * 97.0 + float(attempt) * 3.0 + 11.5);
float rb = random(float(j) * 97.0 + float(attempt) * 3.0 + 613.5);
i = min(n - 2, int(ra * float(n - 1)));
e = (rb < 0.5) ? 1 : -1;
if (j == 0) break;
if (i != idx[j - 1]) break;
if (e == sgn[j - 1]) break;
}
push(idx, i);
push(sgn, e);
}
// ---- walk the word once ---------------------------------------------
// seat[level] answers "who is in this crossing", lvl[strand] answers
// "where does this strand go next", and both are wanted at every letter.
int seat[], lvl[];
resize(seat, n);
resize(lvl, n);
for (int k = 0; k < n; k++){ seat[k] = k; lvl[k] = k; }
int under[], lvlAt[];
resize(lvlAt, (m + 1) * n);
for (int j = 0; j < m; j++){
for (int s = 0; s < n; s++) lvlAt[j * n + s] = lvl[s];
int i = idx[j];
int A = seat[i];
int B = seat[i + 1];
push(under, (sgn[j] == 1) ? B : A); // sigma_i puts A in front
seat[i] = B;
seat[i + 1] = A;
lvl[A] = i + 1;
lvl[B] = i;
}
for (int s = 0; s < n; s++) lvlAt[m * n + s] = lvl[s];
// ---- components of the closure --------------------------------------
// The lap of the strand that started at level k runs on into the lap of
// the strand that started where this one finished, so a component of the
// closed braid is one cycle of that map.
int nxt[], comp[];
resize(nxt, n);
resize(comp, n);
for (int k = 0; k < n; k++) nxt[seat[k]] = k;
for (int k = 0; k < n; k++) comp[k] = -1;
int nc = 0;
for (int s = 0; s < n; s++){
if (comp[s] >= 0) continue;
int q = s;
while (comp[q] < 0){
comp[q] = nc;
q = nxt[q];
}
nc++;
}
// ---- the annulus -----------------------------------------------------
// With a raised cosine across an arc of length L the steepest slope either
// strand reaches is (pi/2)*sp/L, so fixing L at (pi/2)*sp/tight fixes that
// slope at tight itself, at every radius and for every word — the crossing
// angle becomes a property of one constant rather than an accident of the
// other two. What it costs is room: L has to fit inside the angular cell
// its own letter owns, and the tightest cell is the innermost crossing, at
// radius rBar - (n-2)*sp/2. Solving that for sp gives the second term
// below, so the radial span is derived rather than declared and a long
// word on few strands comes out as a thinner ring rather than as a
// crossing nobody can read.
float rBar = forma_R * 0.30;
float spanMax = forma_R * 0.28;
float cell = forma_TAU / float(m);
float sp = min(spanMax / float(n - 1),
(forma_TAU * rBar / float(m)) /
(forma_HALF / tg + M_PI * float(n - 2) / float(m)));
float rIn = rBar - sp * float(n - 1) * 0.5;
float L = forma_HALF * sp / tg;
// The break in the under-strand is a disc about the crossing point, not a
// span of parameter, so it keeps its shape however steep the crossing is.
// The plate also floors it at a multiple of its own stroke width; a stroke
// width has no meaning in a geometry context, so that floor is dropped.
float gap = min(0.24 * sp, 0.30 * rIn * cell);
int seg = 2 * forma_SA + forma_SX;
for (int s = 0; s < n; s++){
// sparse marks stay in the bright lobe of the ramp: a single-component
// closure — a knot rather than a link — gets one colour, which is what
// one curve should look like
float cu = (nc > 1) ? (0.78 + 0.4 * float(comp[s]) / float(nc - 1)) : 0.98;
vector col = forma_ramp(cu);
int prim = -1;
for (int j = 0; j < m; j++){
float rA = rIn + float(lvlAt[j * n + s]) * sp;
float rB = rIn + float(lvlAt[(j + 1) * n + s]) * sp;
int i = idx[j];
float rc = rIn + (float(i) + 0.5) * sp; // the two strands meet here
float thc = forma_th0 + (float(j) + 0.5) * cell;
float xc = rc * cos(thc);
float yc = rc * sin(thc);
// frac is L as a fraction of this letter own cell; sp above is
// chosen so it only reaches 1 at the innermost crossing of a word
// that is span-limited, and the min is the guard for that case
float frac = min(1.0, L / (rc * cell));
float u0 = 0.5 - 0.5 * frac;
float u1 = 0.5 + 0.5 * frac;
int isUnder = (under[j] == s);
// every cell but the last stops one sample short, because that
// sample is the next cell own first; the last one runs to the end
// so the lap finishes exactly where the next lap of the same
// component begins
int qmax = (j == m - 1) ? seg : (seg - 1);
for (int q = 0; q <= qmax; q++){
float uu = 0.0;
if (q <= forma_SA)
uu = u0 * float(q) / float(forma_SA);
else if (q <= forma_SA + forma_SX)
uu = u0 + frac * float(q - forma_SA) / float(forma_SX);
else
uu = u1 + (1.0 - u1) * float(q - forma_SA - forma_SX) / float(forma_SA);
float th = forma_th0 + (float(j) + uu) * cell;
// a raised cosine between the two radii: zero slope at both
// ends, so a straight arc runs into an interchange and out of
// it without a corner. Strands not in this crossing have
// rA == rB and stay on their own circle.
float r = rA;
if (uu >= u1) r = rB;
else if (uu > u0) r = rA + (rB - rA) * (0.5 - 0.5 * cos(M_PI * (uu - u0) / frac));
float px = r * cos(th);
float py = r * sin(th);
if (isUnder == 1){
float dx = px - xc;
float dy = py - yc;
if (dx * dx + dy * dy < gap * gap){
prim = -1; // lift the pen: this strand is behind
continue;
}
}
if (prim < 0) prim = addprim(0, "polyline");
// canvas y runs down, Houdini y runs up: negated once, here
int pt = addpoint(0, set(px, -py, 0.0));
setpointattrib(0, "Cd", pt, col);
setpointattrib(0, "Alpha", pt, 0.88);
addvertex(0, prim, pt);
}
}
}
// The axis of the closed braid, where it passes through the page. A closed
// braid is a link that winds monotonically around an unknotted circle, and
// this is that circle seen end on: every strand above makes exactly one
// turn about it, which is what closed braid means and why the figure is a
// ring at all.
float axisR = rIn * 0.34;
vector axisCol = forma_ramp(0.0);
int axis = addprim(0, "polyline");
for (int i = 0; i <= 64; i++){
float th = forma_TAU * float(i) / 64.0;
int pt = addpoint(0, set(axisR * cos(th), -axisR * sin(th), 0.0));
setpointattrib(0, "Cd", pt, axisCol);
setpointattrib(0, "Alpha", pt, 0.22);
addvertex(0, axis, 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
// FORMA — PL. 121 · BRAID GROUP WORD — Emil Artin, 1925 · closure after J. W. Alexander, 1923
// Bₙ = ⟨σ₁ … σₙ₋₁ | σᵢσⱼ = σⱼσᵢ for |i − j| ≥ 2, σᵢσᵢ₊₁σᵢ = σᵢ₊₁σᵢσᵢ₊₁⟩
// w = a word in σᵢ and σᵢ⁻¹, one crossing per letter
// closure ŵ: level k at the end of w rejoined to level k at its start
// 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 CURVES accent, #3DFF88. Animation runs on time.
// This plate draws 13 separate paths at its published constants:
// duplicate the group (Contents › Group) that many times and each copy draws
// its own part, read from its position in the layer. A Slider Control named
// "part" on the layer pins one instead.
// The figure is seeded like the page: a bare paste is the page's boot seed,
// and a Slider Control named seed_tweak moves to any other.
// https://forma-gen.com/#plate=braid
// 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_strands = 5 + forma_tweak("strands_tweak"); // strands n · live 3 .. 8
var p_word = 12 + forma_tweak("word_tweak"); // word length · live 6 .. 18
var p_tight = 1 + forma_tweak("tight_tweak"); // crossing tightness · live 0.75 .. 1.6
// 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.02455494599416852; // 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]; }
function forma_partIndex(){
try { return Math.round(effect("part")("Slider")); } catch (e){}
try { return thisProperty.propertyGroup(3).propertyIndex - 1; } catch (e){ return 0; }
}
var forma_part = forma_partIndex();
var forma_seed = (1 + Math.round(forma_tweak("seed_tweak"))) >>> 0;
var forma_imul = Math.imul || function (a, b){
var ah = (a >>> 16) & 0xffff, al = a & 0xffff, bh = (b >>> 16) & 0xffff, bl = b & 0xffff;
return (al * bl + (((ah * bl + al * bh) << 16) >>> 0)) | 0;
};
function forma_seeded(k){ // the page's seeded(k): mulberry32 on the atlas seed
var a = (((forma_seed * 2654435761) >>> 0) ^ ((k * 40503) >>> 0)) >>> 0;
return function (){
a = a + 0x6D2B79F5 | 0;
var t = forma_imul(a ^ a >>> 15, 1 | a);
t = t + forma_imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
// A closed braid: a freely reduced word in Artin's generators σ₁ … σₙ₋₁,
// drawn round an annulus with one letter per angular cell, each letter a
// raised-cosine interchange of two neighbouring strands whose crossing angle
// is fixed by `tight`, the closure joining the far end of every level to its
// own near end. The word is the page's own — seeded(11) on the atlas seed, a
// letter that would immediately undo the one before rejected and redrawn —
// and the geometry is the page's to the sample: ten intervals across every
// interchange, the arcs sampled by their own length, a disc of `gap` cut
// from the strand that passes beneath. The break is the diagram; without it
// every braid over the same permutation would look alike. A path cannot
// break, so each strand is its runs between under-crossings — twelve at
// the published word, on the page boot seed: part 0 is the
// braid axis, the circle every strand winds once about, and parts 1 … are
// the runs, strand by strand, in order round the ring.
// parts: 13
var TAU = 6.283185307179586, HALF_PI = 1.5707963267948966;
var n = Math.round(p_strands), m = Math.round(p_word), tight = p_tight;
var rnd = forma_seeded(11);
var idx = [], sgn = [];
for (var j = 0; j < m; j++){
var ii = 0, ee = 1;
for (var attempt = 0; attempt < 16; attempt++){
ii = Math.min(n - 2, Math.floor(rnd() * (n - 1)));
ee = rnd() < 0.5 ? 1 : -1;
if (j === 0 || ii !== idx[j - 1] || ee === sgn[j - 1]) break;
}
idx.push(ii); sgn.push(ee);
}
var seat = [], lvl = [];
for (var k = 0; k < n; k++){ seat.push(k); lvl.push(k); }
var under = [], lvlAt = [];
for (var j2 = 0; j2 < m; j2++){
for (var s = 0; s < n; s++) lvlAt[j2 * n + s] = lvl[s];
var i2 = idx[j2], A = seat[i2], B = seat[i2 + 1];
under.push(sgn[j2] === 1 ? B : A);
seat[i2] = B; seat[i2 + 1] = A;
lvl[A] = i2 + 1; lvl[B] = i2;
}
for (var s2 = 0; s2 < n; s2++) lvlAt[m * n + s2] = lvl[s2];
var R = Math.min(forma_W, forma_H), rBar = R * 0.30, spanMax = R * 0.28;
var cell = TAU / m;
var sp = Math.min(spanMax / (n - 1), (TAU * rBar / m) / (HALF_PI / tight + Math.PI * (n - 2) / m));
var rIn = rBar - sp * (n - 1) / 2, L = HALF_PI * sp / tight;
var cx = forma_W / 2, cy = forma_H / 2, th0 = forma_phase * TAU - Math.PI / 2;
var lw = Math.max(1.0, forma_W / 300);
var gap = Math.max(lw * 2.2, Math.min(0.24 * sp, 0.30 * rIn * cell));
var SX = 10, rcOut = rIn + (n - 1.5) * sp, fracMin = Math.min(1, L / (rcOut * cell));
var SA = Math.max(1, Math.min(10, Math.ceil((1 - fracMin) * cell * (rIn + (n - 1) * sp) / 20)));
var SEG = 2 * SA + SX, nSamp = m * SEG + 1;
var lap = [], cut = [];
for (var s3 = 0; s3 < n; s3++){ lap.push([]); cut.push([]); }
for (var j3 = 0; j3 < m; j3++){
var i3 = idx[j3], rc = rIn + (i3 + 0.5) * sp, thc = th0 + (j3 + 0.5) * cell;
var xc = cx + rc * Math.cos(thc), yc = cy + rc * Math.sin(thc);
var frac = Math.min(1, L / (rc * cell)), u0 = 0.5 - 0.5 * frac, u1 = 0.5 + 0.5 * frac;
for (var s4 = 0; s4 < n; s4++){
var rA = rIn + lvlAt[j3 * n + s4] * sp, rB = rIn + lvlAt[(j3 + 1) * n + s4] * sp;
var isUnder = under[j3] === s4;
for (var q = 0; q <= SEG; q++){
var u = q <= SA ? u0 * q / SA : q <= SA + SX ? u0 + frac * (q - SA) / SX : u1 + (1 - u1) * (q - SA - SX) / SA;
var th = th0 + (j3 + u) * cell, r = rA;
if (u >= u1) r = rB;
else if (u > u0) r = rA + (rB - rA) * (0.5 - 0.5 * Math.cos(Math.PI * (u - u0) / frac));
var px = cx + r * Math.cos(th), py = cy + r * Math.sin(th), ix = j3 * SEG + q;
lap[s4][ix] = [px, py];
var dx = px - xc, dy = py - yc;
cut[s4][ix] = (isUnder && dx * dx + dy * dy < gap * gap) ? 1 : (cut[s4][ix] || 0);
}
}
}
var pts = [];
if (forma_part <= 0){
for (var a = 0; a < 48; a++) pts.push(forma_pt(cx + rIn * 0.34 * Math.cos(a / 48 * TAU), cy + rIn * 0.34 * Math.sin(a / 48 * TAU)));
createPath(pts, [], [], true);
} else {
// The closure joins the far end of every level to its own near end, so a
// strand that ends at level k continues into the strand that began there:
// the closed curves are the cycles of that permutation (Alexander: every
// link arises this way), and a run is read round a whole cycle, where
// sample nSamp − 1 of one strand is sample 0 of the next.
var runs = [], NC = nSamp - 1, done = [];
for (var s5 = 0; s5 < n; s5++){
if (done[s5]) continue;
var P = [], C = [], q5 = s5;
while (!done[q5]){
done[q5] = 1;
for (var z = 0; z < NC; z++){ P.push(lap[q5][z]); C.push(cut[q5][z] || 0); }
q5 = lvl[q5];
}
var start = -1, NP = P.length;
for (var z2 = 0; z2 < NP; z2++) if (!C[z2] && C[(z2 - 1 + NP) % NP]){ start = z2; break; }
if (start < 0){ runs.push([P, true]); continue; }
var cur = null;
for (var w = 0; w < NP; w++){
var iz = (start + w) % NP;
if (C[iz]){ cur = null; continue; }
if (!cur){ cur = []; runs.push([cur, false]); }
cur.push(P[iz]);
}
}
var pick = runs[Math.min(forma_part - 1, runs.length - 1)];
for (var v = 0; v < pick[0].length; v++) pts.push(forma_pt(pick[0][v][0], pick[0][v][1]));
createPath(pts, [], [], pick[1]);
}