Kaleidoscope Symmetry - xelsed.ai

This sketch creates a full-screen, six-fold kaleidoscope built from more than a dozen layered geometric elements - polygons, stars, spirographs, Lissajous curves, recursive nested shapes and a rotating grid - all drawn once and then rotated and mirrored around the canvas center. Every parameter (speeds, sizes, distances, colors) is exposed through a dat.GUI control panel so the viewer can reshape the pattern live.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the color cycle — Raising hueSpeed makes the rainbow color shift noticeably faster across every shape in the kaleidoscope.
  2. Switch to 8-fold symmetry — Changing the loop count and rotation angle together turns the pattern from a 6-wedge kaleidoscope into an 8-wedge one.
  3. Enlarge the background glow — Increasing the multiplier on the background circle's size makes the soft glowing backdrop fill much more of the canvas.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch draws a single wedge of intricate shapes - polygons, stars, pulsing circles, a spirograph, a Lissajous curve, a recursive nested polygon and a rotating grid - then uses rotate(), scale(-1, 1) and a for-loop to stamp that wedge around the canvas six times, creating the classic mirrored kaleidoscope look. It runs entirely in HSB color mode so hues can be shifted with simple addition and modulo math, and every visual parameter is wired up to a dat.GUI panel so you can drag sliders to reshape the pattern in real time.

The code is organized around one big helper, drawKaleidoscopeShapes(), which draws every shape for a single wedge using frameCount-driven sine and cosine waves for animation, plus several small reusable drawing helpers (drawRegularPolygon, drawStar, drawSpirograph, drawLissajous, drawRecursivePolygon, drawDynamicGrid) that each encapsulate one shape's vertex math. By studying it you'll learn how push()/pop() and rotate()/scale() build symmetry from a single piece of art, how parametric curve formulas (spirograph, Lissajous) turn into vertex() calls, and how a dat.GUI object can expose dozens of live-tunable parameters without touching the draw loop's structure.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, switches to HSB color mode, and builds a dat.GUI panel with folders for every shape category (Outer Polygons, Inner Stars, Spirograph, Lissajous, and more), each bound to a value inside the params object.
  2. Every frame, draw() clears the screen to black, moves the origin to the canvas center, and updates a global hueOffset based on frameCount so colors slowly cycle through the rainbow.
  3. A for-loop runs six times, rotating the canvas 60 degrees each time (6 x 60 = 360) and calling drawKaleidoscopeShapes() to render one wedge of shapes, then mirroring that same wedge with scale(-1, 1) to complete the symmetric pattern.
  4. Inside drawKaleidoscopeShapes(), dozens of sin()/cos() calculations driven by frameCount continuously reshuffle each shape's size, distance from center, rotation and hue, so outer polygons pulse, stars spin, and the spirograph and Lissajous curves slowly morph.
  5. Two of the shapes use recursion and loops internally: drawRecursivePolygon() calls itself to nest smaller rotated polygons inside each other, and drawDynamicGrid() loops through rows and columns of lines to build a rotating, expanding grid.
  6. Because every numeric input to these shapes comes from the params object, dragging any dat.GUI slider immediately changes what the next frame draws, letting you sculpt the kaleidoscope live without editing code.

🎓 Concepts You'll Learn

Radial symmetry via rotate() and a for-loopMirroring shapes with scale(-1, 1)HSB color mode for easy hue cyclingTrigonometric vertex generation (cos/sin around a circle)Recursion for self-similar nested shapesParametric curves (spirograph, Lissajous)push()/pop() transformation stackingLive parameter tuning with dat.GUI

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. Here it prepares the canvas, sets the color system, and wires up dozens of dat.GUI sliders to the params object so every other function can read live, user-adjustable values instead of hardcoded numbers.

🔬 These sliders' maximum range is tied to min(width, height) / 2, so it automatically scales with canvas size. What happens if you replace that with a fixed number like 300 - will the sliders still make sense if you resize the browser window afterward?

  const outerFolder = gui.addFolder('Outer Polygons');
  outerFolder.add(params, 'outerDistanceMin', 50, min(width, height) / 2).name('Dist Min');
  outerFolder.add(params, 'outerDistanceMax', 50, min(width, height) / 2).name('Dist Max');
