Kaleidoscope Symmetry - xelsed.ai

This sketch creates an animated kaleidoscope with 6-fold rotational symmetry, featuring multiple layers of geometric shapes including polygons, stars, spirographs, Lissajous curves, and recursive patterns. Each element animates independently with color shifts, scaling, and rotation to create a mesmerizing, continuously evolving mandala-like visual effect.

🎓 Concepts You'll Learn

Rotational symmetryTransform matrices (translate/rotate/scale)HSB color modeAnimation with frameCountParametric curves (spirograph, Lissajous)Recursiondat.gui parameter controlTrigonometric functionsDynamic shape generation

🔄 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 --> canvas-setup[Canvas and Color Mode Setup] setup --> gui-creation[dat.gui Initialization] gui-creation --> folder-creation[GUI Folder Organization] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-setup href "#sub-canvas-setup" click gui-creation href "#sub-gui-creation" click folder-creation href "#sub-folder-creation" draw --> background-clear[Frame Clearing] draw --> hue-animation[Hue Offset Animation] draw --> symmetry-loop[6-Fold Rotational Symmetry] symmetry-loop --> mirror-transform[Horizontal Mirror] symmetry-loop --> drawkaleidoscopeshapes[drawkaleidoscopeshapes] drawkaleidoscopeshapes --> outer-polygons[Outer Ring of Polygons] drawkaleidoscopeshapes --> inner-stars[Inner Rotating Stars] drawkaleidoscopeshapes --> pulsing-circles[Pulsing Circles Along Path] drawkaleidoscopeshapes --> spirograph-section[spirograph-section] drawkaleidoscopeshapes --> lissajous-section[lissajous-section] drawkaleidoscopeshapes --> recursive-section[recursive-section] drawkaleidoscopeshapes --> grid-section[grid-section] click draw href "#fn-draw" click background-clear href "#sub-background-clear" click hue-animation href "#sub-hue-animation" click symmetry-loop href "#sub-symmetry-loop" click mirror-transform href "#sub-mirror-transform" click drawkaleidoscopeshapes href "#fn-drawkaleidoscopeshapes" click outer-polygons href "#sub-outer-polygons" click inner-stars href "#sub-inner-stars" click pulsing-circles href "#sub-pulsing-circles" click spirograph-section href "#sub-spirograph-section" click lissajous-section href "#sub-lissajous-section" click recursive-section href "#sub-recursive-section" click grid-section href "#sub-grid-section" windowresized --> canvas-resize[Canvas Resizing] windowresized --> gui-recreation[GUI Destruction and Recreation] click windowresized href "#fn-windowresized" click canvas-resize href "#sub-canvas-resize" click gui-recreation href "#sub-gui-recreation"

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes the canvas size, sets the color mode to HSB (which makes it easier to create rainbow effects), and creates the interactive control panel using dat.gui. Each gui.add() call creates a slider that lets you adjust parameters in real-time without reloading the page.

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');
}

đź”§ Subcomponents:

function-call Canvas and Color Mode Setup createCanvas(windowWidth, windowHeight); colorMode(HSB, 360, 100, 100, 100);

Creates a full-window canvas and sets color mode to HSB (Hue, Saturation, Brightness) with ranges 0-360 for hue and 0-100 for saturation, brightness, and alpha

function-call dat.gui Initialization gui = new dat.GUI();

Creates an interactive control panel that allows real-time adjustment of all animation parameters

loop GUI Folder Organization const outerFolder = gui.addFolder('Outer Polygons');

Organizes related parameters into collapsible folders for better UI organization

Line by Line:

createCanvas(windowWidth, windowHeight)
Creates a canvas that fills the entire browser window, making the sketch responsive to window size
colorMode(HSB, 360, 100, 100, 100)
Switches from default RGB to HSB color mode where hue ranges 0-360 (like a color wheel), saturation 0-100, brightness 0-100, and alpha 0-100
noStroke()
Sets the default drawing mode to have no outline, only fills (can be overridden later with stroke() calls)
gui = new dat.GUI()
Creates a new dat.gui controller panel that will appear in the top-right corner of the screen
gui.add(params, 'hueSpeed', 0.1, 2.0).name('Hue Shift Speed')
Adds a slider to the GUI that controls hueSpeed parameter, with values between 0.1 and 2.0, labeled 'Hue Shift Speed'

draw()

