Quick Reference
SWCloud is a SketchWave composite class that builds a cumulus-style cloud shape from N overlapping SWEllipse puffs. Each puff is a horizontally-elongated ellipse placed at a random offset around a shared center. The offsets (dx, dy, size) are stored independently so spread sliders rescale the cloud without losing its random shape. SWCloud owns no visible anchor point of its own — all drawing is delegated to its internal _puffs[] array.
- Design Pattern: Composite — owns an array of SWEllipse puffs
- Internal Structure:
_offsets[](random dx/dy/size per puff) +_puffs[](SWEllipse array) - Dependencies: SWPoint, SWColor, SWSinusoid, SWEllipse, SWGrid, p5.js
- Key Features: Random puff layout, spread/spreadV scatter sliders, breathe animation with stagger, horizontal drift + canvas wrap, Perlin-noise morph per puff, showCenters toggle, wrapGap off-canvas overshoot, full reset
- Getters:
width,height— approximate bounding box in user units - Common Uses: Sky scenes, weather graphics, cumulus cloud shapes, organic animated backgrounds, character art
Overview
The SWCloud class represents a cumulus cloud shape. The cloud is defined by a center SWPoint and a set of options that control puff shape, scatter, and animation. Each puff is an SWEllipse placed at a random normalized offset (dx, dy) around the center, scaled by spread × puffWidth horizontally and spreadV × puffHeight vertically. Each puff also gets a random size scale (0.70–1.00 of the base puff dimensions).
The random
dx, dy, and size for each puff are computed once and stored in _offsets[]. Sliders like Spread H and Puff Width rescale the existing offsets, preserving the cloud's random character. Call randomize() (or press z) to generate a completely new layout.
Most setters call
_buildPuffs(), which creates a new SWEllipse array from the current offsets. Color and stroke setters iterate the existing _puffs[] and update properties in-place (no rebuild). Drift and noise morph update puff positions every frame in-place via update(), avoiding 30 fps object creation.
Puff Layout
In user coordinates, puff i is placed at:
px = center.x + offsets[i].dx * spread * puffWidth
py = center.y + offsets[i].dy * spreadV * puffHeight
rX = puffWidth * offsets[i].size
rY = puffHeight * offsets[i].size
Setting spread = 0 stacks all puffs at the center. Increasing it spreads them outward without changing their relative arrangement.
Breathe Animation
Calling breathe(sinX, sinY, t) delegates to each puff with a phase offset of i × breatheStagger seconds. At breatheStagger = 0 all puffs pulse together; with stagger > 0 adjacent puffs breathe slightly out of phase, producing a rolling, organic billowing motion.
Drift & Noise Morph
Call update(t, deltaT, …) once per frame when drift or noise morph is active. Drift advances center.x each frame and wraps the cloud when it travels past the canvas edge plus wrapGap user units. On each wrap, a small random vertical nudge shifts center.y so each pass crosses the sky at a slightly different altitude. Noise Morph adds an independent Perlin-noise displacement to each puff's position every frame, making the cloud shape shift slowly and organically.
Key Capabilities
- N-puff Composite: 1–9 SWEllipse puffs drawn as a single cloud shape
- Random Layout: Every cloud looks different;
randomize()generates a fresh arrangement without changing any parameters - Spread Control: Independent horizontal (
spread) and vertical (spreadV) scatter magnitudes - Breathe: Staggered sinusoidal puff-size animation; configure period, min/max scale, and stagger offset
- Drift: Horizontal motion with canvas wrap and per-pass vertical nudge
- Noise Morph: Per-puff Perlin-noise displacement — shape shifts continuously
- Show Centers:
setShowCenters(true)reveals each puff's SWPoint center dot for layout inspection - Drag: Move the cloud interactively by dragging the center crosshair in the demo
- Full Reset:
reset()restores the exact original cloud — same random offsets, same center, same parameters
Typical Workflow
- Create fill and stroke
SWColorinstances - Construct an
SWCloudwith a centerSWPointand an options object - Draw each frame using
cloud.drawOnGrid(grid) - If breathe is active, call
cloud.breathe(sinX, sinY, t)after drawing - If drift or noise morph is active, call
cloud.update(t, deltaT, …)before drawing - Call
cloud.reset()to restore all original values and the original random shape
Constructor
new SWCloud(center, options)Creates a new SWCloud instance. Random puff offsets are generated at construction and deep-copied into originalOffsets[] so reset() can restore the exact original shape. All numeric option values are also preserved as originalXxx properties for reset.
| Parameter / Option | Type | Default | Description |
|---|---|---|---|
center |
SWPoint | required | Anchor of the cloud in user (grid) coordinates. All puffs are placed relative to this point. |
options.puffCount |
number | 5 | Number of SWEllipse puffs. Clamped to ≥ 1. Changing the count generates new random offsets for all puffs. |
options.puffWidth |
number | 1.8 | Base horizontal radius (radiusX) of each puff in user units. puffWidth > puffHeight produces the typical horizontally-elongated cloud shape. |
options.puffHeight |
number | 1.0 | Base vertical radius (radiusY) of each puff in user units. |
options.spread |
number | 1.0 | Horizontal scatter scale. 0 = all puffs stacked at center; higher values stretch the cloud wider. |
options.spreadV |
number | 0.6 | Vertical scatter scale. Increase for a taller cloud; decrease for a flat stratus band. |
options.fillColor |
SWColor | undefined | undefined | Fill color applied to all puffs. undefined = no fill. |
options.strokeColor |
SWColor | undefined | undefined | Border color applied to all puffs. undefined = no border. |
options.strokeWeight |
number | 1 | Border thickness in pixels applied to all puffs. |
options.showCenters |
boolean | false | Whether to display a SWPoint dot at each puff's center. Useful for layout inspection. |
options.breatheStagger |
number | 0.3 | Phase offset in seconds between adjacent puff breaths. 0 = all puffs pulse simultaneously. |
options.driftSpeed |
number | 0 | Horizontal drift speed in user units/second. Positive = rightward; negative = leftward. 0 = no drift. |
options.noiseAmp |
number | 0.3 | Perlin-noise morph amplitude in user units. 0 = no displacement; larger values make puffs wander more. |
options.noiseSpeed |
number | 0.3 | How fast the Perlin-noise pattern evolves. Low = slow organic shift; high = rapid turbulent morphing. |
options.wrapGap |
number | 2.0 | How far (user units) past the canvas edge the cloud travels before reappearing on the opposite side. 0 = instant re-entry. |
// Default sky-blue cloud centered at origin
const center = new SWPoint(0, 0);
const fill = new SWColor(200, 12, 98, 90, "cloudFill");
const stroke = new SWColor(230, 30, 60, 100, "cloudStroke");
let cloud = new SWCloud(center, {
puffCount: 5,
puffWidth: 1.8,
puffHeight: 1.0,
fillColor: fill,
strokeColor: stroke,
});
// White storm cloud — more puffs, taller, more spread
let stormCloud = new SWCloud(new SWPoint(-3, 2), {
puffCount: 8,
puffWidth: 2.0,
puffHeight: 1.5,
spread: 1.4,
spreadV: 0.9,
fillColor: new SWColor(0, 0, 85, 95, "stormFill"),
strokeColor: new SWColor(0, 0, 40, 100, "darkBorder"),
strokeWeight: 2,
});
// Drifting cloud — moves right, Perlin noise morph active
let driftCloud = new SWCloud(new SWPoint(-10, 1), {
puffCount: 5,
puffWidth: 2.0,
puffHeight: 1.0,
fillColor: fill,
driftSpeed: 2.0, // 2 user units/second rightward
noiseAmp: 0.3,
noiseSpeed: 0.25,
wrapGap: 2.0,
});
Properties
center SWPointThe cloud's anchor point in user (grid) coordinates. All puffs are positioned relative to this. Moving center.x / center.y directly does not rebuild puffs — use setCenter(x, y) to reposition and rebuild, or the demo's drag interaction to move the cloud interactively.
cloud.setCenter(2, 1); // move anchor and rebuild puffs
puffCount numberNumber of SWEllipse puffs. Clamped to ≥ 1 by setPuffCount(). Changing the count calls _generateOffsets() to create a fresh set of random offsets for the new count, then rebuilds all puffs.
cloud.setPuffCount(7); // new random offsets + rebuild
puffWidth number / puffHeight numberpuffWidth is the base horizontal radius (radiusX) of each puff; puffHeight is the base vertical radius (radiusY). Both are in user units. Each puff's actual radii are puffWidth × offset.size and puffHeight × offset.size, where offset.size is in [0.70, 1.00]. Setting either calls _buildPuffs().
cloud.setPuffWidth(2.5); // wider puffs
cloud.setPuffHeight(1.4); // taller puffs
spread number / spreadV numberspread scales how far puffs scatter horizontally; spreadV scales vertical scatter. Both multiply the normalized offset values stored in _offsets[]. Setting 0 collapses all puffs to the center; higher values open the cloud up without changing the random arrangement.
cloud.setSpread(1.5); // wider scatter
cloud.setSpreadV(0.3); // flatter, more stratus-like
fillColor SWColor | undefined / strokeColor SWColor | undefinedFill and border color applied to all puffs. undefined = no fill or no border. Setting either iterates _puffs[] and updates each puff's color in-place (no rebuild). Deep-copied at construction for reset().
cloud.setFillColor(new SWColor(200, 15, 98, 80, "skyFill"));
cloud.setStrokeColor(undefined); // no border
strokeWeight numberBorder thickness in pixels applied to all puffs. Use setStrokeWeight() to change; iterates _puffs[] in-place.
cloud.setStrokeWeight(2); // thicker border
showCenters booleanWhether each puff's SWPoint center dot is visible. Default false. Use setShowCenters() to toggle. Useful for understanding puff placement during development.
cloud.setShowCenters(true); // reveal puff dots
cloud.setShowCenters(false); // hide them again
breatheStagger numberPhase offset in seconds between adjacent puff breaths. At 0, all puffs pulse simultaneously (uniform inflation). Increasing stagger creates a rolling, wave-like billowing effect across the cloud.
cloud.setBreatheStagger(0.5); // more dramatic wave effect
driftSpeed numberHorizontal drift speed in user units/second. Positive = rightward; negative = leftward; 0 = stationary. Applied each frame by update() when drift is active. Use setDriftSpeed() to change.
cloud.setDriftSpeed(1.5); // moderate rightward drift
cloud.setDriftSpeed(-2.0); // leftward drift
noiseAmp number / noiseSpeed numbernoiseAmp controls the maximum Perlin-noise displacement of each puff from its base position (in user units). noiseSpeed controls how fast the noise pattern evolves. Small amp + slow speed = subtle shimmer; large amp + fast speed = turbulent morphing.
cloud.setNoiseAmp(0.6); // larger puff wander
cloud.setNoiseSpeed(0.5); // faster morphing
wrapGap numberHow far (user units) past the canvas edge the cloud travels before reappearing on the opposite side. 0 = the cloud re-enters as soon as its leading edge clears the boundary; 2.0 = the cloud travels 2 more user units off-canvas before wrapping. Allows the full cloud to visually exit before returning.
cloud.setWrapGap(3.0); // longer off-canvas pause before wrap
width getter — number / height getter — numberRead-only approximate bounding-box dimensions in user units. width is computed from the leftmost and rightmost puff edges; height from the bottom and top puff edges. Useful for positioning logic, snap calculations, and HUD readouts.
console.log(`${cloud.width.toFixed(1)} × ${cloud.height.toFixed(1)} user units`);
originalCenter / originalPuffCount / originalPuffWidth / … / originalOffsets[] various restore targetsSnapshots captured at construction. reset() uses all of these to restore the cloud to its exact initial state — same dimensions, same colors, same random offset arrangement. originalOffsets[] is a deep copy of the random offsets generated at construction. originalFillColor and originalStrokeColor are deep-copied SWColor instances. Read-only; used internally by reset().
// Read-only; used internally by reset()
_puffs[] SWEllipse[] internalThe internal array of SWEllipse puffs. Rebuilt by _buildPuffs() whenever shape or layout properties change. Avoid direct access; use the public setter methods instead.
_offsets[] {dx, dy, size}[] internalNormalized random offsets for each puff: dx and dy each in [−1, +1]; size in [0.70, 1.00]. Regenerated by setPuffCount() and randomize(); restored from originalOffsets[] by reset(). Avoid direct access.
Methods
Core Drawing Methods
drawOnGrid(grid)Draws all puffs through the given SWGrid's coordinate system. Converts puff centers and radii from user units to screen pixels via grid.userToScreen(). This is the standard method to call in a p5.js draw() loop.
grid(SWGrid) — the coordinate grid
void
function draw() {
background(bgColor);
grid.draw();
cloud.drawOnGrid(grid);
}
draw()Draws all puffs in raw screen (pixel) coordinates. center.x / center.y and radii are treated as pixel values. Rarely used directly — prefer drawOnGrid() for standard canvas rendering.
void
Breathe Animation
breathe(sinusoidX, sinusoidY, t)Delegates to each puff's breathe() with a phase offset of i × breatheStagger seconds. The sinusoid value at time t + i × stagger drives the scale of puff i's radiusX and/or radiusY. Call after drawOnGrid().
sinusoidX(SWSinusoid | null) — controls radiusX oscillation; configure withadjustWaveUsingExtrema(min, max)sinusoidY(SWSinusoid | null) — controls radiusY oscillation; can be the same sinusoid as X for uniform breathingt(number) — elapsed time in seconds
// Set up sinusoid to scale between 0.70× and 1.30× original size
const breatheSin = SWSinusoid.copy(UNIT_SINUSOID);
breatheSin.setPeriod(3.0);
breatheSin.adjustWaveUsingExtrema(0.70, 1.30);
// In draw():
cloud.drawOnGrid(grid);
cloud.breathe(breatheSin, breatheSin, elapsedSeconds); // call AFTER draw
Drift & Noise Morph Animation
update(t, deltaT, wrapLeft, wrapRight, doDrift, doNoise, wrapTop, wrapBottom)Advances drift and/or Perlin-noise morphing for one frame. Call once per frame from draw() when either animation is active. Updates puff center positions in-place — does not rebuild SWEllipse objects.
Drift: advances center.x by driftSpeed × deltaT. When the cloud travels more than wrapGap user units past a wrap boundary, it is repositioned on the opposite side and its y-position is nudged by a random offset (±1.5 × puffHeight), clamped to keep the cloud fully on-canvas.
Noise Morph: displaces each puff's center from its base position using independent Perlin-noise channels (noise seed based on puff index and time). Displacement is scaled by noiseAmp.
| Parameter | Type | Default | Description |
|---|---|---|---|
t | number | required | Elapsed time in seconds (used for Perlin noise time axis) |
deltaT | number | required | Seconds since last frame (used for drift step = driftSpeed × deltaT) |
wrapLeft | number | −15 | Left wrap boundary in user units (typically grid.UL.x) |
wrapRight | number | 15 | Right wrap boundary in user units (typically grid.LR.x) |
doDrift | boolean | true | Whether to advance horizontal drift this frame |
doNoise | boolean | true | Whether to apply Perlin-noise morph this frame |
wrapTop | number | 10 | Top canvas bound in user units (typically grid.UL.y) — used to clamp the vertical nudge on wrap |
wrapBottom | number | −10 | Bottom canvas bound in user units (typically grid.LR.y) — used to clamp the vertical nudge on wrap |
void
// In draw() — call before drawOnGrid when either animation is active
if (shouldDrift || shouldNoiseMorph) {
cloud.update(t, deltaT,
grid.UL.x, grid.LR.x, // wrap boundaries
shouldDrift, shouldNoiseMorph,
grid.UL.y, grid.LR.y); // top/bottom for nudge clamp
}
cloud.drawOnGrid(grid);
Layout Methods
randomize()Generates a completely new set of random puff offsets and rebuilds all puffs. All other parameters (puffCount, puffWidth, spread, colors, etc.) remain unchanged. The new offsets are not saved as originals — calling reset() after randomize() will still restore the original construction-time offsets.
void
cloud.randomize(); // new cloud layout, same parameters
reset()Restores all original construction values — center position, puffCount, puffWidth, puffHeight, spread, spreadV, driftSpeed, noiseAmp, noiseSpeed, wrapGap, fillColor, strokeColor, strokeWeight — and rebuilds puffs using the original random offsets. The cloud returns to its exact initial appearance, including the same random arrangement.
void
cloud.reset(); // full restore — same shape, same center, same parameters
Setter Methods
setCenter(x, y)Repositions the cloud anchor and rebuilds all puffs around the new center.
x,y(number) — new center position in user coordinates
cloud.setCenter(3, -1); // move cloud and rebuild puffs
setPuffCount(n)Changes the number of puffs. Clamps to ≥ 1. Generates new random offsets for the new count and rebuilds. Useful when exploring how cloud density affects the shape.
n(number) — new puff count; integer; clamped to ≥ 1
cloud.setPuffCount(8); // expand to 8 puffs with new layout
setPuffWidth(w) / setPuffHeight(h)Change the base puff radiusX or radiusY and rebuild. The existing random offsets are preserved — only the scale changes.
cloud.setPuffWidth(2.2); // wider puffs
cloud.setPuffHeight(0.8); // flatter puffs
setSpread(s) / setSpreadV(v)Change horizontal or vertical scatter magnitude and rebuild. 0 collapses all puffs to the center; 1 is the default; higher values spread the cloud out more.
cloud.setSpread(0); // all puffs stacked at center
cloud.setSpreadV(0.8); // taller arrangement
setFillColor(col) / setStrokeColor(col)Update fill or border color on all puffs without rebuilding. Pass an SWColor to set a new color; pass undefined to remove fill or stroke.
cloud.setFillColor(new SWColor(0, 0, 95, 100, "white"));
cloud.setStrokeColor(undefined); // remove border
setStrokeWeight(w)Update border thickness in pixels on all puffs without rebuilding.
cloud.setStrokeWeight(3);
setShowCenters(show)Toggle the SWPoint center dot at each puff's position. Useful for inspecting the random layout during development or teaching.
show(boolean) — true to reveal dots; false to hide
cloud.setShowCenters(true); // show puff center dots
cloud.setShowCenters(false); // hide them
setBreatheStagger(s)Update the phase offset (seconds) between adjacent puff breaths. Higher values produce more dramatic wave-like motion across the cloud.
cloud.setBreatheStagger(0.6); // wider phase spread
setDriftSpeed(v) / setNoiseAmp(v) / setNoiseSpeed(v) / setWrapGap(v)Update the drift and noise morph parameters without rebuilding puffs. Changes take effect on the next update() call.
cloud.setDriftSpeed(3.0); // faster drift
cloud.setNoiseAmp(0.5); // larger puff wander
cloud.setNoiseSpeed(0.4); // faster noise evolution
cloud.setWrapGap(4.0); // cloud exits fully before re-entering
Complete Usage Example
Load the dependencies in the correct order:
<script src="shapeClasses/swSinusoid.js"></script>
<script src="shapeClasses/swColor.js"></script>
<script src="shapeClasses/swPoint.js"></script>
<script src="shapeClasses/swGrid.js"></script>
<script src="shapeClasses/swEllipse.js"></script> <!-- SWCloud depends on SWEllipse -->
<script src="shapeClasses/swCloud.js"></script>
Minimal sketch — a stationary white cloud on a sky-blue background:
let grid, cloud;
function setup() {
createCanvas(500, 400);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
const fill = new SWColor(200, 12, 98, 90, "cloudFill");
const stroke = new SWColor(230, 30, 60, 100, "cloudStroke");
cloud = new SWCloud(new SWPoint(0, 0), {
puffCount: 5,
puffWidth: 1.8,
puffHeight: 1.0,
fillColor: fill,
strokeColor: stroke,
});
frameRate(30);
}
function draw() {
background(200, 55, 88); // sky blue
grid.draw();
cloud.drawOnGrid(grid);
}
Animated cloud — breathe + drift + noise morph:
let grid, cloud, breatheSin;
let prevT = 0;
function setup() {
createCanvas(600, 400);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
const fill = new SWColor(200, 12, 98, 90, "cloudFill");
cloud = new SWCloud(new SWPoint(-5, 1), {
puffCount: 6,
puffWidth: 2.0,
puffHeight: 1.0,
spread: 1.0,
spreadV: 0.6,
fillColor: fill,
driftSpeed: 1.5,
noiseAmp: 0.3,
noiseSpeed: 0.25,
wrapGap: 2.0,
breatheStagger: 0.3,
});
breatheSin = SWSinusoid.copy(UNIT_SINUSOID);
breatheSin.setPeriod(3.0);
breatheSin.adjustWaveUsingExtrema(0.70, 1.30);
frameRate(30);
}
function draw() {
const t = millis() / 1000;
const deltaT = prevT > 0 ? t - prevT : 0;
prevT = t;
background(200, 55, 88); // sky blue
grid.draw();
// Drift + Noise Morph — call before drawing
cloud.update(t, deltaT,
grid.UL.x, grid.LR.x,
true, true, // drift + noise
grid.UL.y, grid.LR.y);
cloud.drawOnGrid(grid);
// Breathe — call after drawing
cloud.breathe(breatheSin, breatheSin, t);
}
Source Code
Show / Hide swCloud.js source
/*
File: swCloud.js
Date: 2026-05-08
Author: klp + GitHub Copilot
Workspace: SketchWaveTNT2026-05-01-Stg9
Purpose: SWCloud class for SketchWaveJS
SWCloud represents a cumulus-style cloud shape built from N overlapping SWEllipse "puffs".
Each puff is a horizontally-elongated ellipse. Puffs are placed at random normalized offsets
(dx, dy each in −1..+1) around the cloud center, scaled by spreadH × puffWidth and
spreadV × puffHeight. Each puff also gets a random size scale (0.70–1.00).
The offsets are stored so sliders can rescale the cloud without changing its random shape.
Animations:
breathe(sinX, sinY, t) — each puff breathes independently with a configurable
phase stagger (seconds) between adjacent puffs.
update(t, deltaT, ...) — advances horizontal drift (with canvas wrap-around) and
applies per-puff Perlin-noise morphing so the cloud shape
shifts organically over time.
Constructor:
new SWCloud(center, options)
center : SWPoint — anchor of the cloud
options :
puffCount : number — number of ellipse puffs (default 5; min 1)
puffWidth : number — base horizontal radius of puffs in user units (default 1.8)
puffHeight : number — base vertical radius of puffs in user units (default 1.0)
spread : number — horizontal scatter scale; 0 = all puffs at center (default 1.0)
spreadV : number — vertical scatter scale (default 0.6)
fillColor : SWColor — fill color (default: undefined = no fill)
strokeColor : SWColor — border color (default: undefined = no border)
strokeWeight : number — border thickness (default 1)
showCenters : boolean — show SWPoint dots at each puff center (default false)
breatheStagger: number — phase offset in seconds between adjacent puff breaths (default 0.3)
driftSpeed : number — horizontal drift speed in user units/sec; + = rightward (default 0)
noiseAmp : number — Perlin-noise morph amplitude in user units (default 0.3)
noiseSpeed : number — Perlin-noise time scale (higher = faster morphing) (default 0.3)
Key methods:
drawOnGrid(grid) — draw all puffs in user/grid coordinates
draw() — draw all puffs in screen (pixel) coordinates
breathe(sinX, sinY, t) — animate puff sizes with staggered phase
update(t, deltaT, ...) — advance drift and/or apply per-puff Perlin noise morph
randomize() — generate a new set of random puff offsets and rebuild
reset() — restore all factory-default properties and rebuild puffs
setShowCenters(bool) — toggle SWPoint center dots on all puffs
setPuffCount(n) — change puff count and rebuild (new random offsets)
setPuffWidth(w) — change base puff radiusX and rebuild
setPuffHeight(h) — change base puff radiusY and rebuild
setSpread(s) — change horizontal scatter scale and rebuild
setSpreadV(v) — change vertical scatter scale and rebuild
setCenter(x, y) — reposition cloud anchor and rebuild
setFillColor(col) — update fill color on all puffs (no rebuild)
setStrokeColor(col) — update border color on all puffs (no rebuild)
setStrokeWeight(w) — update border weight on all puffs (no rebuild)
setBreatheStagger(s) — update phase stagger between puffs (no rebuild)
setDriftSpeed(v) — update horizontal drift speed (no rebuild)
setNoiseAmp(v) — update Perlin-noise morph amplitude (no rebuild)
setNoiseSpeed(v) — update Perlin-noise time scale (no rebuild)
Getters:
width — approximate total width of the cloud in user coords
height — approximate total height of the cloud in user coords
Dependencies: p5.js, SWColor, SWPoint, SWGrid, SWEllipse
*/
console.log("[swCloud.js] SWCloud class loaded.");
class SWCloud {
/**
* @param {SWPoint} center - Anchor point (baseline of cloud center)
* @param {Object} [options={}]
* puffCount : number — number of ellipse puffs (default 5)
* puffWidth : number — base radiusX of each puff (user units, default 1.8)
* puffHeight : number — base radiusY of each puff (user units, default 1.0)
* spread : number — spacing multiplier (default 1.0)
* fillColor : SWColor — fill color
* strokeColor : SWColor — border color
* strokeWeight : number — border thickness (default 1)
* showCenters : boolean — show SWPoint dots at puff centers (default false)
* breatheStagger: number — phase offset (seconds) between adjacent puff breaths (default 0.3)
* driftSpeed : number — horizontal drift speed (user units/sec); + = rightward (default 0)
* noiseAmp : number — Perlin-noise morph amplitude in user units (default 0.3)
* noiseSpeed : number — Perlin-noise time scale (higher = faster morphing) (default 0.3)
*/
constructor(center, options = {}) {
this.center = center;
this.puffCount = options.puffCount !== undefined ? Math.max(1, Math.round(options.puffCount)) : 5;
this.puffWidth = options.puffWidth !== undefined ? options.puffWidth : 1.8;
this.puffHeight = options.puffHeight !== undefined ? options.puffHeight : 1.0;
this.spread = options.spread !== undefined ? options.spread : 1.0;
this.spreadV = options.spreadV !== undefined ? options.spreadV : 0.6;
this.fillColor = options.fillColor ? SWColor.copy(options.fillColor) : undefined;
this.strokeColor = options.strokeColor ? SWColor.copy(options.strokeColor) : undefined;
this.strokeWeight = options.strokeWeight !== undefined ? options.strokeWeight : 1;
this.showCenters = options.showCenters !== undefined ? options.showCenters : false;
this.breatheStagger = options.breatheStagger !== undefined ? options.breatheStagger : 0.3;
this.driftSpeed = options.driftSpeed !== undefined ? options.driftSpeed : 0;
this.noiseAmp = options.noiseAmp !== undefined ? options.noiseAmp : 0.3;
this.noiseSpeed = options.noiseSpeed !== undefined ? options.noiseSpeed : 0.3;
this.wrapGap = options.wrapGap !== undefined ? options.wrapGap : 2.0;
// Store originals for reset()
this.originalCenter = new SWPoint(center.x, center.y);
this.originalPuffCount = this.puffCount;
this.originalPuffWidth = this.puffWidth;
this.originalPuffHeight = this.puffHeight;
this.originalSpread = this.spread;
this.originalSpreadV = this.spreadV;
this.originalDriftSpeed = this.driftSpeed;
this.originalNoiseAmp = this.noiseAmp;
this.originalNoiseSpeed = this.noiseSpeed;
this.originalWrapGap = this.wrapGap;
// Generate initial random offsets and store them for reset()
this._offsets = [];
this._generateOffsets(this.puffCount);
this.originalOffsets = this._offsets.map(o => ({ dx: o.dx, dy: o.dy, size: o.size }));
this._puffs = [];
this._buildPuffs();
}//end constructor
// —— Internal: puff layout ———————————————————————
/**
* Builds the _puffs array from current properties.
* Called by the constructor and by all setters that affect shape or layout.
*/
_buildPuffs() {
this._puffs = [];
const N = this.puffCount;
// Ensure random offsets exist and match current puff count
if (!this._offsets || this._offsets.length !== N) {
this._generateOffsets(N);
}
for (let i = 0; i < N; i++) {
const off = this._offsets[i];
// Puff center — random offset scaled by spread magnitudes and puff dimensions
const px = this.center.x + off.dx * this.spread * this.puffWidth;
const py = this.center.y + off.dy * this.spreadV * this.puffHeight;
// Per-puff size variation stored in the offset
const rX = this.puffWidth * off.size;
const rY = this.puffHeight * off.size;
const puffCenter = new SWPoint(px, py);
const puff = new SWEllipse(
puffCenter, rX, rY,
this.fillColor ? SWColor.copy(this.fillColor) : undefined,
{
strokeColor: this.strokeColor ? SWColor.copy(this.strokeColor) : undefined,
strokeWeight: this.strokeWeight,
showCenter: this.showCenters,
}
);
this._puffs.push(puff);
}
}//end _buildPuffs
/**
* Generates N random normalized offsets for puff placement.
* Each offset has dx/dy in −1..+1 (horizontal/vertical scatter) and
* a random size scale in 0.70..1.00.
* When N=1 the single puff is always placed at the center (dx=dy=0).
* @param {number} n
*/
_generateOffsets(n) {
this._offsets = [];
if (n === 1) {
// Single puff always centered regardless of spread
this._offsets.push({ dx: 0, dy: 0, size: 1.0 });
return;
}
for (let i = 0; i < n; i++) {
this._offsets.push({
dx: Math.random() * 2 - 1, // −1 .. +1 horizontal scatter
dy: Math.random() * 2 - 1, // −1 .. +1 vertical scatter
size: 0.70 + Math.random() * 0.30, // 0.70 .. 1.00 size variation
});
}
}//end _generateOffsets
// —— Getters ————————————————————————————————————
/**
* Approximate total width of the cloud in user coords
* (from left edge of leftmost puff to right edge of rightmost puff).
*/
get width() {
if (this._puffs.length === 0) return 0;
let minX = Infinity, maxX = -Infinity;
for (const p of this._puffs) {
minX = Math.min(minX, p.center.x - p.radiusX);
maxX = Math.max(maxX, p.center.x + p.radiusX);
}
return maxX - minX;
}//end width
/**
* Approximate total height of the cloud in user coords
* (from bottom edge of edge puffs to top edge of center puff).
*/
get height() {
if (this._puffs.length === 0) return 0;
let minY = Infinity, maxY = -Infinity;
for (const p of this._puffs) {
minY = Math.min(minY, p.center.y - p.radiusY);
maxY = Math.max(maxY, p.center.y + p.radiusY);
}
return maxY - minY;
}//end height
// —— Setters ————————————————————————————————————
/** Show or hide the SWPoint dot at each puff center. */
setShowCenters(show = true) {
this.showCenters = show;
for (const p of this._puffs) p.setShowCenter(show);
}//end setShowCenters
/** Change the number of puffs and rebuild. Clamps to ≥ 1. */
setPuffCount(n) {
this.puffCount = Math.max(1, Math.round(n));
this._buildPuffs();
}//end setPuffCount
/** Change the base puff radiusX (horizontal) and rebuild. */
setPuffWidth(w) {
this.puffWidth = w;
this._buildPuffs();
}//end setPuffWidth
/** Change the base puff radiusY (vertical) and rebuild. */
setPuffHeight(h) {
this.puffHeight = h;
this._buildPuffs();
}//end setPuffHeight
/** Change the horizontal scatter magnitude and rebuild. */
setSpread(s) {
this.spread = s;
this._buildPuffs();
}//end setSpread
/** Change the vertical scatter magnitude and rebuild. */
setSpreadV(v) {
this.spreadV = v;
this._buildPuffs();
}//end setSpreadV
/** Reposition the cloud anchor and rebuild all puffs around the new center. */
setCenter(x, y) {
this.center.x = x;
this.center.y = y;
this._buildPuffs();
}//end setCenter
/** Apply a new fill color to all puffs without rebuilding. */
setFillColor(col) {
this.fillColor = col ? SWColor.copy(col) : undefined;
for (const p of this._puffs) {
p.fillColor = this.fillColor ? SWColor.copy(this.fillColor) : undefined;
}
}//end setFillColor
/** Apply a new stroke color to all puffs without rebuilding. */
setStrokeColor(col) {
this.strokeColor = col ? SWColor.copy(col) : undefined;
for (const p of this._puffs) {
p.strokeColor = this.strokeColor ? SWColor.copy(this.strokeColor) : undefined;
}
}//end setStrokeColor
/** Apply a new stroke weight to all puffs without rebuilding. */
setStrokeWeight(w) {
this.strokeWeight = w;
for (const p of this._puffs) p.strokeWeight = w;
}//end setStrokeWeight
/** Update the phase offset (seconds) between adjacent puff breaths. */
setBreatheStagger(s) {
this.breatheStagger = s;
}//end setBreatheStagger
/** Change the horizontal drift speed (user units / second; positive = rightward). */
setDriftSpeed(v) {
this.driftSpeed = v;
}//end setDriftSpeed
/** Change the Perlin-noise morph amplitude (user units; 0 = no displacement). */
setNoiseAmp(v) {
this.noiseAmp = v;
}//end setNoiseAmp
/** Change how fast the Perlin-noise pattern evolves (higher = faster morphing). */
setNoiseSpeed(v) {
this.noiseSpeed = v;
}//end setNoiseSpeed
/** Change how far (user units) past the canvas edge the cloud travels before wrapping back. 0 = instant reappear. */
setWrapGap(v) {
this.wrapGap = Math.max(0, v);
}//end setWrapGap
/**
* Generates a completely new set of random puff offsets and rebuilds.
* Use this to get a fresh random cloud shape without changing any parameters.
*/
randomize() {
this._generateOffsets(this.puffCount);
this._buildPuffs();
}//end randomize
/**
* Advances cloud drift and applies per-puff Perlin-noise morphing each frame.
* Call once per frame from draw() when drift or noise-morph is active.
* Updates puff center positions in-place — does not rebuild SWEllipse objects.
*
* @param {number} t - elapsed time in seconds
* @param {number} deltaT - seconds since last frame
* @param {number} [wrapLeft=-15] - left wrap boundary in user coords
* @param {number} [wrapRight=15] - right wrap boundary in user coords
* @param {boolean} [doDrift=true] - advance center.x and wrap horizontally
* @param {boolean} [doNoise=true] - apply per-puff Perlin noise displacement
*/
update(t, deltaT, wrapLeft = -15, wrapRight = 15, doDrift = true, doNoise = true, wrapTop = 10, wrapBottom = -10) {
// 1. Advance drift — move center.x and wrap when cloud exits the canvas
if (doDrift) {
this.center.x += this.driftSpeed * deltaT;
// wrapGap: user-controlled distance past the canvas edge before wrapping
const margin = this.wrapGap;
// Vertical nudge applied on every wrap: ±1.5× puffHeight, clamped to canvas
const maxNudge = this.puffHeight * 1.5;
const nudge = (Math.random() * 2 - 1) * maxNudge;
const halfH = this.puffHeight * Math.max(this.spreadV, 0.5) * (this.puffCount * 0.3 + 1);
if (this.driftSpeed >= 0 && this.center.x - margin > wrapRight) {
this.center.x = wrapLeft - margin;
this.center.y = Math.min(wrapTop - halfH,
Math.max(wrapBottom + halfH, this.center.y + nudge));
} else if (this.driftSpeed < 0 && this.center.x + margin < wrapLeft) {
this.center.x = wrapRight + margin;
this.center.y = Math.min(wrapTop - halfH,
Math.max(wrapBottom + halfH, this.center.y + nudge));
}
}
// 2. Reposition each puff: base offset from _offsets, plus optional Perlin noise
for (let i = 0; i < this._puffs.length; i++) {
const off = this._offsets[i];
const baseX = this.center.x + off.dx * this.spread * this.puffWidth;
const baseY = this.center.y + off.dy * this.spreadV * this.puffHeight;
if (doNoise) {
// Spatially distinct noise per puff via prime-number multipliers on i
const nx = (noise(i * 1.73, t * this.noiseSpeed) * 2 - 1) * this.noiseAmp;
const ny = (noise(i * 3.17 + 50, t * this.noiseSpeed) * 2 - 1) * this.noiseAmp * 0.5;
this._puffs[i].center.x = baseX + nx;
this._puffs[i].center.y = baseY + ny;
} else {
this._puffs[i].center.x = baseX;
this._puffs[i].center.y = baseY;
}
}
}//end update
// —— Rendering ————————————————————————————————————
/**
* Draws all puffs in user/grid coordinates via the given SWGrid.
* @param {SWGrid} grid
*/
drawOnGrid(grid) {
for (const p of this._puffs) p.drawOnGrid(grid);
}//end drawOnGrid
/**
* Draws all puffs in screen (pixel) coordinates.
* radiusX / radiusY are treated as pixel values.
*/
draw() {
for (const p of this._puffs) p.draw();
}//end draw
// —— Animation ————————————————————————————————————
/**
* Scales each puff's radiusX and/or radiusY using SWSinusoid instances.
* Adjacent puffs breathe with a configurable phase stagger, producing
* an organic, billowing cloud effect.
*
* @param {SWSinusoid|null} sinusoidX — horizontal scale driver (or null)
* @param {SWSinusoid|null} sinusoidY — vertical scale driver (or null)
* @param {number} t — elapsed time in seconds
*/
breathe(sinusoidX, sinusoidY, t) {
for (let i = 0; i < this._puffs.length; i++) {
this._puffs[i].breathe(sinusoidX, sinusoidY, t + i * this.breatheStagger);
}
}//end breathe
/**
* Resets the cloud to its original construction parameters and rebuilds all puffs.
* Also restores the center position.
*/
reset() {
this.center.x = this.originalCenter.x;
this.center.y = this.originalCenter.y;
this.puffCount = this.originalPuffCount;
this.puffWidth = this.originalPuffWidth;
this.puffHeight = this.originalPuffHeight;
this.spread = this.originalSpread;
this.spreadV = this.originalSpreadV;
this.driftSpeed = this.originalDriftSpeed;
this.noiseAmp = this.originalNoiseAmp;
this.noiseSpeed = this.originalNoiseSpeed;
this.wrapGap = this.originalWrapGap;
// Restore the original random offsets so reset returns the exact original cloud shape
this._offsets = this.originalOffsets.map(o => ({ dx: o.dx, dy: o.dy, size: o.size }));
this._buildPuffs();
}//end reset
}//end class SWCloud