Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 128, Seifert Surface: a still of the spanning surface / seifert algorithm plate as the atlas renders it, in the curves accent.

PL. 128  ·  CURVES / SPANNING SURFACE / SEIFERT ALGORITHM

Seifert Surface

Herbert Seifert, 1935

OPEN THE LIVE PLATE ▸

DEFINITION

smooth every crossing with the orientation → disjoint Seifert circles
span each circle with a disc at its own height, join with a half-twisted band per crossing
χ = discs − bands,   genus = (2 − χ − boundary components) / 2
the (2,q) diagram: 2 discs, q bands, χ = 2 − q

NOTES

Seifert proved in 1935 that every knot bounds a surface, and gave an algorithm that builds one. Orient the diagram; at every crossing cut the two strands apart and rejoin them the way the arrows already point, which leaves no crossing at all, only disjoint circles in the plane. Span each circle with a disc, lifted to its own height so no two discs touch. Then put back exactly what was removed: at each crossing, a band with one half twist in it, joining the two discs. The boundary of what you have built is the knot you started with, and it is orientable — it has two sides — which is the whole point, because the smallest genus of such a surface is a number attached to the knot itself. This plate runs the algorithm on the two-strand family: q crossings between two Seifert circles. At q = 3 that is the trefoil, and the surface has genus 1. Odd q gives a knot of genus (q − 1)/2; even q gives a two-component torus link instead, which the dial reaches and which the plate draws just as honestly — at q = 2 the Hopf link with the annulus that spans it. Two things the picture is doing deliberately. The circles are nested in the diagram, one inside the other, and stacked in space here, which is the same surface after the lift; the upper disc is drawn a little smaller so the nesting stays visible from above. And the light and dark faces of each band are not lighting: they are the surface own orientation, a consistent normal carried all the way round, and the place where a band changes value is where it turns over.

PROVENANCE

Origin
H. Seifert, "Über das Geschlecht von Knoten", Mathematische Annalen 110, 1935, 571–592. Verified against Crossref on title, sole author, year, venue, volume and pages before it was cited here
Standing
Pre-war German mathematics, long in the public domain. No patent ever applied. Written here from the published algorithm — orient, smooth, span, reband — and not adapted from any existing figure or program
Checked, not just plotted
Nothing in the draw asserts how many pieces the boundary has. It walks the incidence of free rim arcs and band edges and finds the cycles, and the genus follows from that count and the Euler characteristic. Run across the family: q = 2 gives 2 components and genus 0 (the Hopf link and its annulus), q = 3 gives 1 and genus 1 (the trefoil), q = 4 gives 2 and genus 1, q = 5 gives 1 and genus 2, q = 6 gives 2 and genus 2, q = 7 gives 1 and genus 3. Every one matches the published genus of the (2,q) torus link, and none of it is written down anywhere in the code
Presentation
The two Seifert circles are nested in the diagram and stacked in space. That is an isotopy, not a liberty, but the direction of the slant is not free: the bands must run from the larger lower rim up and inward to the smaller upper one, which makes the crease where a band meets a disc 76 degrees at the published constants and 63 at the worst corner of the box. Slanted the other way, or flattened toward horizontal, that angle runs to 180 and the band folds back over the disc it just left — a crease rather than a surface. The lift floor is set well above the taper ceiling for exactly this reason
What a still shows
Three bands sit at 120 degrees around a rim, so from any one viewpoint about two of three are better than half face-on and only the one presented most nearly face-on shows its half twist as a twist rather than as a sliver. The tumble cycles which. This is structural and worth knowing before looking at a thumbnail, a PNG export or the plate under STATIC: a still of this plate shows one twist at a time, and the moving plate shows them all in turn
Constants
crossings is the mathematics and everything else is the view. The band half-width is clamped so q bands cannot claim more of the upper rim than it has, and the taper ceiling of 0.36 was chosen so that clamp never fires anywhere inside the declared box — at the tightest corner, q = 7 with width and taper both at their ceilings, the clamp would act at half-width 0.2585 and the widest the slider can ask for is 0.25. A clamp that can fire on a slider is dead travel by another name, so it is a guard against a future box rather than a live limiter. The whole box was re-measured once: an earlier one had three of the four view constants defaulting to the illegible side of their own travel. Worth knowing before moving q: the rim has a fixed amount of room to share, so at q = 7 and the published width the gap between two bands is shorter than a band is wide, and the plate reads as a web rather than as discs and straps. It is correct there, and it is crowded there
Source
doi:10.1007/bf01448044

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. 128 · SEIFERT SURFACE — Herbert Seifert, 1935
//   smooth every crossing with the orientation → disjoint Seifert circles
//   span each circle with a disc at its own height, join with a half-twisted band per crossing
//   χ = discs − bands,   genus = (2 − χ − boundary components) / 2
//   the (2,q) diagram: 2 discs, q bands, χ = 2 − q
// 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=seifert