draw() runs 60 times per second and creates the animation. The key to this kaleidoscope is the combination of rotation and mirroring: we rotate the canvas 6 times by 60 degrees each, and within each rotation, we draw shapes twice—once normally and once mirrored. This creates the characteristic kaleidoscope pattern with perfect symmetry. The hueOffset variable animates continuously, making colors shift through the rainbow.

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();
  }
}

đź”§ Subcomponents:

function-call Frame Clearing background(0)

Clears the canvas with black color each frame, preventing motion trails

function-call Origin Translation translate(width / 2, height / 2)

calculation Hue Offset Animation hueOffset = (frameCount * params.hueSpeed) % 360

Continuously cycles the base hue through the color spectrum (0-360) based on frame count and hueSpeed parameter

for-loop 6-Fold Rotational Symmetry for (let i = 0; i < 6; i++) { rotate(radians(60)); drawKaleidoscopeShapes(); }

Creates 6 identical wedges rotated 60 degrees apart, then mirrors each wedge to create 12-fold symmetry

transform Horizontal Mirror push(); scale(-1, 1); drawKaleidoscopeShapes(); pop();

Flips the shapes horizontally (scale -1 on x-axis) to create mirror symmetry within each wedge

Line by Line:

background(0)
Fills the entire canvas with black (hue 0 in HSB mode). This clears the previous frame so shapes don't leave trails
translate(width / 2, height / 2)
Moves the origin point from the top-left corner to the center of the canvas, making rotation and symmetry calculations easier
hueOffset = (frameCount * params.hueSpeed) % 360
Multiplies the current frame number by hueSpeed to create smooth color animation, then uses modulo (%) to keep the value between 0-360
for (let i = 0; i < 6; i++)
Loops 6 times to create 6 identical wedges around the center point
rotate(radians(60))
Rotates the entire coordinate system by 60 degrees (360/6). Each iteration rotates an additional 60 degrees
drawKaleidoscopeShapes()
Draws all the geometric shapes for one wedge at the current rotation angle
scale(-1, 1)
Flips the coordinate system horizontally (multiplies x by -1) to mirror the shapes without affecting y

drawRegularPolygon()

This function draws any regular polygon (triangle, square, pentagon, etc.) by calculating vertex positions around a circle. It uses trigonometry: cos(angle) gives the x-position and sin(angle) gives the y-position on a circle. By dividing the full circle (TWO_PI) into equal parts, we get evenly-spaced vertices that form a perfect regular polygon.

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();
}

đź”§ Subcomponents:

conditional Sides Validation if (sides < 3) return;

Prevents drawing invalid polygons with fewer than 3 sides

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

Loops around a full circle (TWO_PI radians), calculating vertex positions evenly spaced by dividing the circle into 'sides' equal parts

calculation Polar to Cartesian Conversion let sx = cos(a) * radius; let sy = sin(a) * radius;

Converts angle and radius into x,y coordinates using trigonometry (cosine for x, sine for y)

Line by Line:

if (sides < 3) return;
Checks if the polygon has at least 3 sides. If not, the function exits early to avoid errors
push();
Saves the current transformation state (position, rotation, scale) so changes don't affect other shapes
translate(x, y);
Moves the origin to position (x, y) where the polygon will be centered
rotate(rotationAngle);
Rotates the coordinate system by the specified angle in radians
beginShape();
Starts recording vertices to create a shape
for (let a = 0; a < TWO_PI; a += TWO_PI / sides)
Loops from 0 to 2Ď€ (full circle), incrementing by 2Ď€/sides. This creates evenly-spaced angles for each vertex
let sx = cos(a) * radius;
Calculates the x-coordinate of the vertex using cosine. The angle 'a' determines direction, radius determines distance
let sy = sin(a) * radius;
Calculates the y-coordinate using sine
vertex(sx, sy);
Adds this point to the shape being drawn
endShape(CLOSE);
Finishes the shape and automatically connects the last vertex back to the first to close the polygon
pop();
Restores the saved transformation state so subsequent shapes aren't affected by this polygon's transformations

drawStar()

A star is created by alternating between two different radii: outer points (radius2) and inner points (radius1). By placing inner points at half-angles between outer points, we create the characteristic star shape. The function loops through the circle once, but adds two vertices per iteration—one at the outer radius and one at the inner radius.

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();
}

đź”§ Subcomponents:

calculation Angle and Half-Angle Setup let angle = TWO_PI / npoints; let halfAngle = angle / 2.0;

