Overview
SWBellCurve draws a Gaussian bell curve as a closed, filled shape: the smooth arc on top and a perfectly flat baseline on the bottom. It is a standalone Core Class — it does not extend any other SketchWaveJS class.
SWBellCurve both are in pixels.
Quick-Reference
cx, cy = bounding-box centre. Peak is amplitude/2 above; baseline is amplitude/2 below.
breatheVertical(), breatheHorizontal(), and breathe() (both axes) each take a SWSinusoid.
rotate(degPerSec, t) spins about the bounding-box centre; transform() combines breathe + rotate.
Computed getters: fwhm, totalWidth, area — live-updated during animation.
How the Curve is Drawn
Understanding how the bell shape is constructed makes it easier to use the class confidently and to predict what will happen when you change parameters.
Step 1 — The Gaussian Formula
The height of the bell curve at any horizontal distance x from the centre is given by the Gaussian function:
Where:
- A — amplitude (peak height in pixels)
- σ — sigma, the standard deviation (controls width in pixels)
- x — horizontal distance from the centre of the peak
- e — Euler's number (≈ 2.718)
💡 Key values of the Gaussian
- At
x = 0: f(0) = A × e0 = A (the peak) - At
x = ±σ: f = A × e−0.5 ≈ 0.607 A (60.7% of peak) - At
x = ±2σ: f = A × e−2 ≈ 0.135 A (13.5% of peak) - At
x = ±3.5σ: f ≈ 0.002 A (the default drawing range)
Step 2 — Positioning: Bounding-Box Centre
Like most SketchWaveJS shapes, SWBellCurve is positioned by the
centre of its bounding box — not by its peak. The bounding
box spans exactly the amplitude from top to bottom, centred on (cx, cy).
The purple dot marks (cx, cy). The peak is amplitude/2 above it; the baseline is amplitude/2 below.
Step 3 — The Local Coordinate System
Inside _drawAtPx() the code calls
translate(cx, cy) to move the origin to the bounding-box centre.
Everything is then drawn in local coordinates where:
- x = 0 is the vertical symmetry axis of the bell
- y = 0 is the bounding-box centre (same as
cyin screen space) - y = −amplitude/2 is the peak (top of bounding box)
- y = +amplitude/2 is the baseline (bottom of bounding box)
The formula in local coordinates becomes:
At x = 0: y = amplitude/2 − amplitude = −amplitude/2 (the peak, at the top).
As |x| grows large: y → +amplitude/2 (the baseline, at the bottom).
Step 4 — Building the Shape with beginShape()
The class loops through resolution evenly-spaced x values from
−range to +range (where range = σ × drawRangeFactor),
computing a vertex(x, y) for each one. To guarantee a flat bottom regardless
of the drawRangeFactor setting, two explicit corner vertices are added:
beginShape();
// Flat baseline — left corner
vertex(-range, halfAmp);
// Gaussian arc: left tail → peak → right tail
for (let i = 0; i <= resolution; i++) {
const x = -range + i * step;
const y = halfAmp - amplitude * Math.exp(-(x * x) / twoSig2);
vertex(x, y);
}
// Flat baseline — right corner
vertex(range, halfAmp);
endShape(CLOSE); // p5 closes the shape by connecting last → first vertex
Because endShape(CLOSE) connects the last vertex
(+range, halfAmp) directly back to the first vertex
(−range, halfAmp), the bottom of the shape is always a
perfectly horizontal line.
drawRangeFactor values, the "flat" bottom would have a visible
upward bow. Adding the explicit corners fixes this for any range.
Step 5 — Rotation
After translating to (cx, cy), the code calls
rotate(radians(this._rotAngle)) before drawing any vertices.
This spins the entire bell curve — including its flat baseline — around
the bounding-box centre. The pivot is always (cx, cy); the shape
never drifts.
Step 6 — Three Breathing Modes
SWBellCurve has three independent breathing modes, each driven by its
own SWSinusoid:
breatheVertical()
Scales amplitude only. The bell gets taller or shorter while its width (sigma) stays fixed.
Use for a shape that "inhales" up and down.
breatheHorizontal()
Scales sigma only. The bell spreads wider or narrows while its height (amplitude) stays fixed.
Use for a shape that "exhales" sideways.
breathe()
Scales both amplitude AND sigma by the same factor. The shape grows and shrinks uniformly.
Use for a simple "pulsing" effect.
Constructor
let bell = new SWBellCurve(cx, cy, amplitude, sigma, fillColor, options);
Required Parameters
| Parameter | Type | Description |
|---|---|---|
cx |
number | Centre x of the bounding box (pixels). |
cy |
number | Centre y of the bounding box (pixels). |
amplitude |
number | Height from baseline to peak (pixels). Clamped ≥ 1. |
sigma |
number | Standard deviation — controls the width of the bell (pixels). Clamped ≥ 1. |
fillColor |
SWColor | null | Fill colour. Pass null for no fill (outline only). |
Options Object
| Key | Type | Default | Description |
|---|---|---|---|
strokeColor |
SWColor | null | null | Outline colour. null = no stroke. |
strokeWeight |
number | 2 | Stroke width in pixels. |
drawRangeFactor |
number | 3.5 | How many σ to draw on each side of the peak. Minimum 1. Larger values show more of the tails; smaller values crop them. |
resolution |
number | 120 | Number of sample points along the arc. Minimum 20. Higher values produce a smoother curve at the cost of a little more computation. |
Constructor Example
let bell;
function setup() {
createCanvas(640, 480);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
const fillC = new SWColor(240, 55, 97, 100, "bellFill"); // soft indigo
const strokeC = new SWColor(244, 88, 64, 100, "bellStroke"); // deep indigo
bell = new SWBellCurve(width / 2, height / 2, 180, 60, fillC, {
strokeColor: strokeC,
strokeWeight: 2,
drawRangeFactor: 3.5,
resolution: 120
});
}
function draw() {
background(240, 20, 95);
bell.draw();
}
Computed Properties (Getters)
These read-only properties are computed on the fly from the current animation state. They automatically reflect any breathing in progress.
fwhm
Full Width at Half Maximum — the horizontal distance at which the curve drops to exactly half its peak height. A classic statistical measure of "how wide" the bell is.
Uses _currentSigma, so it changes during
breatheHorizontal() or breathe().
totalWidth
The pixel width of the drawn region: 2 × _currentSigma × drawRangeFactor.
This is the bounding box width (excluding stroke).
area
Approximate area under the bell curve (the mathematically exact integral from −∞ to +∞). This is an approximation because the actual drawn shape is clipped at the drawing range.
Uses both _currentAmplitude and _currentSigma.
Methods
Drawing
draw()
Render the bell curve in screen (pixel) coordinates. Call once per frame
inside draw(). Applies the current rotation, amplitude, and sigma.
bell.draw();
drawOnGrid(grid)
Parameter: grid (SWGrid) — the coordinate
system to use.
Renders the bell curve mapped through a SWGrid, translating
(cx, cy) from user coordinates to screen pixels, and scaling
amplitude and sigma by the grid’s y-scale and x-scale respectively.
bell.drawOnGrid(myGrid);
Breathing
breathe(sinusoid, t)
Parameters: sinusoid (SWSinusoid), t (number, seconds)
Scales both amplitude and sigma uniformly. The bell grows and shrinks as a whole without changing its proportions.
const pulse = new SWSinusoid(3.0, 0.75, 1.25);
bell.breathe(pulse, t);
breatheVertical(sinusoid, t)
Parameters: sinusoid (SWSinusoid), t (number, seconds)
Scales amplitude only. The bell gets taller or shorter while its width (sigma) stays fixed. Use this for a shape that "breathes" up and down.
const tall = new SWSinusoid(2.0, 0.8, 1.2);
bell.breatheVertical(tall, t);
breatheHorizontal(sinusoid, t)
Parameters: sinusoid (SWSinusoid), t (number, seconds)
Scales sigma only. The bell spreads wider or narrows while its height (amplitude) stays fixed. Use this for a shape that "spreads" sideways.
const wide = new SWSinusoid(4.0, 0.5, 1.5);
bell.breatheHorizontal(wide, t);
Rotation
rotate(degPerSec, t)
Parameters: degPerSec (number, degrees per second), t (number, seconds)
Sets the rotation angle to degPerSec × t. The pivot is always
the bounding-box centre (cx, cy). Positive values rotate clockwise.
bell.rotate(45, t); // 45 degrees per second, clockwise
Combined Transform
transform(options)
Apply breathing and/or rotation in a single call. All keys are optional:
| Key | Type | Effect |
|---|---|---|
sinusoid | SWSinusoid | Uniform breathe (both axes) |
sinusoidV | SWSinusoid | Vertical breathe (amplitude only) |
sinusoidH | SWSinusoid | Horizontal breathe (sigma only) |
t | number | Time in seconds |
degPerSec | number | Rotation rate; omit to skip rotation |
// Breathe vertically AND spin at the same time
bell.transform({ sinusoidV: tallSin, degPerSec: 30, t });
Reset
reset()
Restores _rotAngle, _currentAmplitude, and
_currentSigma to the values they had when the object was last
constructed or set via a setter. Call this to snap the shape back to its
"resting" state.
bell.reset();
toString
toString()
Returns: string — Human-readable summary of the current state.
console.log(bell.toString());
// → "SWBellCurve(cx:320.0, cy:240.0, amp:180.0, sigma:60.0)"
Setters
All setters also update the original value so that a subsequent
reset() returns to the new value rather than the constructor value.
| Method | Parameter | What it sets |
|---|---|---|
setCenter(cx, cy) | numbers | Move the bounding-box centre |
setAmplitude(a) | number | New amplitude; clamped ≥ 1. Updates originalAmplitude. |
setSigma(s) | number | New sigma; clamped ≥ 1. Updates originalSigma. |
setFillColor(c) | SWColor | null | Fill colour; null removes fill |
setStrokeColor(c) | SWColor | null | Stroke colour; null removes stroke |
setStrokeWeight(w) | number | Stroke width; clamped ≥ 0 |
setDrawRangeFactor(f) | number | Drawing range in σ units; clamped ≥ 1 |
Code Examples
Basic Usage
let bell;
function setup() {
createCanvas(640, 480);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
const fillC = new SWColor(240, 55, 97, 100, "f");
const strokeC = new SWColor(244, 88, 64, 100, "s");
bell = new SWBellCurve(width / 2, height / 2, 200, 70, fillC, {
strokeColor: strokeC
});
}
function draw() {
background(240, 20, 95);
bell.draw();
}
Breathing — Vertical Only
let bell, tallSin;
function setup() {
createCanvas(640, 480);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
const fillC = new SWColor(240, 55, 97, 100, "f");
bell = new SWBellCurve(width / 2, height / 2, 200, 70, fillC);
tallSin = new SWSinusoid(3.0, 0.6, 1.4); // period 3s, min 0.6×, max 1.4×
}
function draw() {
background(240, 20, 95);
const t = millis() / 1000;
bell.breatheVertical(tallSin, t);
bell.draw();
}
Breathing — Horizontal Only
let bell, wideSin;
function setup() {
createCanvas(640, 480);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
const fillC = new SWColor(240, 55, 97, 100, "f");
bell = new SWBellCurve(width / 2, height / 2, 200, 70, fillC);
wideSin = new SWSinusoid(4.0, 0.5, 1.5); // period 4s, min 0.5×, max 1.5×
}
function draw() {
background(240, 20, 95);
const t = millis() / 1000;
bell.breatheHorizontal(wideSin, t);
bell.draw();
}
Combined Transform (Breathe + Rotate)
let bell, pulse;
function setup() {
createCanvas(640, 480);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
const fillC = new SWColor(240, 55, 97, 100, "f");
bell = new SWBellCurve(width / 2, height / 2, 200, 70, fillC);
pulse = new SWSinusoid(2.5, 0.8, 1.2);
}
function draw() {
background(240, 20, 95);
const t = millis() / 1000;
bell.transform({ sinusoid: pulse, degPerSec: 20, t });
bell.draw();
}
Source Code
Show / Hide Source Code
/*
File: swBellCurve.js
Date: 2026-05-18
Author: klp
Workspace: SketchWaveTNT2026-05-01-Stg9
Purpose: SWBellCurve — draws a bell-shaped (Gaussian) curve as a closed filled
shape: the Gaussian arc on top and a flat baseline on the bottom.
Positioning:
cx, cy = centre of the bounding box.
Peak is amplitude/2 ABOVE the centre line (screen y = cy − amplitude/2).
Baseline is amplitude/2 BELOW the centre line (screen y = cy + amplitude/2).
Local-coordinate formula (origin at bounding-box centre, y down):
y = halfAmp − amp × exp(−x² / (2σ²))
• At x = 0: y = −halfAmp (peak, at top)
• At x = ±range: y → halfAmp (baseline, at bottom)
Key formulas:
FWHM = 2σ √(2 ln 2) ≈ 2.355 σ (full width at half maximum)
Area ≈ amp × σ × √(2π) (infinite-integral approximation)
Breathing modes:
breathe() — scales both amplitude AND sigma uniformly
breatheVertical() — scales amplitude only (taller / shorter)
breatheHorizontal() — scales sigma only (wider / narrower)
Dependencies: p5.js, swColor.js, swSinusoid.js, swGrid.js
Notes: assumes p5.js colorMode(HSB, 360, 100, 100, 100)
*/
console.log("[swBellCurve.js] SWBellCurve class loaded.");
class SWBellCurve {
constructor(cx, cy, amplitude, sigma, fillColor, options = {}) {
this.cx = cx;
this.cy = cy;
this.amplitude = Math.max(1, amplitude);
this.sigma = Math.max(1, sigma);
this.originalAmplitude = this.amplitude;
this.originalSigma = this.sigma;
this.fillColor = fillColor ? SWColor.copy(fillColor) : null;
this.strokeColor = options.strokeColor ? SWColor.copy(options.strokeColor) : null;
this.strokeWeight = options.strokeWeight ?? 2;
this.drawRangeFactor = Math.max(1, options.drawRangeFactor ?? 3.5);
this.resolution = Math.max(20, options.resolution ?? 120);
this._rotAngle = 0;
this._currentAmplitude = this.amplitude;
this._currentSigma = this.sigma;
}
get fwhm() { return 2 * this._currentSigma * Math.sqrt(2 * Math.LN2); }
get totalWidth() { return 2 * this._currentSigma * this.drawRangeFactor; }
get area() { return this._currentAmplitude * this._currentSigma * Math.sqrt(2 * Math.PI); }
draw() {
this._drawAtPx(this.cx, this.cy, this._currentAmplitude, this._currentSigma);
}
drawOnGrid(grid) {
const { x: sx, y: sy } = grid.userToScreen(this.cx, this.cy);
const sAmp = this._currentAmplitude * Math.abs(grid.yScale);
const sSig = this._currentSigma * grid.xScale;
this._drawAtPx(sx, sy, sAmp, sSig);
}
_drawAtPx(cx, cy, amp, sig) {
if (amp <= 0 || sig <= 0) return;
push();
if (this.fillColor) { fill(this.fillColor.col); } else { noFill(); }
if (this.strokeColor) {
stroke(this.strokeColor.col);
strokeWeight(this.strokeWeight);
} else { noStroke(); }
translate(cx, cy);
rotate(radians(this._rotAngle));
const range = sig * this.drawRangeFactor;
const n = this.resolution;
const step = (2 * range) / n;
const halfAmp = amp / 2;
const twoSig2 = 2 * sig * sig;
beginShape();
vertex(-range, halfAmp);
for (let i = 0; i <= n; i++) {
const x = -range + i * step;
const y = halfAmp - amp * Math.exp(-(x * x) / twoSig2);
vertex(x, y);
}
vertex(range, halfAmp);
endShape(CLOSE);
pop();
}
breathe(sinusoid, t) {
const s = sinusoid.getValue(t);
this._currentAmplitude = this.originalAmplitude * s;
this._currentSigma = this.originalSigma * s;
}
breatheVertical(sinusoid, t) {
this._currentAmplitude = this.originalAmplitude * sinusoid.getValue(t);
}
breatheHorizontal(sinusoid, t) {
this._currentSigma = this.originalSigma * sinusoid.getValue(t);
}
rotate(degPerSec, t) { this._rotAngle = degPerSec * t; }
transform({ sinusoid = null, sinusoidV = null, sinusoidH = null,
t = 0, degPerSec = null } = {}) {
if (sinusoid !== null) this.breathe(sinusoid, t);
if (sinusoidV !== null) this.breatheVertical(sinusoidV, t);
if (sinusoidH !== null) this.breatheHorizontal(sinusoidH, t);
if (degPerSec !== null) this.rotate(degPerSec, t);
}
reset() {
this._rotAngle = 0;
this._currentAmplitude = this.originalAmplitude;
this._currentSigma = this.originalSigma;
}
setCenter(cx, cy) { this.cx = cx; this.cy = cy; }
setAmplitude(a) { this.amplitude = this.originalAmplitude = this._currentAmplitude = Math.max(1, a); }
setSigma(s) { this.sigma = this.originalSigma = this._currentSigma = Math.max(1, s); }
setFillColor(c) { this.fillColor = c ? SWColor.copy(c) : null; }
setStrokeColor(c) { this.strokeColor = c ? SWColor.copy(c) : null; }
setStrokeWeight(w) { this.strokeWeight = Math.max(0, w); }
setDrawRangeFactor(f) { this.drawRangeFactor = Math.max(1, f); }
toString() {
return `SWBellCurve(cx:${this.cx.toFixed(1)}, cy:${this.cy.toFixed(1)}, ` +
`amp:${this._currentAmplitude.toFixed(1)}, sigma:${this._currentSigma.toFixed(1)})`;
}
}//end class SWBellCurve