function setup() {
  createCanvas(windowWidth, windowHeight);
  colorMode(HSB, 360, 100, 100, 100);
  noStroke();

  // Setup dat.gui
  gui = new dat.GUI();

  // General controls
  gui.add(params, 'hueSpeed', 0.1, 2.0).name('Hue Shift Speed');
  gui.add(params, 'overallAlpha', 20, 100).name('Overall Alpha');
  gui.add(params, 'saturationMin', 0, 100).name('Sat Min');
  gui.add(params, 'saturationMax', 0, 100).name('Sat Max');
  gui.add(params, 'brightnessMin', 0, 100).name('Bright Min');
  gui.add(params, 'brightnessMax', 0, 100).name('Bright Max');

  // Outer Polygons
  const outerFolder = gui.addFolder('Outer Polygons');
  outerFolder.add(params, 'outerDistanceMin', 50, min(width, height) / 2).name('Dist Min');
  outerFolder.add(params, 'outerDistanceMax', 50, min(width, height) / 2).name('Dist Max');
  outerFolder.add(params, 'outerSizeMin', 5, 100).name('Size Min');
  outerFolder.add(params, 'outerSizeMax', 5, 100).name('Size Max');
  outerFolder.add(params, 'outerSidesMin', 3, 10, 1).name('Sides Min');
  outerFolder.add(params, 'outerSidesMax', 3, 10, 1).name('Sides Max');
  outerFolder.add(params, 'outerRotationSpeed', -0.1, 0.1).name('Rotation Speed');

  // Inner Stars
  const innerFolder = gui.addFolder('Inner Stars');
  innerFolder.add(params, 'innerDistanceMin', 10, min(width, height) / 3).name('Dist Min');
  innerFolder.add(params, 'innerDistanceMax', 10, min(width, height) / 3).name('Dist Max');
  innerFolder.add(params, 'innerStarPointsMin', 3, 10, 1).name('Points Min');
  innerFolder.add(params, 'innerStarPointsMax', 3, 10, 1).name('Points Max');
  innerFolder.add(params, 'innerRadius1Min', 5, 50).name('Inner Rad Min');
  innerFolder.add(params, 'innerRadius1Max', 5, 50).name('Inner Rad Max');
  innerFolder.add(params, 'innerRotationSpeed', -0.1, 0.1).name('Rotation Speed');

  // Pulsing Circles
  const pulseFolder = gui.addFolder('Pulsing Circles');
  pulseFolder.add(params, 'pulseCount', 1, 10, 1).name('Count');
  pulseFolder.add(params, 'pathRadiusMin', 10, min(width, height) / 3).name('Path Rad Min');
  pulseFolder.add(params, 'pathRadiusMax', 10, min(width, height) / 3).name('Path Rad Max');
  pulseFolder.add(params, 'pulseSizeMin', 2, 50).name('Size Min');
  pulseFolder.add(params, 'pulseSizeMax', 2, 50).name('Size Max');
  pulseFolder.add(params, 'pulseOffsetSpeed', -0.1, 0.1).name('Offset Speed');

  // Animated Strip
  const stripFolder = gui.addFolder('Animated Strip');
  stripFolder.add(params, 'stripDistanceMin', 10, min(width, height) / 4).name('Dist Min');
  stripFolder.add(params, 'stripDistanceMax', 10, min(width, height) / 4).name('Dist Max');
  stripFolder.add(params, 'stripLengthMin', 10, 200).name('Length Min');
  stripFolder.add(params, 'stripLengthMax', 10, 200).name('Length Max');
  stripFolder.add(params, 'stripHeight', 5, 50).name('Height');
  stripFolder.add(params, 'stripOffsetMin', -50, 50).name('Offset Min');
  stripFolder.add(params, 'stripOffsetMax', -50, 50).name('Offset Max');
  stripFolder.add(params, 'stripRotationSpeed', -0.1, 0.1).name('Rotation Speed');

  // Small Shapes
  const smallFolder = gui.addFolder('Small Shapes');
  smallFolder.add(params, 'smallDistanceMin', 10, min(width, height) / 6).name('Dist Min');
  smallFolder.add(params, 'smallDistanceMax', 10, min(width, height) / 6).name('Dist Max');
  smallFolder.add(params, 'smallSizeMin', 2, 30).name('Size Min');
  smallFolder.add(params, 'smallSizeMax', 2, 30).name('Size Max');
  smallFolder.add(params, 'smallRotationSpeed', -0.1, 0.1).name('Rotation Speed');

  // Spirograph
  const spiroFolder = gui.addFolder('Spirograph');
  spiroFolder.add(params, 'spiroR_min', 10, 200).name('R Min');
  spiroFolder.add(params, 'spiroR_max', 10, 200).name('R Max');
  spiroFolder.add(params, 'spiro_r_min', 5, 100).name('r Min');
  spiroFolder.add(params, 'spiro_r_max', 5, 100).name('r Max');
  spiroFolder.add(params, 'spiro_d_min', 0, 100).name('d Min');
  spiroFolder.add(params, 'spiro_d_max', 0, 100).name('d Max');
  spiroFolder.add(params, 'spiroRotationSpeed', -0.1, 0.1).name('Rotation Speed');
  spiroFolder.add(params, 'spiroStrokeWeight', 0.5, 5).name('Stroke Weight');
  spiroFolder.add(params, 'spiroFillAlpha', 0, 100).name('Fill Alpha');
  spiroFolder.add(params, 'spiroStrokeAlpha', 0, 100).name('Stroke Alpha');

  // Lissajous Curve
  const lissajousFolder = gui.addFolder('Lissajous');
  lissajousFolder.add(params, 'lissajous_A', 10, 200).name('Amplitude A');
  lissajousFolder.add(params, 'lissajous_B', 10, 200).name('Amplitude B');
  lissajousFolder.add(params, 'lissajous_a_min', 1, 10, 1).name('Freq a Min');
  lissajousFolder.add(params, 'lissajous_a_max', 1, 10, 1).name('Freq a Max');
  lissajousFolder.add(params, 'lissajous_b_min', 1, 10, 1).name('Freq b Min');
  lissajousFolder.add(params, 'lissajous_b_max', 1, 10, 1).name('Freq b Max');
  lissajousFolder.add(params, 'lissajous_delta_speed', -0.05, 0.05).name('Delta Speed');
  lissajousFolder.add(params, 'lissajous_rotation_speed', -0.1, 0.1).name('Rotation Speed');
  lissajousFolder.add(params, 'lissajous_stroke_weight', 0.5, 5).name('Stroke Weight');
  lissajousFolder.add(params, 'lissajous_stroke_alpha', 0, 100).name('Stroke Alpha');
  lissajousFolder.add(params, 'lissajous_fill_alpha', 0, 100).name('Fill Alpha');

  // Recursive Polygons
  const recursiveFolder = gui.addFolder('Recursive Polygons');
  recursiveFolder.add(params, 'recursivePolygonMaxDepth', 1, 5, 1).name('Max Depth');
  recursiveFolder.add(params, 'recursivePolygonShrinkFactor', 0.5, 0.99).name('Shrink Factor');
  recursiveFolder.add(params, 'recursivePolygonRotationOffset', -0.1, 0.1).name('Rotation Offset');
  recursiveFolder.add(params, 'recursivePolygonSize', 20, 200).name('Base Size');
  recursiveFolder.add(params, 'recursivePolygonSides', 3, 10, 1).name('Sides');
  recursiveFolder.add(params, 'recursivePolygonX', -min(width, height) / 3, min(width, height) / 3).name('X Position');
  recursiveFolder.add(params, 'recursivePolygonY', -min(width, height) / 3, min(width, height) / 3).name('Y Position');
  recursiveFolder.add(params, 'recursivePolygonHueOffset', 0, 360).name('Hue Offset');

  // Dynamic Grid
  const gridFolder = gui.addFolder('Dynamic Grid');
  gridFolder.add(params, 'dynamicGridSize', 50, 400).name('Size');
  gridFolder.add(params, 'dynamicGridDensity', 5, 20, 1).name('Density');
  gridFolder.add(params, 'dynamicGridRotationSpeed', -0.1, 0.1).name('Rotation Speed');
  gridFolder.add(params, 'dynamicGridExpansionSpeed', -0.05, 0.05).name('Expansion Speed');
  gridFolder.add(params, 'dynamicGridStrokeWeight', 0.5, 3).name('Stroke Weight');
  gridFolder.add(params, 'dynamicGridX', -min(width, height) / 3, min(width, height) / 3).name('X Position');
  gridFolder.add(params, 'dynamicGridY', -min(width, height) / 3, min(width, height) / 3).name('Y Position');
}
Line-by-line explanation (7 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window.
colorMode(HSB, 360, 100, 100, 100);
Switches from the default RGB color model to HSB (hue, saturation, brightness, alpha), with hue running 0-360 like a color wheel - this makes rainbow color cycling as simple as adding a number and wrapping with modulo.
noStroke();
Turns off outlines by default so shapes drawn later start with no stroke unless one is explicitly set.
gui = new dat.GUI();
Creates the floating control panel in the corner of the screen that all the sliders below will be added to.
gui.add(params, 'hueSpeed', 0.1, 2.0).name('Hue Shift Speed');
Adds a slider bound directly to params.hueSpeed with a range of 0.1 to 2.0 - dragging it live-updates the variable the draw loop reads every frame.
const outerFolder = gui.addFolder('Outer Polygons');
Groups a set of related sliders into a collapsible folder so the panel stays organized instead of one huge list.
outerFolder.add(params, 'outerDistanceMin', 50, min(width, height) / 2).name('Dist Min');
One example of many folder sliders - this one's maximum range is calculated from the canvas size so it always makes sense regardless of screen dimensions.

draw()

draw() runs continuously at roughly 60 frames per second. It resets the background, updates the shared hueOffset, and uses a loop combined with rotate() and scale() to turn a single wedge of artwork into a fully symmetric kaleidoscope.

🔬 6 wedges times 60 degrees equals a perfect 360-degree tiling. What happens if you change both the loop limit to 8 and radians(60) to radians(45) to get 8-fold symmetry instead?

  for (let i = 0; i < 6; i++) {
    // Rotate the entire canvas by 60 degrees for each wedge
    rotate(radians(60));
function draw() {
  background(0); // Draw a black background each frame

  // Move the origin of the canvas to the center
  translate(width / 2, height / 2);

  // Animate the base hue over time for rainbow effect
  hueOffset = (frameCount * params.hueSpeed) % 360;

  // Loop 6 times for 6-fold symmetry
  for (let i = 0; i < 6; i++) {
    // Rotate the entire canvas by 60 degrees for each wedge
    rotate(radians(60));

    // 1. Draw the shapes for the current wedge
    drawKaleidoscopeShapes();

    // 2. Mirror the shapes for the other half of the wedge
    push();
    scale(-1, 1);
    drawKaleidoscopeShapes();
    pop();
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

for-loop Six-Fold Symmetry Loop for (let i = 0; i < 6; i++) {

Repeats the wedge-drawing process 6 times, rotating 60 degrees each time so the wedges tile a full 360-degree circle.

background(0); // Draw a black background each frame
Fills the entire canvas with solid black every frame, erasing the previous frame so there are no motion trails.
translate(width / 2, height / 2);
Moves the (0,0) drawing origin to the middle of the canvas, so every rotation and shape below is centered on the screen instead of the top-left corner.
hueOffset = (frameCount * params.hueSpeed) % 360;
Continuously grows a hue value using the built-in frameCount (frames elapsed) multiplied by the GUI-controlled speed, then wraps it back into the 0-360 range with modulo so it cycles forever like a color wheel.
for (let i = 0; i < 6; i++) {
Starts a loop that repeats everything inside it 6 times - once for each of the 6 symmetric wedges.
rotate(radians(60));
Rotates the entire coordinate system by 60 degrees before drawing this wedge; because 6 x 60 = 360, six rotations bring the pattern all the way back around.
drawKaleidoscopeShapes();
Draws one full set of shapes (polygons, stars, curves, etc.) at the current rotation angle.
push();
Saves the current rotation/translation state so the mirroring below can be undone afterward.
scale(-1, 1);
Flips the x-axis, mirroring everything drawn after this point - this is what creates the symmetric left-right reflection inside each wedge.
pop();
Restores the transformation state saved by push(), undoing the mirror flip before the loop moves to the next wedge.

drawRegularPolygon()

This is a reusable helper for drawing any regular polygon (triangle, hexagon, octagon, etc.) at any position, size and rotation. It's called repeatedly for the outer ring of shapes in each wedge with different side counts calculated from a sine wave.

function drawRegularPolygon(x, y, radius, sides, rotationAngle = 0) {
  if (sides < 3) return;
  push();
  translate(x, y);
  rotate(rotationAngle);
  beginShape();
  for (let a = 0; a < TWO_PI; a += TWO_PI / sides) {
    let sx = cos(a) * radius;
    let sy = sin(a) * radius;
    vertex(sx, sy);
  }
  endShape(CLOSE);
  pop();
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Minimum Sides Guard if (sides < 3) return;

Prevents drawing an invalid polygon if fewer than 3 sides are requested.

for-loop Vertex Placement Loop for (let a = 0; a < TWO_PI; a += TWO_PI / sides) {

Walks evenly around a full circle, placing one vertex per side using trigonometry.

if (sides < 3) return;
A polygon needs at least 3 sides to exist, so this guard clause exits early for invalid input instead of drawing garbage.
push();
Saves the current transform so this shape's own translate/rotate doesn't leak into shapes drawn after it.
translate(x, y);
Moves the drawing origin to where this polygon should be centered.
rotate(rotationAngle);
Spins the polygon around its own center by the given angle before drawing any vertices.
beginShape();
Starts recording a custom shape built from a series of vertex() points.
for (let a = 0; a < TWO_PI; a += TWO_PI / sides) {
Loops through angles from 0 to a full circle (TWO_PI radians), stepping by an amount that divides the circle into exactly 'sides' equal pieces.
let sx = cos(a) * radius;
Converts the current angle into an x-coordinate on a circle of the given radius - basic trigonometry for placing points around a circle.
let sy = sin(a) * radius;
Converts the current angle into the matching y-coordinate.
vertex(sx, sy);
Adds this (sx, sy) point as one corner of the polygon shape.
endShape(CLOSE);
Finishes the shape and draws a final line back to the first vertex, closing the polygon.
pop();
Restores the transform saved by push(), so later shapes aren't affected by this polygon's translate/rotate.

drawStar()

This helper draws star polygons by alternating between an outer radius (the tips) and an inner radius (the valleys). It powers the two rotating inner stars in each wedge, whose point count oscillates over time via a sine wave.

🔬 Each iteration adds an outer tip (radius2) then an inner valley (radius1). What happens if you swap radius1 and radius2 throughout this loop, making the star point inward instead of outward?

  for (let a = 0; a < TWO_PI; a += angle) {
    let sx = cos(a) * radius2;
    let sy = sin(a) * radius2;
    vertex(sx, sy);
    sx = cos(a + halfAngle) * radius1;
    sy = sin(a + halfAngle) * radius1;
    vertex(sx, sy);
  }
function drawStar(x, y, radius1, radius2, npoints, rotationAngle = 0) {
  if (npoints < 3) return;
  push();
  translate(x, y);
  rotate(rotationAngle);
  let angle = TWO_PI / npoints;
  let halfAngle = angle / 2.0;
  beginShape();
  for (let a = 0; a < TWO_PI; a += angle) {
    let sx = cos(a) * radius2;
    let sy = sin(a) * radius2;
    vertex(sx, sy);
    sx = cos(a + halfAngle) * radius1;
    sy = sin(a + halfAngle) * radius1;
    vertex(sx, sy);
  }
  endShape(CLOSE);
  pop();
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Minimum Points Guard if (npoints < 3) return;

Skips drawing if fewer than 3 star points are requested.

for-loop Alternating Inner/Outer Vertex Loop for (let a = 0; a < TWO_PI; a += angle) {

For each point, adds an outer tip vertex then an inner valley vertex, alternating to build the zigzag star outline.

if (npoints < 3) return;
A star needs at least 3 points to look like a star, so this guards against invalid input.
let angle = TWO_PI / npoints;
Calculates the angular spacing between each outer tip of the star, based on how many points it should have.
let halfAngle = angle / 2.0;
Finds the midpoint angle between two tips, which is where each inner 'valley' vertex will be placed.
for (let a = 0; a < TWO_PI; a += angle) {
Loops once per star point, advancing the angle by the spacing calculated above.
let sx = cos(a) * radius2;
Places an outer tip vertex using the larger radius2 at the current angle.
sx = cos(a + halfAngle) * radius1;
Places the following inner valley vertex using the smaller radius1, offset by half the angular spacing so it falls between two tips.
endShape(CLOSE);
Closes the shape, connecting the last vertex back to the first to complete the star outline.

drawSpirograph()

This function implements a real hypotrochoid curve, the same math behind physical Spirograph toys. It relies on gcd() to figure out the curve's true period so the loop draws exactly one full, closed pattern.

🔬 This is the classic spirograph (hypotrochoid) formula. What happens if you flip the minus sign to a plus in the sy line, changing the curve's chirality (its left/right winding direction)?

  for (let i = 0; i <= steps; i++) {
    let t = map(i, 0, steps, 0, TWO_PI * r / gcd(R, r));
    let sx = (R - r) * cos(t) + d * cos(((R - r) / r) * t);
    let sy = (R - r) * sin(t) - d * sin(((R - r) / r) * t);
    vertex(sx, sy);
  }
function drawSpirograph(x, y, R, r, d, rotationAngle = 0, steps = 200) {
  if (R <= 0 || r <= 0) return;
  push();
  translate(x, y);
  rotate(rotationAngle);
  beginShape();
  for (let i = 0; i <= steps; i++) {
    let t = map(i, 0, steps, 0, TWO_PI * r / gcd(R, r));
    let sx = (R - r) * cos(t) + d * cos(((R - r) / r) * t);
    let sy = (R - r) * sin(t) - d * sin(((R - r) / r) * t);
    vertex(sx, sy);
  }
  endShape();
  pop();
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Positive Radius Guard if (R <= 0 || r <= 0) return;

Avoids drawing the curve with invalid zero or negative circle radii.

for-loop Hypotrochoid Curve Loop for (let i = 0; i <= steps; i++) {

Samples the classic spirograph (hypotrochoid) parametric formula at many points to build a smooth curve.

if (R <= 0 || r <= 0) return;
The spirograph math divides by these radii, so this guard avoids errors or degenerate curves from invalid values.
let t = map(i, 0, steps, 0, TWO_PI * r / gcd(R, r));
Calculates how far around the curve's full repeating period to sample; the gcd() call finds the exact point where the pattern starts repeating so the curve closes cleanly.
let sx = (R - r) * cos(t) + d * cos(((R - r) / r) * t);
This is the classic hypotrochoid formula's x-coordinate: it combines the motion of a small circle rolling inside a larger fixed circle with a pen offset by distance d.
let sy = (R - r) * sin(t) - d * sin(((R - r) / r) * t);
The matching y-coordinate for the same formula, producing the looping spirograph pattern when plotted together with sx.
endShape();
Finishes the shape without closing it (no CLOSE argument), since the spirograph curve already returns to its start point mathematically.

gcd()

gcd() finds the Greatest Common Divisor of two numbers using recursion - a function that calls itself with smaller inputs until it reaches a simple base case. It's used by drawSpirograph() to compute the true repeating period of the curve.

function gcd(a, b) {
  return b === 0 ? a : gcd(b, a % b);
}
Line-by-line explanation (1 lines)

🔧 Subcomponents:

conditional Recursion Base Case return b === 0 ? a : gcd(b, a % b);

Stops the recursion once b reaches 0, returning a as the greatest common divisor; otherwise it recurses using the Euclidean algorithm.

return b === 0 ? a : gcd(b, a % b);
A ternary expression implementing the Euclidean algorithm: if b is 0, a is the answer; otherwise the function calls itself with (b, a % b), gradually shrinking the numbers until b becomes 0.

drawLissajous()

This function draws a Lissajous curve, a classic parametric shape formed by combining two sine waves at right angles. Its frequency parameters oscillate over time in drawKaleidoscopeShapes(), morphing the curve continuously.

🔬 Lissajous curves are famous for how the ratio between the x-frequency and y-frequency changes the shape. What happens if you hardcode both to the same frequency, like sin(3 * t), turning the curve into a perfect diagonal loop instead of a complex knot?

  for (let i = 0; i <= steps; i++) {
    let t = map(i, 0, steps, 0, TWO_PI);
    let sx = A * sin(a * t + delta);
    let sy = B * sin(b * t);
    vertex(sx, sy);
  }
function drawLissajous(x, y, A, B, a, b, delta, rotationAngle = 0, steps = 200) {
  push();
  translate(x, y);
  rotate(rotationAngle);
  beginShape();
  for (let i = 0; i <= steps; i++) {
    let t = map(i, 0, steps, 0, TWO_PI);
    let sx = A * sin(a * t + delta);
    let sy = B * sin(b * t);
    vertex(sx, sy);
  }
  endShape();
  pop();
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Lissajous Sampling Loop for (let i = 0; i <= steps; i++) {

Samples the Lissajous parametric equations across a full cycle to build the curve's vertices.

let t = map(i, 0, steps, 0, TWO_PI);
Converts the loop counter into an angle t that sweeps from 0 to a full circle (TWO_PI) across all the steps.
let sx = A * sin(a * t + delta);
The Lissajous x-formula: amplitude A times a sine wave whose frequency is controlled by 'a' and phase-shifted by delta.
let sy = B * sin(b * t);
The matching y-formula with its own amplitude B and frequency b; the ratio between a and b determines the overall shape of the curve (circle, figure-eight, complex knot, etc.).
endShape();
Finishes drawing the curve without closing it back to the start.

drawRecursivePolygon()

This function demonstrates recursion in a visual, geometric way: it draws a polygon, then calls itself to draw a smaller, rotated polygon inside it, repeating until a base case (max depth or minimum size) stops the chain.

🔬 The comment hints you can vary the side count at each level. What happens if you change nestedSides to sides - 1, so each nested polygon has one fewer side (hexagon, pentagon, square, triangle...)?

  // Recursive call for nested polygons
  let nestedRadius = radius * shrinkFactor;
  let nestedSides = sides; // Can also vary sides here, e.g., sides - 1
  drawRecursivePolygon(0, 0, nestedRadius, nestedSides, depth + 1, maxDepth, shrinkFactor, rotationOffset, baseHue, sat, bright, alpha);
function drawRecursivePolygon(x, y, radius, sides, depth, maxDepth, shrinkFactor, rotationOffset, baseHue, sat, bright, alpha) {
  if (sides < 3 || depth > maxDepth || radius < 5) return; // Base cases for recursion

  push();
  translate(x, y);
  rotate(depth * rotationOffset); // Rotate based on depth

  // Calculate color for this depth
  let currentHue = (baseHue + depth * 30) % 360;
  let currentAlpha = map(depth, 0, maxDepth, alpha, alpha * 0.3); // Fade out deeper levels
  let currentStrokeWeight = map(depth, 0, maxDepth, 2, 0.5);

  fill(currentHue, sat, bright, currentAlpha);
  stroke((currentHue + 180) % 360, sat, bright, currentAlpha * 0.8);
  strokeWeight(currentStrokeWeight);

  beginShape();
  for (let a = 0; a < TWO_PI; a += TWO_PI / sides) {
    let sx = cos(a) * radius;
    let sy = sin(a) * radius;
    vertex(sx, sy);
  }
  endShape(CLOSE);
  noStroke();

  // Recursive call for nested polygons
  let nestedRadius = radius * shrinkFactor;
  let nestedSides = sides; // Can also vary sides here, e.g., sides - 1
  drawRecursivePolygon(0, 0, nestedRadius, nestedSides, depth + 1, maxDepth, shrinkFactor, rotationOffset, baseHue, sat, bright, alpha);
  pop();
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Recursion Stop Conditions if (sides < 3 || depth > maxDepth || radius < 5) return; // Base cases for recursion

Stops the recursion once the max depth is reached or the polygon becomes too small, preventing an infinite loop of ever-shrinking shapes.

for-loop Polygon Vertex Loop for (let a = 0; a < TWO_PI; a += TWO_PI / sides) {

Builds the current depth's polygon outline using the same trig technique as drawRegularPolygon.

calculation Self-Call For Next Depth drawRecursivePolygon(0, 0, nestedRadius, nestedSides, depth + 1, maxDepth, shrinkFactor, rotationOffset, baseHue, sat, bright, alpha);

Calls the function itself with a smaller radius and incremented depth, nesting a nested polygon inside the current one.

if (sides < 3 || depth > maxDepth || radius < 5) return; // Base cases for recursion
Without this stopping condition, the function would call itself forever - it exits once the polygon gets too small, too many sides are invalid, or the depth limit set by the GUI is reached.
rotate(depth * rotationOffset); // Rotate based on depth
Each nested level rotates a little more than the last, based on how deep it is, creating a twisting, spiral-like nesting effect.
let currentAlpha = map(depth, 0, maxDepth, alpha, alpha * 0.3); // Fade out deeper levels
Maps the current recursion depth to a transparency value, making deeper (smaller) nested polygons fade out for a sense of depth.
let nestedRadius = radius * shrinkFactor;
Shrinks the radius for the next nested polygon by multiplying it by a factor less than 1 (from the GUI), so each level is smaller than the last.
drawRecursivePolygon(0, 0, nestedRadius, nestedSides, depth + 1, maxDepth, shrinkFactor, rotationOffset, baseHue, sat, bright, alpha);
The recursive call: the function draws itself again at the origin (already translated/rotated by push/translate above) with a smaller radius and depth + 1, continuing until the base case stops it.

drawDynamicGrid()

This function builds a simple rotating, pulsing wireframe grid using two nested loops - one for horizontal lines, one for vertical - a common technique for procedural mesh or graph-paper effects.

🔬 This loop draws lines spanning exactly the grid's own width (-halfSize to halfSize). What happens if you double that span so each line extends beyond the grid's bounding box?

  // Horizontal lines
  for (let i = -density / 2; i <= density / 2; i++) {
    let yPos = i * step;
    line(-halfSize, yPos, halfSize, yPos);
  }
function drawDynamicGrid(x, y, size, density, rotationAngle, expansionFactor, baseHue, sat, bright, alpha, strokeWeightValue) {
  push();
  translate(x, y);
  rotate(rotationAngle);
  scale(1 + expansionFactor * 0.5); // Expand the grid

  stroke((baseHue + 100) % 360, sat, bright, alpha * 0.5);
  strokeWeight(strokeWeightValue);

  let step = size / density;
  let halfSize = size / 2;

  // Horizontal lines
  for (let i = -density / 2; i <= density / 2; i++) {
    let yPos = i * step;
    line(-halfSize, yPos, halfSize, yPos);
  }

  // Vertical lines
  for (let i = -density / 2; i <= density / 2; i++) {
    let xPos = i * step;
    line(xPos, -halfSize, xPos, halfSize);
  }

  pop();
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Grid Expansion Scale scale(1 + expansionFactor * 0.5); // Expand the grid

Scales the whole grid up or down based on an animated expansion factor, making it pulse in size.

for-loop Horizontal Lines Loop for (let i = -density / 2; i <= density / 2; i++) {

Draws evenly spaced horizontal lines across the grid's height.

for-loop Vertical Lines Loop for (let i = -density / 2; i <= density / 2; i++) {

Draws evenly spaced vertical lines across the grid's width, completing the mesh pattern.

scale(1 + expansionFactor * 0.5); // Expand the grid
Scales the entire grid uniformly; expansionFactor animates between 0 and 1 elsewhere, so this makes the grid gently grow and shrink over time.
let step = size / density;
Calculates the spacing between adjacent grid lines by dividing the total size by how many lines (density) should fit.
for (let i = -density / 2; i <= density / 2; i++) {
Loops from negative half the density to positive half, so lines are drawn symmetrically on both sides of the center.
let yPos = i * step;
Calculates this line's vertical position by multiplying its index by the step spacing.
line(-halfSize, yPos, halfSize, yPos);
Draws one horizontal line spanning the full width of the grid at height yPos.
let xPos = i * step;
Same idea as yPos but for the vertical lines' horizontal position.

drawKaleidoscopeShapes()

This is the heart of the sketch - it draws every shape for a single kaleidoscope wedge, reading dozens of values from the params object and animating almost all of them with sine/cosine waves tied to frameCount. Because draw() calls this function 12 times per frame (6 rotations x mirrored copy), whatever happens here gets multiplied into the full symmetric pattern.

🔬 This loop spreads pulseCount circles across a narrow PI/6 arc using map(j, 0, pulseCount, 0, PI/6). What happens if you change PI/6 to TWO_PI so the circles wrap all the way around the center instead of clustering in a thin arc?

  for (let j = 0; j < pulseCount; j++) {
    let angle = map(j, 0, pulseCount, 0, PI/6);
    let x = cos(angle) * pathRadius;
    let y = sin(angle) * pathRadius;
    fill((baseHue + j * 30 + pulseOffset * 10) % 360, sat * 0.9, bright * 1.1, alpha * 0.7);
    circle(x, y, pulseSize);
  }
function drawKaleidoscopeShapes() {
  // Base hue for the wedge, animated over time
  let baseHue = (hueOffset + 0) % 360;

  // Animate saturation and brightness slightly for more dynamic colors
  let sat = map(sin(frameCount * 0.01), -1, 1, params.saturationMin, params.saturationMax);
  let bright = map(cos(frameCount * 0.015), -1, 1, params.brightnessMin, params.brightnessMax);
  let alpha = params.overallAlpha; // General alpha for translucency

  // --- Subtle pulsating background effect (very transparent) ---
  let bgPulseSat = map(sin(frameCount * 0.008), -1, 1, 20, 40);
  let bgPulseBright = map(cos(frameCount * 0.01), -1, 1, 20, 40);
  let bgPulseHue = (baseHue + 180) % 360;
  fill(bgPulseHue, bgPulseSat, bgPulseBright, 20); // Very low alpha
  noStroke(); // Ensure no stroke for the background circle
  circle(0, 0, min(width, height) * 0.8);

  // --- Central glowing blob (multiple circles) ---
  push();
  fill((baseHue + 180) % 360, sat * 0.8, bright * 1.2, 50); // Slightly different color, brighter, more transparent
  circle(0, 0, map(sin(frameCount * 0.02), -1, 1, 10, 30));
  fill((baseHue + 210) % 360, sat * 0.7, bright * 1.1, 40);
  circle(0, 0, map(cos(frameCount * 0.015), -1, 1, 20, 50));
  pop();

  // --- Outer ring of polygons ---
  let outerDistance = map(sin(frameCount * 0.018), -1, 1, params.outerDistanceMin, params.outerDistanceMax);
  let outerSize = map(sin(frameCount * 0.03), -1, 1, params.outerSizeMin, params.outerSizeMax);
  let outerSides = floor(map(sin(frameCount * 0.02), -1, 1, params.outerSidesMin, params.outerSidesMax)); // Oscillate sides from triangle to octagon
  let outerRotation = frameCount * params.outerRotationSpeed;

  for (let j = 0; j < 3; j++) { // Draw 3 polygons in an arc
    let angle = map(j, 0, 2, -PI/8, PI/8); // Spread across a small arc
    let x = cos(angle) * outerDistance;
    let y = sin(angle) * outerDistance;
    fill((baseHue + j * 40) % 360, sat, bright, alpha);
    stroke((baseHue + j * 40 + 180) % 360, sat, bright, alpha * 0.8);
    strokeWeight(1.5);
    drawRegularPolygon(x, y, outerSize, outerSides, outerRotation + angle * 2);
  }
  noStroke();

  // --- Inner rotating stars ---
  let innerDistance = map(cos(frameCount * 0.022), -1, 1, params.innerDistanceMin, params.innerDistanceMax);
  let starPoints = floor(map(sin(frameCount * 0.025), -1, 1, params.innerStarPointsMin, params.innerStarPointsMax)); // Oscillate star points
  let innerRadius1 = map(cos(frameCount * 0.02), -1, 1, params.innerRadius1Min, params.innerRadius1Max);
  let innerRadius2 = innerRadius1 * 1.8; // Outer radius is larger
  let innerRotation = frameCount * params.innerRotationSpeed; // Rotate in opposite direction

  for (let j = 0; j < 2; j++) { // Draw 2 stars
    let angle = map(j, 0, 1, -PI/12, PI/12);
    let x = cos(angle) * innerDistance;
    let y = sin(angle) * innerDistance;
    fill((baseHue + 100 + j * 60) % 360, sat * 1.1, bright * 0.9, alpha * 0.9);
    stroke((baseHue + 100 + j * 60 + 180) % 360, sat * 1.1, bright * 0.9, alpha * 0.7);
    strokeWeight(1);
    drawStar(x, y, innerRadius1, innerRadius2, starPoints, innerRotation + angle * 3);
  }
  noStroke();

  // --- Dynamic pulsing circles along a path ---
  let pulseCount = floor(params.pulseCount);
  let pathRadius = map(sin(frameCount * 0.016), -1, 1, params.pathRadiusMin, params.pathRadiusMax);
  let pulseSize = map(sin(frameCount * 0.05), -1, 1, params.pulseSizeMin, params.pulseSizeMax);
  let pulseOffset = frameCount * params.pulseOffsetSpeed; // Animate movement along the path

  for (let j = 0; j < pulseCount; j++) {
    let angle = map(j, 0, pulseCount, 0, PI/6);
    let x = cos(angle) * pathRadius;
    let y = sin(angle) * pathRadius;
    fill((baseHue + j * 30 + pulseOffset * 10) % 360, sat * 0.9, bright * 1.1, alpha * 0.7);
    circle(x, y, pulseSize);
  }

  // --- Animated rectangle/polygon strip ---
  let stripDistance = map(cos(frameCount * 0.021), -1, 1, params.stripDistanceMin, params.stripDistanceMax);
  let stripLength = map(sin(frameCount * 0.04), -1, 1, params.stripLengthMin, params.stripLengthMax);
  let stripHeight = params.stripHeight;
  let stripOffset = map(cos(frameCount * 0.03), -1, 1, params.stripOffsetMin, params.stripOffsetMax);
  let stripRotation = frameCount * params.stripRotationSpeed;

  fill((baseHue + 150 + stripOffset) % 360, sat * 1.2, bright * 0.8, alpha);
  stroke((baseHue + 150 + stripOffset + 180) % 360, sat * 1.2, bright * 0.8, alpha * 0.6);
  strokeWeight(1);
  push();
  translate(stripDistance, stripOffset);
  rotate(stripRotation);
  rectMode(CENTER);
  rect(0, 0, stripLength, stripHeight);
  pop();
  noStroke();

  // --- Additional small rotating shapes ---
  let smallDistance = map(sin(frameCount * 0.025), -1, 1, params.smallDistanceMin, params.smallDistanceMax);
  let smallSize = map(sin(frameCount * 0.06), -1, 1, params.smallSizeMin, params.smallSizeMax);
  let smallRotation = frameCount * params.smallRotationSpeed;

  fill((baseHue + 250) % 360, sat, bright, alpha);
  stroke((baseHue + 250 + 180) % 360, sat, bright, alpha * 0.7);
  strokeWeight(1);
  push();
  translate(smallDistance, -smallDistance * 0.5);
  rotate(smallRotation);
  circle(0, 0, smallSize);
  pop();

  fill((baseHue + 300) % 360, sat * 0.9, bright * 1.1, alpha);
  stroke((baseHue + 300 + 180) % 360, sat * 0.9, bright * 1.1, alpha * 0.7);
  strokeWeight(1);
  push();
  translate(smallDistance * 1.2, smallDistance * 0.7);
  rotate(-smallRotation * 1.5);
  rectMode(CENTER);
  rect(0, 0, smallSize * 1.2, smallSize * 0.8);
  pop();
  noStroke();

  // --- Dynamic Spirograph Curve ---
  let spiroR = map(sin(frameCount * 0.009), -1, 1, params.spiroR_min, params.spiroR_max); // Fixed circle radius
  let spiror = map(cos(frameCount * 0.012), -1, 1, params.spiro_r_min, params.spiro_r_max); // Rolling circle radius
  let spirod = map(sin(frameCount * 0.015), -1, 1, params.spiro_d_min, params.spiro_d_max); // Pen distance
  let spiroAngle = frameCount * params.spiroRotationSpeed;
  let spiroHue = (baseHue + 75) % 360;

  fill(spiroHue, sat, bright, params.spiroFillAlpha); // Use a very low alpha for the fill
  stroke(spiroHue, sat, bright, params.spiroStrokeAlpha); // Use a higher alpha for the stroke to see the line
  strokeWeight(params.spiroStrokeWeight); // Make the line visible
  drawSpirograph(0, 0, spiroR, spiror, spirod, spiroAngle, 300);
  noStroke(); // Reset stroke for subsequent shapes

  // --- Fast Rotating Abstract Shape ---
  let abstractSize = map(sin(frameCount * 0.08), -1, 1, 10, 30);
  let abstractRotation = frameCount * 0.1; // Faster rotation
  let abstractHue = (baseHue + 330) % 360;

  fill(abstractHue, sat * 1.3, bright * 0.7, alpha * 0.8);
  stroke((abstractHue + 180) % 360, sat * 1.3, bright * 0.7, alpha * 0.6);
  strokeWeight(1);
  push();
  translate(min(width, height) / 5, -min(width, height) / 10);
  rotate(abstractRotation);
  // An abstract shape combining two overlapping circles
  circle(0, 0, abstractSize);
  circle(abstractSize * 0.5, 0, abstractSize * 0.8);
  pop();
  noStroke();

  // --- NEW: Dynamic Lissajous Curve ---
  let lissX = min(width, height) / 6;
  let lissY = -min(width, height) / 8;
  let lissA = params.lissajous_A;
  let lissB = params.lissajous_B;
  let liss_a = floor(map(sin(frameCount * 0.007), -1, 1, params.lissajous_a_min, params.lissajous_a_max));
  let liss_b = floor(map(cos(frameCount * 0.009), -1, 1, params.lissajous_b_min, params.lissajous_b_max));
  let liss_delta = frameCount * params.lissajous_delta_speed;
  let liss_rotation = frameCount * params.lissajous_rotation_speed;
  let lissHue = (baseHue + 220) % 360;

  fill(lissHue, sat, bright, params.lissajous_fill_alpha);
  stroke(lissHue, sat, bright, params.lissajous_stroke_alpha);
  strokeWeight(params.lissajous_stroke_weight);
  drawLissajous(lissX, lissY, lissA, lissB, liss_a, liss_b, liss_delta, liss_rotation, 400);
  noStroke();

  // --- NEW: Recursive/Nested Polygons ---
  let recursiveHue = (baseHue + params.recursivePolygonHueOffset) % 360;
  drawRecursivePolygon(
    params.recursivePolygonX,
    params.recursivePolygonY,
    params.recursivePolygonSize,
    params.recursivePolygonSides,
    0,
    params.recursivePolygonMaxDepth,
    params.recursivePolygonShrinkFactor,
    params.recursivePolygonRotationOffset,
    recursiveHue,
    sat,
    bright,
    alpha
  );
  noStroke();

  // --- NEW: Dynamic Grid ---
  let gridRotation = frameCount * params.dynamicGridRotationSpeed;
  let gridExpansion = map(sin(frameCount * params.dynamicGridExpansionSpeed), -1, 1, 0, 1);
  drawDynamicGrid(
    params.dynamicGridX,
    params.dynamicGridY,
    params.dynamicGridSize,
    params.dynamicGridDensity,
    gridRotation,
    gridExpansion,
    baseHue,
    sat,
    bright,
    alpha,
    params.dynamicGridStrokeWeight
  );
  noStroke();
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Background Pulse Circle circle(0, 0, min(width, height) * 0.8);

Draws a large, very transparent slowly-shifting circle behind everything for a subtle glow.

calculation Central Glowing Blob circle(0, 0, map(sin(frameCount * 0.02), -1, 1, 10, 30));

Layers two semi-transparent circles at the center that pulse in size to create a soft glowing core.

for-loop Outer Polygon Arc for (let j = 0; j < 3; j++) { // Draw 3 polygons in an arc

Draws 3 rotating regular polygons spread across a small arc, forming the outer ring.

for-loop Inner Star Pair for (let j = 0; j < 2; j++) { // Draw 2 stars

Draws 2 rotating star shapes closer to the center, spinning opposite to the outer polygons.

for-loop Pulsing Circle Path for (let j = 0; j < pulseCount; j++) {

Draws pulseCount small circles spread along an arc, each with a slightly shifted hue, creating a beaded trail.

calculation Spirograph Curve Draw drawSpirograph(0, 0, spiroR, spiror, spirod, spiroAngle, 300);

Draws the animated spirograph curve using continuously changing radii and pen distance.

calculation Lissajous Curve Draw drawLissajous(lissX, lissY, lissA, lissB, liss_a, liss_b, liss_delta, liss_rotation, 400);

Draws the animated Lissajous curve with slowly changing frequency and phase values.

calculation Recursive Polygon Draw drawRecursivePolygon(

Kicks off the recursive nested-polygon drawing using parameters from the GUI.

let baseHue = (hueOffset + 0) % 360;
Reads the globally animated hueOffset (updated once per frame in draw()) as the starting color for this whole wedge.
let sat = map(sin(frameCount * 0.01), -1, 1, params.saturationMin, params.saturationMax);
Uses a slow sine wave based on frameCount to oscillate saturation between the GUI-controlled min and max, adding gentle color variation over time.
circle(0, 0, min(width, height) * 0.8);
Draws a big, nearly-invisible circle (alpha 20) behind everything else to add a soft glowing backdrop.
let outerSides = floor(map(sin(frameCount * 0.02), -1, 1, params.outerSidesMin, params.outerSidesMax)); // Oscillate sides from triangle to octagon
Uses a sine wave to smoothly oscillate the number of sides of the outer polygons between the GUI's min and max, then floor() rounds it down to a whole number since a polygon can't have a fractional number of sides.
for (let j = 0; j < 3; j++) { // Draw 3 polygons in an arc
Repeats 3 times to draw a small cluster of polygons spread across a narrow arc rather than a single shape.
drawRegularPolygon(x, y, outerSize, outerSides, outerRotation + angle * 2);
Calls the reusable polygon helper with this wedge's computed position, size, side count and rotation.
for (let j = 0; j < pulseCount; j++) {
Loops pulseCount times (a GUI-controlled value) to draw that many small circles along the path.
let spiroR = map(sin(frameCount * 0.009), -1, 1, params.spiroR_min, params.spiroR_max); // Fixed circle radius
Animates the spirograph's fixed-circle radius smoothly between its GUI min and max using a slow sine wave.
drawSpirograph(0, 0, spiroR, spiror, spirod, spiroAngle, 300);
Draws the spirograph curve using the freshly calculated radii, pen distance and rotation, sampled at 300 points for smoothness.
drawLissajous(lissX, lissY, lissA, lissB, liss_a, liss_b, liss_delta, liss_rotation, 400);
Draws the Lissajous curve at a fixed offset position using frequencies that slowly change over time.
drawRecursivePolygon(
Kicks off the recursive nested-polygon shape, starting at depth 0 and letting the function call itself up to the GUI's max depth.
drawDynamicGrid(
Draws the rotating, expanding grid using its own position, size, density and animation speed, all sourced from the GUI.

windowResized()

windowResized() is a special p5.js function that automatically runs whenever the browser window changes size. Here it keeps the canvas full-screen and rebuilds the GUI so its sliders' ranges stay sensible for the new dimensions.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Re-initialize GUI if canvas size affects parameter ranges
  if (gui) {
    gui.destroy();
    setup(); // Re-run setup to re-create GUI with new width/height
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional GUI Existence Check if (gui) {

Only tries to destroy and rebuild the GUI if it was already created, avoiding errors on the very first call.

resizeCanvas(windowWidth, windowHeight);
p5.js's built-in function for changing the canvas size to match the new browser window dimensions after a resize.
if (gui) {
Checks that the dat.GUI panel already exists before trying to destroy it, to avoid a crash.
gui.destroy();
Removes the old GUI panel from the page so a fresh one can be created with ranges appropriate to the new canvas size.
setup(); // Re-run setup to re-create GUI with new width/height
Calls setup() again, which re-creates the canvas settings and rebuilds the entire GUI panel with folders sized to the new width/height.

📦 Key Variables

hueOffset number

Stores the continuously animated base hue (0-360) used to color every shape in the current frame, updated once per frame in draw().

let hueOffset = 0;
params object

A single object holding every tunable number in the sketch (speeds, sizes, distances, alphas, colors) - dat.GUI binds sliders directly to its properties so changing a slider immediately changes what gets drawn.

const params = { hueSpeed: 0.5, overallAlpha: 80, /* ...many more... */ };
gui object

Holds the dat.GUI panel instance so it can be destroyed and rebuilt in windowResized() when the browser window changes size.

let gui;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG windowResized()

Several params values (like outerDistanceMin, innerDistanceMax, dynamicGridX, etc.) are calculated once at script load time using windowWidth/windowHeight and stored as plain numbers in the params object. Calling setup() again on resize rebuilds the GUI but does not recompute these derived values, so shape distances stay based on the original window size even after the canvas resizes.

💡 Move the window-size-dependent calculations into a function (e.g. computeDerivedParams()) and call it both at the top of setup() and inside windowResized() before rebuilding the GUI, so distances stay proportional to the current canvas size.

PERFORMANCE draw() / drawKaleidoscopeShapes()

drawKaleidoscopeShapes() is called 12 times per frame (6 wedges x mirrored copy), and each call independently recomputes baseHue, sat, bright and every shape's animated values with fresh sin()/cos()/map() calls - work that is identical across all 12 calls within the same frame.

💡 Calculate the shared per-frame values (baseHue, sat, bright, and any other frameCount-derived numbers) once at the top of draw() and pass them into drawKaleidoscopeShapes() as arguments, cutting the redundant trigonometry roughly in half.

STYLE drawKaleidoscopeShapes()

Many animation speeds and arc angles are hardcoded magic numbers scattered through the function (e.g. frameCount * 0.008, PI/8, PI/12, frameCount * 0.1) instead of being part of the params object like almost everything else.

💡 Move these constants into params (e.g. params.abstractRotationSpeed) so every animated rate can be tuned live from the dat.GUI panel, keeping the level of live-configurability consistent throughout the sketch.

FEATURE sketch-wide

The sketch has no keyboard or mouse interaction - it is purely a passive, GUI-driven animation with no way to capture a moment or respond to input.

💡 Add a keyPressed() function that calls saveCanvas() to let viewers save their favorite kaleidoscope frame as an image, or a mousePressed() handler that randomizes a subset of params for instant new patterns.

🔄 Code Flow

Code flow showing setup, draw, drawregularpolygon, drawstar, drawspirograph, gcd, drawlissajous, drawrecursivepolygon, drawdynamicgrid, drawkaleidoscopeshapes, windowresized

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> symmetry-loop[Symmetry Loop] symmetry-loop --> drawregularpolygon[drawRegularPolygon] drawregularpolygon --> sides-guard[Sides Guard] sides-guard --> vertex-loop[Vertex Placement Loop] vertex-loop --> drawstar[drawStar] drawstar --> npoints-guard[NPoints Guard] npoints-guard --> star-vertex-loop[Star Vertex Loop] drawstar --> drawspirograph[drawSpirograph] drawspirograph --> radius-guard[Radius Guard] radius-guard --> spiro-curve-loop[Spirograph Curve Loop] drawspirograph --> drawlissajous[drawLissajous] drawlissajous --> lissajous-loop[Lissajous Loop] drawlissajous --> drawrecursivepolygon[drawRecursivePolygon] drawrecursivepolygon --> recursion-base-case[Recursion Base Case] recursion-base-case --> recursive-vertex-loop[Recursive Vertex Loop] recursive-vertex-loop --> recursive-call[Recursive Call] drawrecursivepolygon --> drawdynamicgrid[drawDynamicGrid] drawdynamicgrid --> grid-expand[Grid Expansion Scale] grid-expand --> horizontal-lines-loop[Horizontal Lines Loop] horizontal-lines-loop --> vertical-lines-loop[Vertical Lines Loop] drawdynamicgrid --> background-pulse[Background Pulse Circle] background-pulse --> central-blob[Central Glowing Blob] drawdynamicgrid --> drawkaleidoscopeshapes[drawKaleidoscopeShapes] drawkaleidoscopeshapes --> outer-polygon-loop[Outer Polygon Arc] outer-polygon-loop --> inner-star-loop[Inner Star Pair] inner-star-loop --> pulse-circle-loop[Pulsing Circle Path] drawkaleidoscopeshapes --> spirograph-call[Spirograph Curve Draw] spirograph-call --> lissajous-call[Lissajous Curve Draw] draw --> windowresized[windowResized] windowresized --> gui-check[GUI Existence Check] click setup href "#fn-setup" click draw href "#fn-draw" click symmetry-loop href "#sub-symmetry-loop" click drawregularpolygon href "#fn-drawregularpolygon" click sides-guard href "#sub-sides-guard" click vertex-loop href "#sub-vertex-loop" click drawstar href "#fn-drawstar" click npoints-guard href "#sub-npoints-guard" click star-vertex-loop href "#sub-star-vertex-loop" click drawspirograph href "#fn-drawspirograph" click radius-guard href "#sub-radius-guard" click spiro-curve-loop href "#sub-spiro-curve-loop" click drawlissajous href "#fn-drawlissajous" click lissajous-loop href "#sub-lissajous-loop" click drawrecursivepolygon href "#fn-drawrecursivepolygon" click recursion-base-case href "#sub-recursion-base-case" click recursive-vertex-loop href "#sub-recursive-vertex-loop" click recursive-call href "#sub-recursive-call" click drawdynamicgrid href "#fn-drawdynamicgrid" click grid-expand href "#sub-grid-expand" click horizontal-lines-loop href "#sub-horizontal-lines-loop" click vertical-lines-loop href "#sub-vertical-lines-loop" click background-pulse href "#sub-background-pulse" click central-blob href "#sub-central-blob" click drawkaleidoscopeshapes href "#fn-drawkaleidoscopeshapes" click outer-polygon-loop href "#sub-outer-polygon-loop" click inner-star-loop href "#sub-inner-star-loop" click pulse-circle-loop href "#sub-pulse-circle-loop" click spirograph-call href "#sub-spirograph-call" click lissajous-call href "#sub-lissajous-call" click windowresized href "#fn-windowresized" click gui-check href "#sub-gui-check"

❓ Frequently Asked Questions

What visual effects can I expect from the Kaleidoscope Symmetry sketch?

The sketch creates intricate geometric patterns that exhibit kaleidoscopic symmetry, featuring vibrant colors and dynamic shapes that evolve over time.

Is there any way for users to customize the visual output of the sketch?

Yes, users can interact with the sketch by adjusting parameters using the dat.gui interface, allowing them to change color, size, and rotation of various geometric elements.

What creative coding concepts are showcased in the Kaleidoscope Symmetry sketch?

This sketch demonstrates concepts such as procedural generation, symmetry, and animation in creative coding, utilizing shapes like stars, pulsing circles, and Lissajous curves.

Preview

Kaleidoscope Symmetry - xelsed.ai - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Kaleidoscope Symmetry - xelsed.ai - Code flow showing setup, draw, drawregularpolygon, drawstar, drawspirograph, gcd, drawlissajous, drawrecursivepolygon, drawdynamicgrid, drawkaleidoscopeshapes, windowresized
Code Flow Diagram