PL. 44 · CURVES / ENVELOPE / MODULAR CHORDS
Cardioid Chord Envelope
Luigi Cremona, 1862 · string-art tradition
OPEN THE LIVE PLATE ▸DEFINITION
N points on a circle: Pₖ = e^(2πik/N) chord: Pₖ → P₍m·k mod N₎ envelope: an epicycloid with m − 1 cusps
NOTES
Join every point k on a circle to point m·k, working modulo N, and a curve nobody drew appears where the chords crowd together: the cardioid at m = 2, the nephroid at 3, an epicycloid of m − 1 cusps in general. Cremona described the construction; string artists have been hammering nails around circles and threading it ever since. The same picture hides in the Mandelbrot set, whose main cardioid is the m = 2 case.
PROVENANCE
- Origin
- L. Cremona, "Introduzione ad una teoria geometrica delle curve piane", 1862; folk practice as string art
- Standing
- Public domain — 19th-century geometry
- Constants
- Integer m resolves the envelope sharply; the sweep drifts through them
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. 44 · CARDIOID CHORD ENVELOPE — Luigi Cremona, 1862 · string-art tradition
// N points on a circle: Pₖ = e^(2πik/N)
// chord: Pₖ → P₍m·k mod N₎
// envelope: an epicycloid with m − 1 cusps
// 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=cremona
float p_m = 2 + chf('m_tweak'); // m — multiplier · live 2 .. 8
float p_n = 200 + chf('n_tweak'); // N — points · live 80 .. 320
float p_off = 0 + chf('off_tweak'); // shift — k·m + s · live 0 .. 80
// 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)));
}
// String art on N nails: chord k → m·k + s for every k. The envelope is
// never drawn — the eye finds it where neighbouring chords crowd. A
// non-integer m lets the family slide between the classic figures; the
// additive shift s is a second family from the same nails.
int forma_circ = 128;
float TAU = 6.28318530718;
int n = int(rint(p_n));
// the nail circle, faint, as the plate strokes it
int circ = addprim(0, "polyline");
for (int i = 0; i <= forma_circ; i++){
float th = float(i) / float(forma_circ) * TAU;
int pt = addpoint(0, set(cos(th), -sin(th), 0.0));
setpointattrib(0, "Cd", pt, forma_ramp(0.95));
setpointattrib(0, "Alpha", pt, 0.22);
addvertex(0, circ, pt);
}
// the chords — one two-point polyline each, colour running along k inside
// the bright lobe of the ramp
for (int k = 1; k <= n; k++){
float a0 = float(k) * TAU / float(n);
float a1 = (float(k) * p_m + p_off) % float(n) * TAU / float(n);
vector col = forma_ramp(0.84 + 0.2 * float(k) / float(n));
int chord = addprim(0, "polyline");
// canvas y runs down; negated so the cusps sit as the plate shows them
int pa = addpoint(0, set(cos(a0), -sin(a0), 0.0));
int pb = addpoint(0, set(cos(a1), -sin(a1), 0.0));
setpointattrib(0, "Cd", pa, col);
setpointattrib(0, "Cd", pb, col);
setpointattrib(0, "Alpha", pa, 0.38);
setpointattrib(0, "Alpha", pb, 0.38);
addvertex(0, chord, pa);
addvertex(0, chord, pb);
}
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. 44 · CARDIOID CHORD ENVELOPE — Luigi Cremona, 1862 · string-art tradition
// N points on a circle: Pₖ = e^(2πik/N)
// chord: Pₖ → P₍m·k mod N₎
// envelope: an epicycloid with m − 1 cusps
// 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.
// https://forma-gen.com/#plate=cremona
// 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_m = 2 + forma_tweak("m_tweak"); // m — multiplier · live 2 .. 8
var p_n = 200 + forma_tweak("n_tweak"); // N — points · live 80 .. 320
var p_off = 0 + forma_tweak("off_tweak"); // shift — k·m + s · live 0 .. 80
// 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.9929179286118597; // 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 Cremona modular multiplication table: n nails round a circle and, from
// each nail k, a chord to nail (k·m + off) mod n — the cardioid at m = 2, the
// nephroid at 3, the whole family of epicycloid envelopes as m climbs. One
// path: from each nail the chord is walked out and straight back (a chord
// retraced lies on itself), then the rim is followed to the next nail, so the
// circle itself is drawn by the hops and Trim Paths lays the chords down in
// order, as the page colours them. The table turns slowly from this plate's
// own phase, as on the page.
var R = Math.min(forma_W, forma_H) * 0.44, cx = forma_W / 2, cy = forma_H / 2;
var TAU = 6.283185307179586, n = Math.round(p_n);
var base = forma_phase * TAU + forma_t * 0.03;
function forma_nail(a){ return forma_pt(cx + R * Math.cos(base + a * TAU / n), cy + R * Math.sin(base + a * TAU / n)); }
var pts = [];
for (var k = 1; k <= n; k++){
pts.push(forma_nail(k));
pts.push(forma_nail((k * p_m + p_off) % n));
pts.push(forma_nail(k));
pts.push(forma_nail(k + 0.5)); // along the rim to the next nail
}
pts.push(forma_nail(n + 1));
createPath(pts, [], [], false);