Calculates the angle between star points and the half-angle for placing inner points between outer points

for-loop Alternating Radius Vertices for (let a = 0; a < TWO_PI; a += angle) { vertex at radius2; vertex at radius1; }

Alternates between outer radius (radius2) and inner radius (radius1) to create the star's pointed shape

Line by Line:

if (npoints < 3) return;
Validates that the star has at least 3 points
let angle = TWO_PI / npoints;
Divides the full circle by the number of points to get the angle between each outer point
let halfAngle = angle / 2.0;
Calculates half of that angle, which is where the inner points will be placed (between outer points)
for (let a = 0; a < TWO_PI; a += angle)
Loops around the circle, incrementing by the full angle between points (not half-angle)
let sx = cos(a) * radius2; let sy = sin(a) * radius2; vertex(sx, sy);
Calculates and adds an outer point at the current angle using the larger radius (radius2)
sx = cos(a + halfAngle) * radius1; sy = sin(a + halfAngle) * radius1; vertex(sx, sy);
Calculates and adds an inner point halfway between outer points using the smaller radius (radius1)

drawSpirograph()

A spirograph is a mathematical curve created by a point on a circle rolling inside another circle. The parameters are: R (fixed circle radius), r (rolling circle radius), and d (distance of pen from rolling circle center). The formula traces the path of the pen as the small circle rolls. The GCD (greatest common divisor) determines when the pattern repeats and closes.

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();
}

đź”§ Subcomponents:

for-loop Spirograph Point Generation for (let i = 0; i <= steps; i++)

Iterates through 'steps' number of points to trace the spirograph curve

calculation Parameter t Calculation let t = map(i, 0, steps, 0, TWO_PI * r / gcd(R, r));

Maps the loop counter to a parameter t that goes from 0 to the full period of the spirograph curve

calculation Spirograph Mathematical Formula let sx = (R - r) * cos(t) + d * cos(((R - r) / r) * t); let sy = (R - r) * sin(t) - d * sin(((R - r) / r) * t);

Implements the mathematical equations for a spirograph curve based on fixed circle radius R, rolling circle radius r, and pen distance d

Line by Line:

if (R <= 0 || r <= 0) return;
Validates that both radii are positive to avoid mathematical errors
let t = map(i, 0, steps, 0, TWO_PI * r / gcd(R, r));
Maps the loop counter (0 to steps) to a parameter t that goes from 0 to the complete spirograph period. The period is calculated using GCD to ensure the curve closes properly
let sx = (R - r) * cos(t) + d * cos(((R - r) / r) * t);
Calculates x-coordinate using the spirograph formula: the difference in radii creates the main circle motion, and the d term adds the pen offset
let sy = (R - r) * sin(t) - d * sin(((R - r) / r) * t);
Calculates y-coordinate similarly, with a negative sign on the d term to match the mathematical definition

gcd()

GCD (Greatest Common Divisor) is used in spirograph calculations to determine the complete period of the curve. The Euclidean algorithm is an efficient recursive method: keep replacing the larger number with the remainder of division until one number becomes 0. For example, gcd(12, 8) → gcd(8, 4) → gcd(4, 0) = 4.

function gcd(a, b) {
  return b === 0 ? a : gcd(b, a % b);
}

đź”§ Subcomponents:

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

When b reaches 0, returns a (the GCD). Otherwise, recursively calls gcd with swapped values

Line by Line:

return b === 0 ? a : gcd(b, a % b);
Uses the Euclidean algorithm: if b is 0, a is the GCD. Otherwise, recursively find GCD of b and (a modulo b). This efficiently finds the greatest common divisor

drawLissajous()

A Lissajous curve is created by combining two sine waves with different frequencies. The x-coordinate oscillates at frequency 'a' and the y-coordinate at frequency 'b'. When frequencies are different, you get complex looping patterns. The delta parameter creates a phase shift, and the ratio of frequencies determines the shape (2:1 creates a figure-8, 3:2 creates more complex patterns).

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();
}

đź”§ Subcomponents:

for-loop Lissajous Point Iteration for (let i = 0; i <= steps; i++)

Generates points along the Lissajous curve by iterating through the parameter space

calculation Lissajous Parametric Equations let sx = A * sin(a * t + delta); let sy = B * sin(b * t);

Implements the mathematical definition of a Lissajous curve using sine functions with different frequencies and amplitudes

Line by Line:

