Quick Reference
SWLimacon is a SketchWave polar curve class representing the entire limaçon family. The curve is defined by r = a + b·cos(θ) (or the sine variant), sampled over θ ∈ [0, 2π) at SAMPLE_COUNT points and drawn as a closed polygon. The two parameters a and b together control the shape; their ratio |a/b| determines which of the five major variants appears.
- Design Pattern: Polar curve (not composition or inheritance)
- Equations: r = a + b·cos(θ) or r = a + b·sin(θ)
- Negative r is allowed: when a < b, r goes negative for part of θ range, creating an inner loop automatically
- Internal Structure:
SAMPLE_COUNT = 360polygon vertices over [0, 2π); naturally closed - Dependencies: SWPoint, SWColor, SWGrid, p5.js
- Variants: Convex, Trisectrix (a=2b), Dimpled, Cardioid (a=b), Inner Loop — all determined by |a/b|
- Common Uses: Polar curve exploration, limaçon family classification, variant morphing animation, Pascalian curve art
📖 History
Pronunciation: limaçon is a French word pronounced “LEE-mah-sohn” (IPA: /li.ma.sɔ̃/). In casual English usage you will also hear “LIM-uh-son”. The cedilla on the c (ç) gives it the soft /s/ sound — without it the word would end in a hard /k/.
The limaçon was first studied by Étienne Pascal (1588–1651), father of the famous mathematician Blaise Pascal, in the early 17th century. The name was given by the mathematician Gilles de Roberval (1602–1675), who named it after the Latin word limax meaning snail — a fitting description for the curve's shell-like spiral outline, especially when an inner loop is present.
The limaçon belongs to the class of Pascalian curves (limaçons of Pascal) and is defined as the locus of a point on a circle rolling around a fixed circle of the same radius. The cardioid (a = b) is the most famous member of this family — its name comes from the Greek kardia (heart), inspired by its distinctive heart-like shape.
The special case known as the trisectrix (a = 2b) was historically used to trisect an arbitrary angle — one of the classical geometric construction problems that cannot be solved with compass and straightedge alone.
Overview
The SWLimacon class draws a polar limaçon as a closed polygon. The general form in polar coordinates is:
r = a + b · cos(θ) (cosine form, default; pole at left when b > 0)
r = a + b · sin(θ) (sine form; 90° CCW rotation of the cosine form)
The shape is completely determined by the two parameters a and b:
- a (offset) — a constant added to the oscillation. Controls where r sits when cosθ = 0.
- b (amplitude) — the oscillation range. Controls how much r varies as θ sweeps from 0 to 2π.
Maximum radius: a + b — reached at θ = 0 (cos form).
Minimum radius: a − b — negative if a < b (creates inner loop).
Range of r: [a−b, a+b].
When a = b: minimum r = 0 — one cusp at pole = cardioid.
Negative Radius and the Inner Loop
When a < b, the value a − b is negative, so r goes negative for a range of θ values. In polar coordinates, a negative r places the point in the opposite direction from θ — effectively reflecting it through the pole. This is exactly what creates the second inner loop. SWLimacon uses the direct conversion:
x = r · cos(θ)
y = r · sin(θ)
With this formulation, negative r is handled automatically — no special-case logic is needed. When r is negative, cos/sin simply place the vertex in the reflected direction, naturally tracing out the inner loop as θ passes through the range where r < 0.
p5.js's non-zero winding fill rule then fills both loops independently when the path self-intersects, giving the inner loop a distinct filled region.
Cosine vs Sine Form
The cosine form opens to the right (pole on the left in standard math orientation). The sine form is a pure 90° CCW rotation of the cosine form — the shapes are identical; only the orientation differs. The toggle is controlled by the useCosine constructor parameter and the setUseCosine() method.
Rotation
SWLimacon supports two layers of rotation, both applied about the center point (the pole):
rotationDeg— Static base rotation set bysetRotation(). Persists across frames; survivesreset().rotation— Accumulated rotation incremented byrotate(). Starts at 0; cleared byreset().
Effective rotation = rotationDeg + rotation. All angles are CCW positive (user-space convention).
📊 The Five Variants
The limaçon's appearance depends entirely on the ratio |a/b|. Here are all five named variants, plus the two circle degenerate cases:
| Condition | Variant Name | |a/b| range | Preset (a, b) | Description |
|---|---|---|---|---|
b = 0 |
Circle (b = 0) | — | any a, b = 0 | Degenerate; b = 0 means r = a always — a perfect circle of radius a |
a/b > 2 |
Convex | > 2 | a=8, b=3 | Smooth oval-like shape; r always positive; no dimple, no cusp, no inner loop |
a/b = 2 |
Trisectrix | = 2 | a=6, b=3 | Convex limaçon with an inflection point; historically used for angle trisection |
1 < a/b < 2 |
Dimpled | between 1 and 2 | a=7, b=5 | r stays non-negative but dips inward; visible concavity (dimple) on the near side |
a/b = 1 |
Cardioid | = 1 | a=5, b=5 | r just reaches zero; exactly one cusp at the pole; the classic heart-shaped curve |
0 < a/b < 1 |
Inner Loop | < 1 | a=3, b=5 | r goes negative; a second inner loop appears inside the main outer loop |
a = 0 |
Circle (a = 0) | = 0 | a=0, b=4 | Degenerate; r = b·cosθ traces a circle of diameter b (passes through origin) |
The Trisectrix and Cardioid are both boundary cases — at precisely a/b = 2 and a/b = 1 respectively. SWLimacon uses a small epsilon for detection, so exact values like new SWLimacon(p, 6, 3, ...) will be labelled "Trisectrix".
Convex
a/b > 2. The curve is entirely convex; r is always positive. Example: r = 8 + 3·cos(θ).
new SWLimacon(center, 8, 3, stroke, fill)
Trisectrix
a/b = 2 exactly. Convex with an inflection point. Example: r = 6 + 3·cos(θ).
new SWLimacon(center, 6, 3, stroke, fill)
Dimpled
1 < a/b < 2. r ≥ 0, but a concave dip appears. Example: r = 7 + 5·cos(θ).
new SWLimacon(center, 7, 5, stroke, fill)
Cardioid
a/b = 1. One cusp at pole, r touches zero. Example: r = 5 + 5·cos(θ).
new SWLimacon(center, 5, 5, stroke, fill)
Inner Loop
a/b < 1. r goes negative; inner loop forms inside outer curve. Example: r = 3 + 5·cos(θ).
new SWLimacon(center, 3, 5, stroke, fill)
Constructor
new SWLimacon(center, a, b, strokeColor, fillColor, thickness, useCosine, rotationDeg)Creates a new SWLimacon instance. All constructor values are saved as originals for reset().
| Parameter | Type | Default | Description |
|---|---|---|---|
center |
SWPoint | required | The pole (origin) of the limaçon in user (grid) coordinates. For the cardioid variant, the cusp is here. For inner loop variants, both loops surround this point. |
a |
number | required | The additive offset. Controls the overall bias of r. Increasing a moves the variant toward Convex; decreasing moves toward Inner Loop. |
b |
number | required | The oscillation amplitude. Controls how much r varies as θ sweeps 0 to 2π. Maximum radius = a+b; minimum radius = a−b. |
strokeColor |
SWColor | undefined | undefined | Outline color. undefined = no stroke drawn. |
fillColor |
SWColor | undefined | undefined | Fill color for the limaçon interior. undefined = no fill (transparent). p5.js non-zero winding fills both loops for inner-loop variants. |
thickness |
number | 2 | Stroke weight in pixels. |
useCosine |
boolean | true | true: r=a+b·cos(θ) (opens right). false: r=a+b·sin(θ) (90° CCW rotation; opens upward). |
rotationDeg |
number | 0 | Static base rotation in CCW degrees; applied in addition to accumulated rotation. |
const stroke = SWColor.fromHex('#4a0080', 100, 'limStroke');
const fill = SWColor.fromHex('#9933cc', 55, 'limFill');
const center = new SWPoint(0, 0);
// Dimpled limaçon (a/b = 1.4)
let dimpled = new SWLimacon(center, 7, 5, stroke, fill);
// Cardioid (a = b)
let cardioid = new SWLimacon(center, 5, 5, stroke, fill);
// Inner loop (a < b)
let inner = new SWLimacon(center, 3, 5, stroke, fill);
// Convex, sine form (rotated 90° CCW, opens upward)
let convex = new SWLimacon(center, 8, 3, stroke, fill, 2, false);
// Pre-rotated 45°
let rotated = new SWLimacon(center, 6, 4, stroke, fill, 2, true, 45);
Properties
center SWPointThe limaçon's pole in user (grid) coordinates. Moving center.x or center.y repositions the entire curve. For the cardioid variant the cusp is here; for inner loop variants both loops are drawn relative to this point.
lim.center.x = 2; lim.center.y = -1;a numberAdditive offset. Together with b it determines the variant. Use setA() to change it; any real number is accepted (including 0 and negatives).
lim.setA(8); // moves toward Convex
lim.setA(3); // moves toward Inner Loop (if b > 3)
b numberOscillation amplitude. Controls the size variation around a. Max radius = a+b, min radius = a−b. Use setB() to change it.
lim.setB(5); // adjusts amplitude independently of a
useCosine booleanSelects the equation form. true (default) uses r=a+b·cos(θ) (opens right); false uses r=a+b·sin(θ) (opens upward, 90° CCW rotation).
lim.setUseCosine(false); // switch to sine form
rotationDeg numberStatic base rotation in CCW degrees. Set by setRotation(); survives reset().
lim.setRotation(90); // rotate 90° CCW (same as switching to sine form)rotation numberAccumulated rotation in degrees, incremented each frame by rotate(). Starts at 0; cleared by reset().
// Cleared automatically by reset(); read-only in normal useSWLimacon.SAMPLE_COUNT staticNumber of polygon vertices (default: 360 — one per degree over [0, 2π)). Higher values produce smoother curves. Can be changed globally at any time.
SWLimacon.SAMPLE_COUNT = 72; // faceted look
SWLimacon.SAMPLE_COUNT = 720; // ultra-smooth
Methods
Drawing Methods
draw()Draws the limaçon in raw screen (pixel) coordinates. The center SWPoint is interpreted as screen pixels. Prefer drawOnGrid() for standard canvas use with an SWGrid.
function draw() {
background(220);
limacon.draw();
}
drawOnGrid(grid)Draws the limaçon mapped through the given SWGrid's coordinate system. The grid handles the y-flip and coordinate scaling.
function draw() {
background(220);
grid.draw();
limacon.drawOnGrid(grid);
grid.updateScreenBounds();
}
Rotation Animation
rotate(deltaAngle)Spins the limaçon about its center (pole) by deltaAngle degrees (CCW+, CW−). Accumulates into this.rotation. Call once per frame before drawOnGrid().
const SPIN_SPEED = 45; // degrees per second
let prevT = 0;
function draw() {
const t = millis() / 1000;
const deltaT = t - prevT;
prevT = t;
background(220);
limacon.rotate(SPIN_SPEED * deltaT);
limacon.drawOnGrid(grid);
}
Mutation Methods
setA(a)Sets the offset parameter a. Any real value is accepted; negative values and zero are allowed. Changing a can shift the curve across variant boundaries — watch the variant badge update in real time in the demo.
// Pulse animation: morph through variants
function draw() {
const t = millis() / 1000;
const pulsedA = baseA + depth * sin(TWO_PI * speed * t);
limacon.setA(pulsedA);
limacon.drawOnGrid(grid);
}
setB(b)Sets the amplitude parameter b. Any real value is accepted. Note: setB(0) collapses the limaçon to a circle of radius a.
limacon.setB(6); // increase oscillation amplitude
setUseCosine(val)Switches between the cosine form (true) and sine form (false). The sine form is a 90° CCW rotation of the cosine form.
limacon.setUseCosine(false); // switch to sine form
setRotation(deg)Sets the static base rotation in CCW degrees. Does not affect the accumulated rotation (from rotate()).
setStrokeColor(swColor)Replaces the stroke SWColor object. Pass undefined to disable stroke.
setFillColor(swColor)Replaces the fill SWColor object. Pass undefined to disable fill.
setStrokeWeight(w)Sets the stroke thickness in pixels. 0 = no stroke.
setFillAlpha(alpha) / setStrokeAlpha(alpha)Convenience methods to change only the alpha channel (0–100) of the existing fill or stroke color without rebuilding the SWColor object.
limacon.setFillAlpha(30); // semi-transparent fill
limacon.setStrokeAlpha(80); // slightly faded stroke
Variant Classification
getVariantName() instanceReturns a human-readable string describing the current variant based on the current a and b values. Call as often as needed (typically once per frame when displaying the variant badge).
const name = limacon.getVariantName();
// e.g. "Dimpled", "Cardioid (a = b)", "Inner Loop", "Convex", "Trisectrix (a = 2b)"
SWLimacon.getVariantName(a, b) staticSame as the instance method but can be called without an existing SWLimacon object. Useful for pre-checking what variant a pair of (a, b) values will produce.
SWLimacon.getVariantName(6, 3); // returns "Trisectrix (a = 2b)"
Utility Methods
reset()Restores all original constructor values: a, b, strokeColor, fillColor, thickness, useCosine. Clears accumulated rotation to 0. Does not move the center point.
limacon.reset(); // back to factory state
SWLimacon.copy(other) staticCreates a deep copy of another SWLimacon object. Useful for saving state before making experimental changes.
let saved = SWLimacon.copy(limacon);
toString()Returns a descriptive string showing the formula, current a/b values, and the variant name. Useful for debugging.
console.log(limacon.toString());
// → "SWLimacon: r = 5.00 + 4.00·cos(θ) | Dimpled"
Code Examples
Example 1: Static Convex Limaçon on a Grid
let grid, limacon;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
grid = new SWGrid({ UL: new SWPoint(-12, 12), LR: new SWPoint(12, -12) });
grid.showAxes = true;
const stroke = SWColor.fromHex('#1e4d00', 100, 's');
const fill = SWColor.fromHex('#3cb371', 60, 'f');
limacon = new SWLimacon(new SWPoint(0, 0), 8, 3, stroke, fill, 2);
// a/b = 8/3 ≈ 2.67 → Convex
}
function draw() {
background(0, 0, 93);
grid.draw();
limacon.drawOnGrid(grid);
grid.updateScreenBounds();
}
Example 2: Cardioid (a = b special case)
// SWLimacon can reproduce a cardioid exactly when a = b
const stroke = SWColor.fromHex('#880044', 100, 's');
const fill = SWColor.fromHex('#cc2266', 55, 'f');
let limacon = new SWLimacon(new SWPoint(0, 0), 5, 5, stroke, fill, 3);
// a/b = 1.0 → classified as "Cardioid (a = b)"
// equivalent to: r = 5 + 5·cos(θ) = 5·(1 + cos(θ))
Example 3: Inner Loop — Watch Negative r
// a=3, b=5: min r = 3-5 = -2 (negative!) → inner loop forms automatically
const stroke = SWColor.fromHex('#1a1a2e', 100, 's');
const fill = SWColor.fromHex('#16213e', 50, 'f');
let limacon = new SWLimacon(new SWPoint(0, 0), 3, 5, stroke, fill, 2);
// At θ where r < 0, x = r·cos(θ) and y = r·sin(θ) place the vertex in the
// opposite direction — forming the inner loop automatically.
Example 4: Variant-Morphing Pulse Animation
let grid, limacon;
const BASE_A = 5, BASE_B = 5;
const PULSE_SPEED = 0.5; // Hz
const PULSE_DEPTH = 4.0; // Δa — large enough to cross all variant boundaries
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
grid = new SWGrid({ UL: new SWPoint(-12, 12), LR: new SWPoint(12, -12) });
grid.showAxes = true;
const stroke = SWColor.fromHex('#4a0080', 100, 's');
const fill = SWColor.fromHex('#9933cc', 55, 'f');
limacon = new SWLimacon(new SWPoint(0, 0), BASE_A, BASE_B, stroke, fill, 3);
}
function draw() {
const t = millis() / 1000;
// Oscillate a: Inner Loop → Cardioid → Dimpled → Convex → back
const pulsedA = BASE_A + PULSE_DEPTH * sin(TWO_PI * PULSE_SPEED * t);
limacon.setA(pulsedA);
background(0, 0, 93, 30); // low alpha = motion trails
grid.draw();
limacon.drawOnGrid(grid);
grid.updateScreenBounds();
// Log the current variant
fill(0); noStroke(); textSize(14);
text(limacon.getVariantName(), 10, 20);
}
Example 5: Spinning Trisectrix
let grid, trisectrix;
let prevT = 0;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
grid = new SWGrid({ UL: new SWPoint(-12, 12), LR: new SWPoint(12, -12) });
const stroke = SWColor.fromHex('#006080', 100, 's');
const fill = SWColor.fromHex('#00b4d8', 60, 'f');
// a/b = 6/3 = 2 exactly → Trisectrix
trisectrix = new SWLimacon(new SWPoint(0, 0), 6, 3, stroke, fill, 2);
}
function draw() {
const t = millis() / 1000;
const deltaT = t - prevT;
prevT = t;
background(0, 0, 93);
grid.draw();
trisectrix.rotate(60 * deltaT); // 60°/s CCW spin
trisectrix.drawOnGrid(grid);
grid.updateScreenBounds();
}
Design Notes
- Negative r handled automatically: The conversion x = r·cos(θ), y = r·sin(θ) naturally handles negative r by placing vertices in the opposite direction. No special-case logic is needed; the inner loop emerges automatically from the math.
- Variant detection uses epsilon comparisons: Because a and b are floating-point numbers, exact equality tests (a/b == 1, a/b == 2) are unreliable. SWLimacon uses small epsilon values (±0.03 for cardioid, ±0.04 for trisectrix) to classify boundary cases correctly even when a/b has small floating-point imprecision.
- Two-pass drawing:
_drawShape()draws the fill pass first (with CLOSE), then the stroke pass (with CLOSE). This ensures the fill is always behind the stroke, even for inner-loop variants where the path self-intersects. - p5.js non-zero winding: When the limaçon path self-intersects (inner loop variant), p5.js's default non-zero winding fill rule fills both regions independently. This is intentional and gives inner-loop limaçons a distinct visual with both loops filled.
- Variant morphing animation: Because
setA()andsetB()accept any real value without clamping, the pulse animation can smoothly morph the curve through all five variants in a single continuous animation. This makes SWLimacon ideal for demonstrating the limaçon family to students. - Relationship to SWCardioid: SWLimacon with a = b is mathematically equivalent to SWCardioid with the same a. The cardioid r = a·(1+cosθ) = a + a·cosθ is exactly r = a + b·cosθ with b = a. SWCardioid exists as a dedicated class because it predates SWLimacon and has a simpler constructor for the common case.
- SAMPLE_COUNT = 360: At 360 samples (one per degree) the curve is smooth enough for all educational uses. For very large grids or print-quality output, increase to 720 or higher.
Source Code
The complete SWLimacon class implementation:
Show/Hide Source Code
// Loading source code...
Related SketchWaveJS Classes
- SWCardioid — The special case a = b; dedicated class for the heart-shaped curve.
- SWRose — Another polar curve class; r = a·cos(nθ) or r = a·sin(nθ).
- SWGrid — Coordinate system used by
drawOnGrid(). - SWPoint — Used as the center (pole) of the limaçon.
- SWColor — Used for stroke and fill colors.