float p_crossings = 3 + chf('crossings_tweak');       // q — crossings in the (2,q) diagram · live 2 .. 7
float p_lift      = 1.2 + chf('lift_tweak');          // gap between the two disc levels · live 0.8 .. 1.5
float p_width     = 0.4 + chf('width_tweak');         // band width / disc radius · live 0.28 .. 0.5
float p_taper     = 0.3 + chf('taper_tweak');         // upper disc smaller by · live 0.14 .. 0.36
float p_rulings   = 6 + chf('rulings_tweak');         // band wireframe density · live 4 .. 14
float p_wash      = 0.14 + chf('wash_tweak');         // surface wash · live 0 .. 0.26

// 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)));
}

// The Seifert algorithm on the two-strand diagram: q crossings between two
// Seifert circles, so two discs and q half-twisted bands. The lower disc has
// radius 1, the upper 1 - taper, lifted apart so they are disjoint; a band
// runs from the lower rim up and inward to the upper one, turning its width
// vector through pi on the way. That half turn is the crossing.
//
// Three things this port does differently from the page, all forced and all
// deliberate:
//
//  - The page flattens the surface onto a canvas and so has to pick a
//    viewpoint. A cook does not, so the elevation constant is waived below
//    and Houdini gets the actual 3-D surface with the viewport as the camera.
//  - The page colours each band ruling by which side faces its camera, which
//    is how a canvas with no lighting shows that the surface has two sides.
//    Here the orientation is handed over as the datum it actually is: every
//    band point carries N = cos(pi.s).m + sin(pi.s).e_theta, the globally
//    consistent normal, m at the lower rim and -m at the upper, matching both
//    discs across their creases. Shade it and the two sides appear by
//    themselves. Nothing in this file tests facing.
//  - The discs carry normal +z, which is +y here, and both carry the SAME
//    one. That sameness is the orientability.
//
// The boundary is walked, not asserted: each band corner meets exactly one
// free rim arc and one band edge, so alternating rim, edge, rim, edge finds
// the cycles. Odd q closes after all 4q arcs into a single knot of genus
// (q - 1)/2; even q gives two components and the (2,q) torus link. Nothing
// here is random, so two cooks are identical.
//
// waived: tilt — camera elevation, and a cook has no camera; the page needs
// one because it projects to a canvas, Houdini gets the surface itself

// z-up: the stacking axis maps onto Houdini's y, the mapping every z-up
// specimen in this corpus uses (see catenoid.vex). VEX has no closures, so
// every helper below takes ALL of its inputs as parameters — a function that
// reads a snippet-scope variable fails to cook with "variable is not
// accessible in this function". PI is spelled out inside the helpers for the
// same reason: nothing is inherited.

// A point on band k at station s (0 = lower rim, 1 = upper) and offset v
// across the ribbon. Written in cylindrical coordinates so both attachment
// edges land exactly on their rim circles: the tangential offset is an angle,
// and sin(pi.s) vanishes at both ends, so the ribbon meets the rims by
// construction rather than by a chord that misses them.
function vector forma_sf_band(int k, q; float s, v, lift, rtop, hw, mrho, mz){
    float c = cos(3.14159265359 * s), sn = sin(3.14159265359 * s);
    float rho = (1.0 + (rtop - 1.0) * s) - hw * v * sn * mrho;
    float z   = (-lift * 0.5 + lift * s) - hw * v * sn * mz;
    float a   = 6.28318530718 * float(k) / float(q) + hw * v * c / max(0.25, rho);
    return set(rho * cos(a), z, rho * sin(a));
}

