◎ Parametric Mathematics
The Curve Equations
SWLoopyCircle uses a direct parametric form that generates smooth closed curves with inner loops. Two frequency components combine — a primary circle and a secondary oscillation — to produce the characteristic Spirograph-like shape:
x(t) = r · cos(t) + d · cos(b · t)
y(t) = r · sin(t) − d · sin(b · t)
t ∈ [0, 2π), 600 sample points
Understanding the Parameters
| Parameter | Role | Effect |
r |
Main radius |
Sets the overall size of the figure. The curve spans roughly r+d in radius. |
d |
Displacement |
Controls the depth of the inner loops. Larger d = deeper, more dramatic loops. When d > r, loops extend outside the main circle. |
b |
Frequency (integer ≥ 0) |
Number of complete secondary oscillations per revolution. Creates b+1 inner loops when d > r/b; smooth with dimples otherwise. b=0: offset circle; b=1: ellipse. |
Loop Count by b
| b | Loops (d > r/b) | Shape / Description |
| 0 | — | Offset circle: circle of radius r shifted d units along x; cos(0)=1, sin(0)=0 |
| 1 | — | Ellipse: semi-axes (r+d) and (r−d); collapses to a line segment when d = r |
| 2 | 3 | 3-loop figure; dimpled when d < r/2; loops appear once d > r/2 |
| 3 | 4 | 4-loop curve; balanced and symmetric (default with r=5, d=2) |
| 4 | 5 | 5-loop figure; resembles a rounded star |
| 5 | 6 | 6-loop star |
| 6 | 7 | 7-loop rosette |
| 7–10 | 8–11 | Dense multi-loop rosettes with intricate overlap |
Why the Negative Sign in y?
The primary circle terms (r·cos(t), r·sin(t)) rotate counter-clockwise. The secondary terms add oscillation at frequency b. The negative sign in the y-equation — −d·sin(b·t) instead of +d·sin(b·t) — causes the secondary component to rotate in the opposite direction. This opposing rotation is what creates the crossing, looping path geometry.
Boundary Behavior
The curve's overall bounding radius oscillates between |r − d| and r + d. Self-intersection (loops) is governed by a separate threshold:
- d ≤ r/b — curve is smooth with inward dimples; no self-intersections.
- d > r/b — curve self-intersects, producing exactly b+1 inner loops.
- d < r — loops remain interior to the main (navy guide) circle.
- d = r — inner boundary touches the origin; loop tips become cusps.
- d > r — loops extend past the main circle into the amber outer boundary region.
Connection to Spirograph / Trochoids
The Spirograph toy (Denys Fisher, 1965) produces hypotrochoids (inner gear rolling) and epitrochoids (outer gear rolling). The SWLoopyCircle parametric form is equivalent to a simplified trochoid with gear ratio b. The parameter b corresponds to the ratio of the fixed ring gear to the rolling gear; with integer b, the curve closes perfectly after one revolution. Non-integer b values produce open spirals that require many revolutions to close — SWLoopyCircle uses integer b only.
Constructor
new SWLoopyCircle(center, r, d, b, fillColor, strokeColor, thickness, rotationDeg)
| Parameter | Type | Default | Description |
center | SWPoint | required | Center position in user (grid) coordinates |
r | number | 5 | Main radius in grid units (min 0.01) |
d | number | 2 | Displacement arm length in grid units (min 0); loops appear when d > r/b; d=0 gives a pure circle of radius r |
b | integer | 3 | Frequency multiplier (min 0); b+1 loops when d > r/b; fractional values are rounded; 0 = offset circle, 1 = ellipse |
fillColor | SWColor | undefined | Fill color; undefined = no fill |
strokeColor | SWColor | undefined | Stroke color; undefined = no stroke |
thickness | number | 2 | Stroke weight in pixels |
rotationDeg | number | 0 | Static base rotation in CCW degrees; preserved across reset() |
// 4-loop loopy circle (b=3, d=2 > r/b=1.67)
const center = new SWPoint(0, 0, undefined, 8, new SWColor(235, 60, 25, 100));
const fill = SWColor.fromHex('#4a6fd8', 35, 'fill');
const stroke = SWColor.fromHex('#1a2d6c', 100, 'stroke');
const lc = new SWLoopyCircle(center, 5, 2, 3, fill, stroke, 2, 0);
lc.drawOnGrid(grid);
Code Examples
1. Basic Setup (p5.js global mode)
let grid, lc;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
const center = new SWPoint(0, 0, undefined, 8,
new SWColor(235, 60, 25, 100));
const fill = SWColor.fromHex('#4a6fd8', 35, 'fill');
const stroke = SWColor.fromHex('#1a2d6c', 100, 'stroke');
lc = new SWLoopyCircle(center, 5, 2, 3, fill, stroke, 2, 0);
}
function draw() {
background(0, 0, 93);
grid.draw();
lc.drawOnGrid(grid);
grid.updateScreenBounds();
}
2. Spin Animation
let prevT = 0;
const SPIN_SPEED = 45; // degrees per second
function draw() {
const t = millis() / 1000;
const deltaT = (prevT > 0) ? (t - prevT) : 0;
prevT = t;
background(0, 0, 93);
grid.draw();
lc.rotate(SPIN_SPEED * deltaT); // CCW spin
lc.drawOnGrid(grid);
grid.updateScreenBounds();
}
3. Breathe Animation (Main Radius Oscillation)
const BASE_R = 5.0;
const BREATHE_SPEED = 0.5; // Hz
const BREATHE_AMOUNT = 1.5; // grid units
function draw() {
const t = millis() / 1000;
background(0, 0, 93);
grid.draw();
// Sinusoidal radius oscillation
const r = BASE_R + BREATHE_AMOUNT * Math.sin(2 * Math.PI * BREATHE_SPEED * t);
lc.setR(Math.max(0.5, r));
lc.drawOnGrid(grid);
grid.updateScreenBounds();
}
4. Three-Loop Figure (b = 2, d > r/2)
// 3-loop figure — d=3 exceeds threshold r/b = 5/2 = 2.5, so loops appear
const center = new SWPoint(0, 0, undefined, 8,
new SWColor(235, 60, 25, 100));
const fill = SWColor.fromHex('#6a3fd8', 40, 'fill'); // purple fill
const stroke = SWColor.fromHex('#2d0a6c', 100, 'stroke');
const lc = new SWLoopyCircle(center, 5, 3, 2, fill, stroke, 2, 0);
5. Star Rosette (b = 5, d > r)
// 6-loop star (b+1=6) — d=5 > r/b=0.8, and d > r so loops extend beyond main circle
const lc = new SWLoopyCircle(center, 4, 5, 5, fill, stroke, 2, 0);
6. Copy and Reset
// Deep copy
const copy = SWLoopyCircle.copy(lc);
// Reset to constructor values
lc.reset();
// Center position preserved; r, d, b, rotation restored
// toString
console.log(lc.toString());
// → SWLoopyCircle(center:(0.00, 0.00), r:5.00, d:2.00, b:3, rotation:0.0°)