let t = map(i, 0, steps, 0, TWO_PI);
Maps the loop counter to a parameter t that goes from 0 to 2Ď€ (one complete cycle)
let sx = A * sin(a * t + delta);
Calculates x-coordinate as A (amplitude) times sine of (a times t plus delta). The frequency 'a' controls how many oscillations occur, delta creates a phase shift
let sy = B * sin(b * t);
Calculates y-coordinate as B times sine of (b times t). Different frequencies in x and y create complex patterns

drawRecursivePolygon()

Recursion is when a function calls itself. This function draws a polygon, then calls itself with a smaller radius to draw a polygon inside it, which calls itself again, and so on until the radius becomes too small. Each level has a different color and rotation, creating beautiful nested patterns. The key is having a base case (the if statement) that stops the recursion to prevent infinite loops.

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();
}

đź”§ Subcomponents:

conditional Recursion Base Cases if (sides < 3 || depth > maxDepth || radius < 5) return;

Stops recursion when polygon becomes invalid, max depth is reached, or radius becomes too small

calculation Depth-Based Color and Style let currentHue = (baseHue + depth * 30) % 360; let currentAlpha = map(depth, 0, maxDepth, alpha, alpha * 0.3);

Changes hue and alpha based on recursion depth, creating color variation and fade-out effect for nested polygons

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

Draws the polygon at the current recursion level

function-call Recursive Nested Polygon drawRecursivePolygon(0, 0, nestedRadius, nestedSides, depth + 1, maxDepth, shrinkFactor, rotationOffset, baseHue, sat, bright, alpha);

Calls itself with a smaller radius and increased depth to draw nested polygons inside

Line by Line:

if (sides < 3 || depth > maxDepth || radius < 5) return;
Three conditions stop the recursion: invalid polygon, exceeded max depth, or radius too small to draw
rotate(depth * rotationOffset);
Rotates each nested polygon by an additional amount based on its depth, creating a spiral effect
let currentHue = (baseHue + depth * 30) % 360;
Shifts the hue by 30 degrees for each depth level, creating a rainbow effect from center outward
let currentAlpha = map(depth, 0, maxDepth, alpha, alpha * 0.3);
Gradually reduces opacity as depth increases, making nested polygons more transparent
let nestedRadius = radius * shrinkFactor;
Calculates the radius for the next nested polygon by multiplying by shrinkFactor (e.g., 0.7 makes each nested polygon 70% of the previous size)
drawRecursivePolygon(0, 0, nestedRadius, nestedSides, depth + 1, maxDepth, shrinkFactor, rotationOffset, baseHue, sat, bright, alpha);
Calls itself with the new smaller radius and incremented depth, creating nested polygons inside the current one

drawDynamicGrid()

This function creates a grid of lines that can rotate and expand. The grid is centered at the origin and extends from -halfSize to +halfSize in both directions. The density parameter controls how many lines appear, and the expansionFactor makes the grid grow or shrink over time. Horizontal and vertical loops create perpendicular lines.

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();
}

đź”§ Subcomponents:

calculation Dynamic Grid Scaling scale(1 + expansionFactor * 0.5);

Scales the grid based on expansionFactor, making it grow and shrink dynamically

for-loop Horizontal Line Generation for (let i = -density / 2; i <= density / 2; i++)

Draws horizontal lines spaced evenly across the grid

for-loop Vertical Line Generation for (let i = -density / 2; i <= density / 2; i++)

Draws vertical lines spaced evenly across the grid

Line by Line:

scale(1 + expansionFactor * 0.5);
Multiplies all coordinates by this scale factor. When expansionFactor is positive, the grid grows; when negative, it shrinks
let step = size / density;
Calculates the spacing between grid lines by dividing the total size by the density (number of lines)
let halfSize = size / 2;
Calculates half the grid size to center the grid at the origin (lines go from -halfSize to +halfSize)
for (let i = -density / 2; i <= density / 2; i++)
Loops from negative to positive density/2, centering the grid lines around the origin
let yPos = i * step;
Calculates the y-position of each horizontal line by multiplying the loop counter by the step spacing
line(-halfSize, yPos, halfSize, yPos);
Draws a horizontal line from left edge to right edge at the calculated y position

drawKaleidoscopeShapes()