// The globally consistent orientation normal at the same station.
function vector forma_sf_nrm(int k, q; float s, mrho, mz){
    float c = cos(3.14159265359 * s), sn = sin(3.14159265359 * s);
    float a = 6.28318530718 * float(k) / float(q);
    float ca = cos(a), sa = sin(a);
    return set(c * mrho * ca - sn * sa, c * mz, c * mrho * sa + sn * ca);
}

// A point on the rim of disc `level` (0 = lower, 1 = upper) at angle a.
function vector forma_sf_rim(int level; float a, lift, rtop){
    float R = level == 1 ? rtop : 1.0;
    float z = level == 1 ? lift * 0.5 : -lift * 0.5;
    return set(R * cos(a), z, R * sin(a));
}

float forma_tau  = 6.28318530718;
int   forma_q    = max(2, int(rint(p_crossings)));
float forma_lift = p_lift;
float forma_rtop = 1.0 - p_taper;
// Guarded so q bands cannot claim more of the upper rim than it has: past
// 2q.hw/rtop = 2.pi the free rim arcs go negative and the walk below crosses
// itself. The declared box keeps every reachable setting clear of it, so this
// is a guard against a future range rather than a live limiter.
float forma_hw   = min(p_width * 0.5, 0.9 * 3.14159265359 * forma_rtop / float(forma_q));
float forma_len  = sqrt((1.0 - forma_rtop) * (1.0 - forma_rtop) + forma_lift * forma_lift);
float forma_mrho = -forma_lift / forma_len;
float forma_mz   = (forma_rtop - 1.0) / forma_len;
int   forma_nr   = max(4, int(rint(p_rulings))) * 2;
int   forma_seg  = 72;

// ---- the two Seifert discs: rim polyline, plus the disc itself as a fan
// carrying the plate's own wash alpha. Both normals are +y, and they are the
// same, which is what orientability looks like here.
for (int level = 0; level < 2; level++){
    float zc = level == 1 ? forma_lift * 0.5 : -forma_lift * 0.5;
    int rimpt[];
    resize(rimpt, forma_seg);
    for (int i = 0; i < forma_seg; i++){
        float a = forma_tau * float(i) / float(forma_seg);
        int pt = addpoint(0, forma_sf_rim(level, a, forma_lift, forma_rtop));
        setpointattrib(0, "Cd", pt, forma_ramp(0.9));
        setpointattrib(0, "Alpha", pt, 0.5);
        setpointattrib(0, "N", pt, set(0.0, 1.0, 0.0));
        rimpt[i] = pt;
    }
    int prim = addprim(0, "polyline");
    for (int i = 0; i <= forma_seg; i++) addvertex(0, prim, rimpt[i % forma_seg]);

    if (p_wash > 0.001){
        int fanpt[];
        resize(fanpt, forma_seg);
        for (int i = 0; i < forma_seg; i++){
            float a = forma_tau * float(i) / float(forma_seg);
            int pt = addpoint(0, forma_sf_rim(level, a, forma_lift, forma_rtop));
            setpointattrib(0, "Cd", pt, forma_ramp(0.9));
            setpointattrib(0, "Alpha", pt, p_wash);
            setpointattrib(0, "N", pt, set(0.0, 1.0, 0.0));
            fanpt[i] = pt;
        }
        int cpt = addpoint(0, set(0.0, zc, 0.0));
        setpointattrib(0, "Cd", cpt, forma_ramp(0.9));
        setpointattrib(0, "Alpha", cpt, p_wash);
        setpointattrib(0, "N", cpt, set(0.0, 1.0, 0.0));
        for (int i = 0; i < forma_seg; i++)
            addprim(0, "poly", cpt, fanpt[i], fanpt[(i + 1) % forma_seg]);
    }
}

