🔬 These lines turn mouse distance into a breathing rhythm. What happens if you swap the map() output range from (2.5, 0.6) to (0.6, 2.5)? Would moving toward the center now slow the breathing instead of speeding it up?
const breathingSpeedFactor = map(d, 0, maxD, 2.5, 0.6, true);
// Use sin(frameCount * 0.02 * factor) for breathing phase
const t = frameCount * 0.02 * breathingSpeedFactor;
const breathingScale = map(sin(t), -1, 1, 0.65, 1.12); // scale oscillates smoothly
🔬 This controls how many shapes appear per ring and what hue each ring starts at. What happens if numShapes stays constant across all rings instead of growing with i?
const numShapes = 6 + i * 4;
// Base hue shifts as rings go outward
const baseHue = (200 + i * 25) % 360;
function draw() {
// Dark, slightly tinted background (HSB)
background(230, 60, 3, 100);
const cx = width / 2;
const cy = height / 2;
// ----- Breathing speed from mouse distance -----
const d = dist(mouseX, mouseY, cx, cy);
const maxD = max(width, height) / 2;
// Closer to center = higher factor (faster breathing)
const breathingSpeedFactor = map(d, 0, maxD, 2.5, 0.6, true);
// Use sin(frameCount * 0.02 * factor) for breathing phase
const t = frameCount * 0.02 * breathingSpeedFactor;
const breathingScale = map(sin(t), -1, 1, 0.65, 1.12); // scale oscillates smoothly
// ----- Dust layer (normal blending) -----
for (let p of dustParticles) {
p.update();
p.display();
}
// ----- Mandala -----
push();
translate(cx, cy);
scale(breathingScale);
// Additive blending for glowing overlaps
blendMode(ADD);
const maxRadius = min(width, height) * 0.4;
const radiusStep = maxRadius / NUM_RINGS;
for (let i = 0; i < NUM_RINGS; i++) {
const ringRadius = radiusStep * (i + 1);
// More shapes as we go outward
const numShapes = 6 + i * 4;
// Base hue shifts as rings go outward
const baseHue = (200 + i * 25) % 360;
// Each ring slowly rotates; alternate direction for variety
const ringRotation = t * 0.1 * (i % 2 === 0 ? 1 : -1);
for (let j = 0; j < numShapes; j++) {
const angle = TWO_PI * (j / numShapes) + ringRotation;
const x = cos(angle) * ringRadius;
const y = sin(angle) * ringRadius;
push();
translate(x, y);
// Choose shape type per ring: 0 circle, 1 triangle, 2 hexagon
const shapeType = i % 3;
// Shape size relative to ring spacing
const diameter = radiusStep * 0.5;
// Subtle hue and brightness variation per shape
const hueJitter = 10 * sin(t + i + j * 0.3);
const sat = 60 + 30 * sin(t * 0.3 + i * 0.7);
const bright = 70 + 20 * sin(t * 0.4 - j * 0.2);
fill((baseHue + hueJitter + 360) % 360, sat, bright, 70);
// Shapes also rotate slowly for added motion
rotate(t * 0.2 + angle * 0.1);
drawMandalaShape(shapeType, diameter);
pop();
}
}
pop();
// Reset blending for anything drawn later
blendMode(BLEND);
}
Line-by-line explanation (21 lines)
🔧 Subcomponents:
for-loop
Dust Particle Loop
for (let p of dustParticles) {
Updates and draws every dust particle before the mandala is drawn, so it sits visually behind the glowing shapes.
for-loop
Ring Loop
for (let i = 0; i < NUM_RINGS; i++) {
Iterates over each concentric ring, computing its radius, shape count, color, and rotation before drawing its shapes.
for-loop
Shape Placement Loop
for (let j = 0; j < numShapes; j++) {
Places numShapes copies of a shape evenly around the current ring's circumference using trigonometry.
calculation
Shape Type Selection
const shapeType = i % 3;
Cycles through circle, triangle, and hexagon shapes ring by ring using the modulo operator.
background(230, 60, 3, 100);
- Repaints the whole canvas each frame with a very dark, slightly blue-tinted color (HSB hue 230, low saturation, very low brightness), which both clears the previous frame and sets the mood.
const d = dist(mouseX, mouseY, cx, cy);
- Calculates the pixel distance between the mouse cursor and the canvas center - this drives the interactive breathing speed.
const breathingSpeedFactor = map(d, 0, maxD, 2.5, 0.6, true);
- Maps the mouse distance to a speed factor: when the mouse is at the center (d=0) the factor is 2.5 (fast), and at the farthest edge it's 0.6 (slow). The final 'true' clamps the result so it never goes outside that range.
const t = frameCount * 0.02 * breathingSpeedFactor;
- Builds an ever-increasing 'time' value from the frame counter, scaled by the speed factor - this t variable drives every animated sine wave in the sketch.
const breathingScale = map(sin(t), -1, 1, 0.65, 1.12);
- Takes the sine wave of t (which oscillates between -1 and 1) and remaps it to a scale factor between 0.65 and 1.12 - this is the actual 'breathing' zoom in/out amount.
for (let p of dustParticles) {
- Loops through every dust particle object and calls its update() and display() methods, moving and drawing each one.
translate(cx, cy);
- Moves the coordinate origin (0,0) to the center of the canvas, so all mandala shapes can be positioned relative to the center instead of the top-left corner.
scale(breathingScale);
- Scales everything drawn afterward by the breathing factor, making the entire mandala grow and shrink as one unit - this is the 'breathing' effect.
blendMode(ADD);
- Switches to additive color blending so overlapping translucent shapes add their brightness together, creating a glowing effect where colors intersect.
const radiusStep = maxRadius / NUM_RINGS;
- Divides the total available radius evenly among all rings, so each ring is spaced consistently from the center outward.
const numShapes = 6 + i * 4;
- Increases the number of shapes per ring the further out you go (ring 0 has 6 shapes, ring 1 has 10, etc.), making outer rings denser.
const ringRotation = t * 0.1 * (i % 2 === 0 ? 1 : -1);
- Gives each ring its own slow rotation over time; even-indexed rings rotate one way and odd-indexed rings rotate the opposite way, creating a swirling counter-rotation effect.
const angle = TWO_PI * (j / numShapes) + ringRotation;
- Calculates the angle for shape j by dividing the full circle (TWO_PI radians) evenly among numShapes, then adds the ring's current rotation offset.
const x = cos(angle) * ringRadius;
- Converts the angle and ring radius into an x coordinate using cosine - this is standard 'polar to cartesian' conversion for placing points on a circle.
const y = sin(angle) * ringRadius;
- Converts the angle and ring radius into a y coordinate using sine, completing the circular placement.
const shapeType = i % 3;
- Uses modulo 3 on the ring index to cycle through three shape types (circle, triangle, hexagon) as rings go outward.
const hueJitter = 10 * sin(t + i + j * 0.3);
- Adds a small wobble to the hue based on time, ring index, and shape index, so colors shimmer slightly instead of staying static.
fill((baseHue + hueJitter + 360) % 360, sat, bright, 70);
- Sets the fill color for this shape using the jittered hue (wrapped into the 0-360 range with +360 % 360 to avoid negative values), computed saturation, brightness, and a fixed alpha of 70.
rotate(t * 0.2 + angle * 0.1);
- Rotates this individual shape slightly based on time and its own angle, adding extra layered motion on top of the ring's overall rotation.
drawMandalaShape(shapeType, diameter);
- Calls the helper function to actually draw the circle, triangle, or hexagon at the current transformed position.
blendMode(BLEND);
- Restores normal (non-additive) blending at the end of the frame so nothing drawn afterward is accidentally affected by the glow blend mode.
function drawMandalaShape(type, diameter) {
if (type === 0) {
// circle(d) uses diameter: https://p5js.org/reference/#/p5/circle
circle(0, 0, diameter);
} else if (type === 1) {
polygon(0, 0, diameter * 0.55, 3); // triangle
} else {
polygon(0, 0, diameter * 0.55, 6); // hexagon
}
}
Line-by-line explanation (6 lines)
🔧 Subcomponents:
conditional
Shape Type Branching
if (type === 0) {
Chooses which shape to draw (circle, triangle, or hexagon) based on the numeric type passed in.
if (type === 0) {
- Checks if the shape type is 0, which is used for circles.
circle(0, 0, diameter);
- Draws a circle centered at the local origin (0,0) - since this function is always called after translate(), (0,0) is wherever the shape is supposed to appear.
} else if (type === 1) {
- Checks if the shape type is 1, used for triangles.
polygon(0, 0, diameter * 0.55, 3); // triangle
- Calls the polygon() helper with 3 points to draw a triangle, using a slightly smaller radius (diameter * 0.55) so triangles look visually similar in size to the circles.
} else {
- Any other type value (here, only 2) falls into this final branch, used for hexagons.
polygon(0, 0, diameter * 0.55, 6); // hexagon
- Calls polygon() with 6 points to draw a hexagon at the same relative size as the triangle.
🔬 This is the 'gentle pull toward center' force. What happens if you increase 0.02 to something like 0.2? Would the dust cluster tightly around the middle instead of spreading across the whole screen?
this.x += 0.02 * cos(angleToCenter) * this.z;
this.y += 0.02 * sin(angleToCenter) * this.z;
class DustParticle {
constructor() {
this.reset();
}
reset() {
this.x = random(width);
this.y = random(height);
this.z = random(0.5, 1.2); // depth factor for speed/size
this.size = random(1, 3);
this.alpha = random(5, 20); // very subtle
this.vx = random(-0.15, 0.15);
this.vy = random(-0.15, 0.15);
}
update() {
this.x += this.vx * this.z;
this.y += this.vy * this.z;
// Gentle drift back toward center for cohesion
const cx = width / 2;
const cy = height / 2;
const angleToCenter = atan2(cy - this.y, cx - this.x);
this.x += 0.02 * cos(angleToCenter) * this.z;
this.y += 0.02 * sin(angleToCenter) * this.z;
// Wrap or reset if far out of bounds
if (this.x < -50 || this.x > width + 50 || this.y < -50 || this.y > height + 50) {
this.reset();
}
}
display() {
// Soft white dust in HSB (S=0 gives grayscale)
fill(0, 0, 100, this.alpha);
circle(this.x, this.y, this.size * this.z);
}
}
Line-by-line explanation (13 lines)
🔧 Subcomponents:
conditional
Out-of-Bounds Reset
if (this.x < -50 || this.x > width + 50 || this.y < -50 || this.y > height + 50) {
Checks whether a particle has drifted far past the canvas edges and, if so, resets it to a new random position.
constructor() {
- Runs automatically whenever 'new DustParticle()' is called - it immediately calls reset() to give the particle its starting randomized properties.
this.x = random(width);
- Picks a random starting horizontal position anywhere across the canvas width.
this.z = random(0.5, 1.2); // depth factor for speed/size
- Gives the particle a fake 'depth' value between 0.5 and 1.2, used later to scale both its speed and its size - larger z means it moves faster and appears bigger, simulating closer particles.
this.size = random(1, 3);
- Sets a small random base size for the particle, between 1 and 3 pixels.
this.alpha = random(5, 20); // very subtle
- Gives the particle a very low opacity (5 to 20 out of 100) so the dust stays subtle and doesn't distract from the mandala.
this.vx = random(-0.15, 0.15);
- Assigns a tiny random horizontal velocity, which can be positive or negative, so particles drift in different directions.
this.x += this.vx * this.z;
- Moves the particle horizontally each frame, with the depth factor z scaling how fast it moves (deeper/closer particles drift faster).
const angleToCenter = atan2(cy - this.y, cx - this.x);
- Calculates the angle pointing from the particle's current position toward the canvas center, using atan2 (a math function that returns the angle of a direction vector).
this.x += 0.02 * cos(angleToCenter) * this.z;
- Nudges the particle a tiny bit toward the center each frame, creating a gentle gravitational pull that keeps particles loosely clustered rather than drifting away forever.
if (this.x < -50 || this.x > width + 50 || this.y < -50 || this.y > height + 50) {
- Checks if the particle has drifted more than 50 pixels past any edge of the canvas.
this.reset();
- Calls reset() again to give the particle a brand new random position and properties, effectively recycling it instead of letting it disappear forever.
fill(0, 0, 100, this.alpha);
- Sets the fill color to white (hue and saturation both 0, brightness 100) with this particle's individual low alpha, giving a soft grayscale dust look.
circle(this.x, this.y, this.size * this.z);
- Draws the particle as a small circle, with its final diameter scaled by the depth factor z so 'closer' particles appear larger.