This is the main function that creates all the visual elements within one kaleidoscope wedge. It's called twice per rotation in the draw() function (once normally and once mirrored). The function combines multiple animation techniques: sine/cosine waves for smooth color and size changes, loops to arrange shapes in patterns, and calls to helper functions like drawSpirograph() and drawLissajous(). Every element animates independently at different rates, creating the complex, mesmerizing effect.

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));
  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));
  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;

  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;

  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);
  let spiror = map(cos(frameCount * 0.012), -1, 1, params.spiro_r_min, params.spiro_r_max);
  let spirod = map(sin(frameCount * 0.015), -1, 1, params.spiro_d_min, params.spiro_d_max);
  let spiroAngle = frameCount * params.spiroRotationSpeed;
  let spiroHue = (baseHue + 75) % 360;

  fill(spiroHue, sat, bright, params.spiroFillAlpha);
  stroke(spiroHue, sat, bright, params.spiroStrokeAlpha);
  strokeWeight(params.spiroStrokeWeight);
  drawSpirograph(0, 0, spiroR, spiror, spirod, spiroAngle, 300);
  noStroke();

  // --- Fast Rotating Abstract Shape ---
  let abstractSize = map(sin(frameCount * 0.08), -1, 1, 10, 30);
  let abstractRotation = frameCount * 0.1;
  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);
  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();
}

đź”§ Subcomponents:

calculation Color and Brightness Animation let baseHue = (hueOffset + 0) % 360; let sat = map(sin(frameCount * 0.01), -1, 1, params.saturationMin, params.saturationMax);

Animates the base hue and saturation using sine/cosine functions to create smooth color transitions

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

Draws a large semi-transparent circle that pulses with the animation

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

Creates multiple overlapping circles at the center that pulse at different rates

for-loop Outer Ring of Polygons for (let j = 0; j < 3; j++)

Draws 3 polygons arranged in an arc, each with different colors and rotation

for-loop Inner Rotating Stars for (let j = 0; j < 2; j++)

Draws 2 stars that rotate and pulse

for-loop Pulsing Circles Along Path for (let j = 0; j < pulseCount; j++)

Creates multiple small circles arranged in an arc that pulse and change color

calculation Animated Rectangle Strip rect(0, 0, stripLength, stripHeight);

Draws a rotating rectangle that moves and scales dynamically

calculation Small Rotating Shapes circle(0, 0, smallSize); rect(0, 0, smallSize * 1.2, smallSize * 0.8);

Draws small circles and rectangles that rotate at different speeds

function-call Spirograph Curve drawSpirograph(0, 0, spiroR, spiror, spirod, spiroAngle, 300);

Draws an animated spirograph curve with dynamically changing parameters

calculation Abstract Overlapping Circles circle(0, 0, abstractSize); circle(abstractSize * 0.5, 0, abstractSize * 0.8);

Draws two overlapping circles that rotate and pulse quickly

function-call Lissajous Curve drawLissajous(lissX, lissY, lissA, lissB, liss_a, liss_b, liss_delta, liss_rotation, 400);

Draws an animated Lissajous curve with changing frequencies

function-call Recursive Nested Polygons drawRecursivePolygon(...);

Draws nested polygons that shrink and rotate at each level

function-call Dynamic Rotating Grid drawDynamicGrid(...);

Draws a grid of lines that rotates and expands dynamically

Line by Line:

let baseHue = (hueOffset + 0) % 360;
Sets the base hue for this wedge using the animated hueOffset from the main draw() function
let sat = map(sin(frameCount * 0.01), -1, 1, params.saturationMin, params.saturationMax);
Animates saturation by mapping a sine wave (which oscillates between -1 and 1) to the saturation range
let bright = map(cos(frameCount * 0.015), -1, 1, params.brightnessMin, params.brightnessMax);
Animates brightness using cosine at a slightly different rate than saturation, creating independent color changes
circle(0, 0, min(width, height) * 0.8);
Draws a large semi-transparent circle that serves as a subtle pulsating background
for (let j = 0; j < 3; j++)
Loops 3 times to draw 3 polygons in the outer ring
let angle = map(j, 0, 2, -PI/8, PI/8);
Spreads the 3 polygons across a small arc from -22.5 degrees to +22.5 degrees
let x = cos(angle) * outerDistance; let y = sin(angle) * outerDistance;
Converts angle and distance to x,y coordinates using trigonometry
drawRegularPolygon(x, y, outerSize, outerSides, outerRotation + angle * 2);
Draws a polygon at the calculated position with rotation that combines the global rotation and the angle offset

windowResized()