// ---- the q half-twisted bands, as ruled polyline sweeps. One polyline
// across the ribbon at each station; the ribbon is very slightly curved
// across, because the tangential offset is an angle rather than a chord, so
// four samples rather than two. A second point set carries the wash as a
// quad sheet, which is what the page's fill pass lays down. The page splits
// both alphas by facing; there is no facing here, so both go down at the
// front-side value and N carries the information instead.
for (int k = 0; k < forma_q; k++){
    int grid[];
    resize(grid, (forma_nr + 1) * 4);
    for (int i = 0; i <= forma_nr; i++){
        float s = float(i) / float(forma_nr);
        vector nrm = forma_sf_nrm(k, forma_q, s, forma_mrho, forma_mz);
        for (int j = 0; j < 4; j++){
            float v = -1.0 + 2.0 * float(j) / 3.0;
            int pt = addpoint(0, forma_sf_band(k, forma_q, s, v, forma_lift,
                                               forma_rtop, forma_hw, forma_mrho, forma_mz));
            setpointattrib(0, "Cd", pt, forma_ramp(0.9));
            setpointattrib(0, "Alpha", pt, 0.8);
            setpointattrib(0, "N", pt, nrm);
            grid[i * 4 + j] = pt;
        }
    }
    for (int i = 0; i <= forma_nr; i++){
        int prim = addprim(0, "polyline");
        for (int j = 0; j < 4; j++) addvertex(0, prim, grid[i * 4 + j]);
    }

    if (p_wash > 0.001){
        int sheet[];
        resize(sheet, (forma_nr + 1) * 4);
        for (int i = 0; i <= forma_nr; i++){
            float s = float(i) / float(forma_nr);
            vector nrm = forma_sf_nrm(k, forma_q, s, forma_mrho, forma_mz);
            for (int j = 0; j < 4; j++){
                float v = -1.0 + 2.0 * float(j) / 3.0;
                int pt = addpoint(0, forma_sf_band(k, forma_q, s, v, forma_lift,
                                                   forma_rtop, forma_hw, forma_mrho, forma_mz));
                setpointattrib(0, "Cd", pt, forma_ramp(0.9));
                setpointattrib(0, "Alpha", pt, p_wash);
                setpointattrib(0, "N", pt, nrm);
                sheet[i * 4 + j] = pt;
            }
        }
        for (int i = 0; i < forma_nr; i++)
            for (int j = 0; j < 3; j++)
                addprim(0, "poly", sheet[i * 4 + j], sheet[(i + 1) * 4 + j],
                                   sheet[(i + 1) * 4 + j + 1], sheet[i * 4 + j + 1]);
    }
}

// ---- the boundary: the knot itself, as its own bright polyline per
// component. Corner (level, k, side) is indexed (level.q + k).2 + (side>0?0:1);
// each meets one rim arc and one band edge, so the walk alternates. The guard
// counter cannot fire on any valid configuration and exists so that a cook can
// never hang: a wrong answer is findable, a hung Houdini is not.
int forma_seen[];
resize(forma_seen, 4 * forma_q);
for (int i = 0; i < 4 * forma_q; i++) forma_seen[i] = 0;

