∿ SketchWave Bell Curve Class Reference

Standalone Core Class • Gaussian Bell Curve • Three Breathing Modes • FWHM & Area Getters

▶ Try SWBellCurve Demo

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.

Quick-Reference

Positioning

cx, cy = bounding-box centre. Peak is amplitude/2 above; baseline is amplitude/2 below.

Breathing

breatheVertical(), breatheHorizontal(), and breathe() (both axes) each take a SWSinusoid.

Rotation

rotate(degPerSec, t) spins about the bounding-box centre; transform() combines breathe + rotate.

Statistics

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:

f(x) = A × e−x² / (2σ²)

Where:

  • Aamplitude (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).

cy baseline peak cx amp/2 amp/2

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 cy in 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:

ylocal = amplitude/2 − amplitude × e−x² / (2σ²)

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.

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

ParameterTypeDescription
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

KeyTypeDefaultDescription
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.

FWHM = 2σ × √(2 ln 2) ≈ 2.355 σ

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.

Area ≈ amplitude × σ × √(2π) ≈ 2.507 × amplitude × σ

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:

KeyTypeEffect
sinusoidSWSinusoidUniform breathe (both axes)
sinusoidVSWSinusoidVertical breathe (amplitude only)
sinusoidHSWSinusoidHorizontal breathe (sigma only)
tnumberTime in seconds
degPerSecnumberRotation 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.

MethodParameterWhat it sets
setCenter(cx, cy)numbersMove the bounding-box centre
setAmplitude(a)numberNew amplitude; clamped ≥ 1. Updates originalAmplitude.
setSigma(s)numberNew sigma; clamped ≥ 1. Updates originalSigma.
setFillColor(c)SWColor | nullFill colour; null removes fill
setStrokeColor(c)SWColor | nullStroke colour; null removes stroke
setStrokeWeight(w)numberStroke width; clamped ≥ 0
setDrawRangeFactor(f)numberDrawing 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