windowResized() is a special p5.js function that automatically runs whenever the browser window is resized. This sketch uses it to make the canvas responsive—it grows or shrinks to fill the window. The GUI is also recreated because many parameters depend on canvas size (like outerDistanceMin which is calculated as min(width, height) / 3.5).

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
  }
}

đź”§ Subcomponents:

function-call Canvas Resizing resizeCanvas(windowWidth, windowHeight);

Resizes the canvas to match the new window dimensions

conditional GUI Destruction and Recreation if (gui) { gui.destroy(); setup(); }

Destroys the old GUI and recreates it so parameter ranges adjust to the new canvas size

Line by Line:

resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to fill the entire window when the browser window is resized
if (gui)
Checks if the GUI exists before trying to destroy it
gui.destroy();
Removes the old GUI panel from the page
setup();
Calls setup() again to recreate the GUI with updated parameter ranges based on the new canvas size

📦 Key Variables

hueOffset number

Stores the animated base hue value that cycles through 0-360 to create the rainbow color shift effect

let hueOffset = 0;
params object

Contains all adjustable parameters for the sketch, organized by feature (outer polygons, inner stars, spirograph, etc.). These values are controlled by the dat.gui interface

const params = { hueSpeed: 0.5, outerDistanceMin: ..., ... };
gui object

Stores the dat.gui controller object that creates the interactive control panel for adjusting parameters in real-time

let gui; // Declared globally, initialized in setup()

đź§Ş Try This!

Experiment with the code by making these changes:

  1. Change params.hueSpeed from 0.5 to 2.0 in the setup() function to make colors shift much faster through the rainbow
  2. Modify params.outerSidesMin and params.outerSidesMax to change the polygon shapes—try values like 5 and 6 to create pentagons and hexagons instead of triangles and octagons
  3. Increase params.recursivePolygonMaxDepth from 3 to 5 to create more nested layers of polygons, and adjust params.recursivePolygonShrinkFactor to 0.8 to make each layer larger
  4. Change the scale factor in drawDynamicGrid() from 1 + expansionFactor * 0.5 to 1 + expansionFactor * 2 to make the grid expand and contract more dramatically
  5. Modify the number of symmetry wedges in draw() from 6 to 8 by changing the loop condition to i < 8 and rotate(radians(45)) to create 8-fold symmetry instead of 6-fold
  6. Adjust params.spiroStrokeWeight from 2 to 5 to make the spirograph curves much thicker and more visible
  7. Change params.pulseCount from 5 to 10 to create more pulsing circles along the path, and adjust params.pulseSizeMin and Max to make them larger or smaller
Open in Editor & Experiment →

đź”§ Potential Improvements

Here are some ways this code could be enhanced:

PERFORMANCE windowResized() function

Calling setup() on every window resize recreates the entire GUI, which is expensive. If the user resizes the window frequently, this causes lag

đź’ˇ Instead of destroying and recreating the GUI, just update the parameter ranges: outerFolder.controllers[0].max(min(width, height) / 2) to adjust slider ranges without rebuilding

BUG drawRecursivePolygon() function

The recursion check uses radius < 5 as a hard limit, but with very large initial sizes, this might create too many recursion levels before hitting the limit, potentially causing performance issues

đź’ˇ Add a check for radius < 1 or use a stricter shrinkFactor validation to prevent excessive recursion depth

STYLE drawKaleidoscopeShapes() function

The function is very long (400+ lines) with many similar animation patterns (map(sin/cos(frameCount * speed), -1, 1, min, max)). This makes it hard to maintain and modify

đź’ˇ Create a helper function like animateValue(speed, min, max, useSin=true) to reduce repetition and make the code more readable

FEATURE setup() function

The GUI folders are created but not organized logically—all general controls are at the top level, making the interface cluttered

đź’ˇ Create a 'General' folder and move hueSpeed, overallAlpha, saturationMin/Max, brightnessMin/Max into it for better organization

BUG drawSpirograph() function

The function doesn't validate that r is not equal to 0 before calculating gcd(R, r), which could cause issues if r becomes 0 from animation

đź’ˇ Change the validation to: if (R <= 0 || r <= 0 || r > R) return; to ensure valid spirograph parameters

PERFORMANCE drawKaleidoscopeShapes() function

Multiple calls to map() with sin/cos are computed every frame, even for parameters that might not be visible or important

đź’ˇ Pre-calculate frequently used values like sat and bright once per frame in draw() and pass them as parameters rather than recalculating in drawKaleidoscopeShapes()

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