for (int lv = 0; lv < 2; lv++){
    for (int k0 = 0; k0 < forma_q; k0++){
        for (int si = 0; si < 2; si++){
            if (forma_seen[(lv * forma_q + k0) * 2 + si] == 1) continue;
            int sd0 = si == 0 ? 1 : -1;
            int prim = addprim(0, "polyline");
            int cl = lv, ck = k0, cs = sd0;
            int guard = 0;
            while (1){
                forma_seen[(cl * forma_q + ck) * 2 + (cs > 0 ? 0 : 1)] = 1;
                // the free rim arc leaving this corner
                float del = forma_hw / (cl == 1 ? forma_rtop : 1.0);
                int nk = cs > 0 ? ck + 1 : ck - 1;
                float a0 = forma_tau * float(ck) / float(forma_q) + (cs > 0 ? del : -del);
                float a1 = forma_tau * float(nk) / float(forma_q) + (cs > 0 ? -del : del);
                for (int i = 0; i <= 20; i++){
                    float a = a0 + (a1 - a0) * float(i) / 20.0;
                    int pt = addpoint(0, forma_sf_rim(cl, a, forma_lift, forma_rtop));
                    setpointattrib(0, "Cd", pt, set(1.0, 1.0, 1.0));
                    setpointattrib(0, "Alpha", pt, 0.94);
                    addvertex(0, prim, pt);
                }
                ck = ((nk % forma_q) + forma_q) % forma_q;
                cs = -cs;
                forma_seen[(cl * forma_q + ck) * 2 + (cs > 0 ? 0 : 1)] = 1;
                // the band edge leaving the corner that arc landed on. v is
                // constant along an edge; the half twist is why the lower rim
                // reads it as +side and the upper as -side.
                float ev = float(cl == 0 ? cs : -cs);
                for (int i = 0; i <= 16; i++){
                    float u = float(i) / 16.0;
                    float s = cl == 0 ? u : 1.0 - u;
                    int pt = addpoint(0, forma_sf_band(ck, forma_q, s, ev, forma_lift,
                                                       forma_rtop, forma_hw, forma_mrho, forma_mz));
                    setpointattrib(0, "Cd", pt, set(1.0, 1.0, 1.0));
                    setpointattrib(0, "Alpha", pt, 0.94);
                    addvertex(0, prim, pt);
                }
                cl = 1 - cl;
                cs = -cs;
                guard = guard + 1;
                if (cl == lv && ck == k0 && cs == sd0) break;
                if (guard > 4 * forma_q + 4) break;
            }
        }
    }
}

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. 128 · SEIFERT SURFACE — Herbert Seifert, 1935
//   smooth every crossing with the orientation → disjoint Seifert circles
//   span each circle with a disc at its own height, join with a half-twisted band per crossing
//   χ = discs − bands,   genus = (2 − χ − boundary components) / 2
//   the (2,q) diagram: 2 discs, q bands, χ = 2 − q
// 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 7 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.
// https://forma-gen.com/#plate=seifert

// 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_crossings = 3 + forma_tweak("crossings_tweak");     // q — crossings in the (2,q) diagram · live 2 .. 7
var p_lift      = 1.2 + forma_tweak("lift_tweak");        // gap between the two disc levels · live 0.8 .. 1.5
var p_width     = 0.4 + forma_tweak("width_tweak");       // band width / disc radius · live 0.28 .. 0.5
var p_taper     = 0.3 + forma_tweak("taper_tweak");       // upper disc smaller by · live 0.14 .. 0.36
var p_rulings   = 6 + forma_tweak("rulings_tweak");       // band wireframe density · live 4 .. 14
var p_tilt      = 30 + forma_tweak("tilt_tweak");         // camera elevation (deg) · live 24 .. 42

// 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.7736467584036291;   // 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();

// Seifert's algorithm on the (2, q) torus link: two Seifert circles, a disc
// on each at different heights, and q half-twisted bands joining them, one
// per crossing — an orientable surface whose boundary is the link. Built as
// the page builds it: the lower disc of radius 1 at z = −lift/2, the upper of
// radius 1 − taper at +lift/2, each band a straight core from rim to rim
// with a width vector that turns from +e_θ to −e_θ along it (the half twist
// that IS the crossing), and the boundary found by walking rim arc, band
// edge, rim arc, band edge until the walk closes — one closed curve when q
// is odd, which is the plate's proof that the boundary is a knot. The
// tumble at 0.055 rad/s from this plate's phase, at the elevation on the
// slider, as on the page. Parts: 0 and 1 the boundary curves (the second
// exists only for even q; otherwise it repeats the first), 2 and 3 the lower
// and upper rims, and 4 … 4+q−1 each band's rulings as a zigzag across it,
// the hops between rulings running along the band's own edges. The page's
// two-sided shading and its disc washes are fills, and stay the stroke's.
// waived: wash — it is the alpha of the page's disc and band fills, which are not paths
// parts: 7
var q = Math.max(2, Math.round(p_crossings)), lift = p_lift, rTop = 1 - p_taper;
var hw = Math.min(p_width / 2, 0.9 * Math.PI * rTop / q);
function forma_th(k){ return 2 * Math.PI * k / q; }
var Lc = Math.sqrt((1 - rTop) * (1 - rTop) + lift * lift);
var mRho = -lift / Lc, mZ = (rTop - 1) / Lc;
function forma_coreRho(s){ return 1 + (rTop - 1) * s; }
function forma_coreZ(s){ return -lift / 2 + lift * s; }
function forma_bandPt(k, s, v){
  var c = Math.cos(Math.PI * s), sn = Math.sin(Math.PI * s);
  var rho = forma_coreRho(s) - hw * v * sn * mRho, z = forma_coreZ(s) - hw * v * sn * mZ;
  var a = forma_th(k) + hw * v * c / Math.max(0.25, rho);
  return [rho * Math.cos(a), rho * Math.sin(a), z];
}
function forma_radOf(level){ return level ? rTop : 1; }
function forma_zOf(level){ return level ? lift / 2 : -lift / 2; }
function forma_rimPt(level, a){ return [forma_radOf(level) * Math.cos(a), forma_radOf(level) * Math.sin(a), forma_zOf(level)]; }
// the boundary: alternate rim arc and band edge until the walk closes
var seen = {}, loops = [];
function forma_key(lv, k, sd){ return lv + ':' + (((k % q) + q) % q) + ':' + (sd > 0 ? 1 : -1); }
for (var lv = 0; lv < 2; lv++) for (var k = 0; k < q; k++) for (var sdi = 0; sdi < 2; sdi++){
  var sd0 = sdi ? -1 : 1;
  if (seen[forma_key(lv, k, sd0)]) continue;
  var pts3 = [], cl = lv, ck = k, cs = sd0;
  do {
    seen[forma_key(cl, ck, cs)] = 1;
    var fwd = cs > 0, del = hw / forma_radOf(cl), nk = fwd ? ck + 1 : ck - 1;
    var a0 = forma_th(ck) + (fwd ? del : -del), a1 = forma_th(nk) + (fwd ? -del : del);
    for (var i = 0; i <= 20; i++) pts3.push(forma_rimPt(cl, a0 + (a1 - a0) * i / 20));
    ck = ((nk % q) + q) % q; cs = -cs;
    seen[forma_key(cl, ck, cs)] = 1;
    var v = cl === 0 ? cs : -cs;
    for (var i2 = 0; i2 <= 16; i2++){ var u = i2 / 16; pts3.push(forma_bandPt(ck, cl === 0 ? u : 1 - u, v)); }
    cl = 1 - cl; cs = -cs;
  } while (!(cl === lv && ck === k && cs === sd0));
  loops.push(pts3);
}
var rot = forma_t * 0.055 + forma_phase * 6.28318, el = p_tilt * Math.PI / 180;
var cr = Math.cos(rot), sr = Math.sin(rot), ce = Math.cos(el), se = Math.sin(el);
var rMax = 1 + hw;
var sc = Math.min(forma_W, forma_H) * 0.46 / Math.max(rMax, rMax * se + lift / 2 * ce);
var cx = forma_W / 2, cy = forma_H / 2;
function forma_proj(P){
  var xr = P[0] * cr + P[1] * sr, yr = -P[0] * sr + P[1] * cr;
  return forma_pt(cx + xr * sc, cy - (yr * se + P[2] * ce) * sc);
}
var pts = [], part = forma_part;
if (part <= 1){
  var loop = loops[Math.min(part, loops.length - 1)];
  for (var a = 0; a < loop.length; a++) pts.push(forma_proj(loop[a]));
  createPath(pts, [], [], true);
} else if (part <= 3){
  var level = part - 2;
  for (var b = 0; b < 64; b++) pts.push(forma_proj(forma_rimPt(level, b / 64 * 2 * Math.PI)));
  createPath(pts, [], [], true);
} else {
  var band = (part - 4) % q, NR = Math.max(4, Math.round(p_rulings)) * 2;
  for (var r = 0; r <= NR; r++){
    var s = r / NR, down = (r % 2 === 0);
    for (var j = 0; j <= 3; j++){
      var vv = down ? -1 + 2 * j / 3 : 1 - 2 * j / 3;
      pts.push(forma_proj(forma_bandPt(band, s, vv)));
    }
  }
  createPath(pts, [], [], false);
}