AI Kaleidoscope Generator - Symmetrical Pattern Art Draw mesmerizing symmetrical patterns with real

This sketch turns your mouse or finger movements into a glowing, rotating kaleidoscope. Every stroke you draw is mirrored across multiple symmetry axes in real time, wrapped in a jewel-toned radial gradient, and colored with an auto-generated harmonious palette that can be reshuffled on demand.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the spin — Increasing rotationSpeed makes the whole kaleidoscope whirl noticeably faster.
  2. Add more symmetry axes — Raising symmetryCount packs more mirrored copies of your stroke into the pattern, creating a denser mandala.
  3. Always show crystallized dots — Forcing isCrystallized to true makes every stroke render as glowing points instead of connected lines.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates an interactive kaleidoscope: whatever line you drag out with your mouse (or finger) is instantly duplicated and reflected around a rotating center, producing a symmetrical mandala-like pattern. It leans on p5's WEBGL render mode for 3D-style transforms, HSB color mode for smooth palette generation, push()/pop()/rotateZ() for the radial symmetry, and the raw drawingContext (the underlying HTML5 canvas API) for a glowing radial gradient and drop-shadow effects that p5 doesn't expose directly.

The code is organized around a single growing array of points (drawingPoints) that records your stroke, and a settings object wired up to a dat.GUI panel so you can tweak symmetry count, rotation speed, color cycling, and more without touching code. By studying it you'll learn how to build radial symmetry with a rotation loop, how to mix p5.js with the native canvas 2D context for gradients and shadows, and how to drive an external GUI library from a plain JavaScript settings object.

⚙️ How It Works

  1. On load, setup() creates a full-window WEBGL canvas, switches to HSB color mode, generates a random harmonious color palette, and builds a dat.GUI control panel linked to the settings object.
  2. Every frame, draw() clears the screen, paints a glowing radial gradient behind everything, then applies a slow global rotation driven by frameCount and settings.rotationSpeed (plus device tilt if available).
  3. draw() then loops settings.symmetryCount times, rotating a little further each iteration and calling drawStroke() twice per slice - once normally and once after flipping vertically with scale(1, -1) - so your single stroke is mirrored into a full radial kaleidoscope.
  4. When you press and drag the mouse (or touch and move on mobile), mousePressed() and mouseDragged() append points to the drawingPoints array in real time, which is what drawStroke() renders each frame.
  5. drawStroke() colors each point or line segment by cycling through the generated palette based on frameCount and position, and adds a soft shadow glow via drawingContext for a neon effect.
  6. Every 500 recorded points, or whenever you click 'AI Suggest Palette' / 'Clear Canvas' in the GUI, generatePalette() picks a new base hue and builds a fresh set of harmonious colors, instantly changing the kaleidoscope's color scheme.

🎓 Concepts You'll Learn

WEBGL rendering mode in p5.jsHSB color mode and palette generationRadial symmetry via rotateZ() and scale()dat.GUI for live parameter controlsCanvas 2D gradients and shadows via drawingContextDevice orientation input for mobileMouse/touch drawing and state arrays

📝 Code Breakdown

setup()

setup() runs once and is the perfect place to configure rendering mode, color mode, and build any external UI like dat.GUI before the animation loop begins.

function setup() {
  // Create a canvas in WEBGL mode for better gradient and glow effects
  createCanvas(windowWidth, windowHeight, WEBGL);

  // Use HSB color mode for easier color harmony generation and cycling
  colorMode(HSB, 360, 100, 100, 100);

  // Set angle mode to RADIANS for rotate() function
  angleMode(RADIANS);

  // No fill for strokes
  noFill();

  // Initial stroke weight
  strokeWeight(2);

  // Generate an initial color palette
  generatePalette();

  // Initialize dat.GUI
  gui = new dat.GUI();
  gui.add(settings, 'symmetryCount', [6, 8, 12]).name('Symmetry Axes');
  gui.add(settings, 'rotationSpeed', 0, 0.02).name('Rotation Speed');
  gui.add(settings, 'colorCyclingSpeed', 0, 0.1).name('Color Cycle Speed');
  gui.add(settings, 'showSymmetryLines').name('Show Symmetry Lines');
  gui.add(settings, 'isCrystallized').name('Crystallize Pattern');
  gui.add(settings, 'clearCanvas').name('Clear Canvas');
  gui.add(settings, 'suggestPalette').name('AI Suggest Palette'); // Manual trigger for AI suggestion

  // Request device access for motion sensors (for mobile devices)
  if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
    DeviceOrientationEvent.requestPermission()
      .then(permissionState => {
        if (permissionState === 'granted') {
          console.log('Device orientation permission granted.');
        } else {
          console.log('Device orientation permission denied.');
        }
      })
      .catch(console.error);
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Device Orientation Permission Request if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {

Checks if the browser supports iOS-style motion sensor permission and requests access if so

createCanvas(windowWidth, windowHeight, WEBGL);
Creates a full-window canvas using WEBGL mode, which enables 3D-style transforms and better gradient/glow rendering than the default 2D mode
colorMode(HSB, 360, 100, 100, 100);
Switches p5's color system to Hue-Saturation-Brightness with ranges 0-360 for hue and 0-100 for saturation, brightness and alpha - much easier for generating harmonious palettes than RGB
angleMode(RADIANS);
Sets rotation functions like rotateZ() to expect radians instead of degrees, matching p5's TWO_PI constant used later
noFill();
Disables shape fill so only strokes (outlines/points) are visible - important since we're drawing lines and points, not solid shapes
strokeWeight(2);
Sets the default thickness of lines and points drawn afterward to 2 pixels
generatePalette();
Calls the custom function that builds the initial set of harmonious colors used to paint the kaleidoscope
gui = new dat.GUI();
Creates a dat.GUI control panel instance that will appear in the corner of the screen
gui.add(settings, 'symmetryCount', [6, 8, 12]).name('Symmetry Axes');
Adds a dropdown control to the GUI that lets the user pick 6, 8, or 12 symmetry axes, directly editing settings.symmetryCount
DeviceOrientationEvent.requestPermission()
On iOS devices, motion sensor access requires explicit permission - this asks the browser for that permission

draw()

draw() runs 60 times per second. Here it combines a global slow rotation with a per-slice loop that uses push()/pop() to isolate each rotated copy's transform, a core technique for any radial/mandala pattern in p5.js.

🔬 This block draws your stroke, then flips it and draws it again to create a mirror within each slice. What happens if you comment out the scale(1, -1) line and the second drawStroke() call - does the pattern still look symmetrical?

    // Rotate for each symmetry slice
    rotateZ(i * angle);

    // Draw the strokes
    drawStroke(drawingPoints, settings.isCrystallized);

    // Apply horizontal reflection (scale Y by -1)
    scale(1, -1);

    // Draw the reflected strokes
    drawStroke(drawingPoints, settings.isCrystallized);
function draw() {
  // Always clear the background in WEBGL mode
  background(0);

  // Translate to the center of the canvas in 3D space and move slightly back
  // This allows for better perspective and viewing of 2D shapes in WEBGL
  translate(0, 0, -min(width, height) / 4);

  // Draw the radial gradient background
  drawRadialGradient();

  // Apply global rotation based on settings and device sensors
  // Use deviceRotationX and deviceRotationY for subtle device-based rotation
  rotateZ(frameCount * settings.rotationSpeed + deviceRotationY * 0.005);
  rotateX(deviceRotationX * 0.005);

  // Calculate the angle for each symmetry slice
  let angle = TWO_PI / settings.symmetryCount;

  // Draw all symmetrical reflections
  for (let i = 0; i < settings.symmetryCount; i++) {
    push(); // Save the current transformation state

    // Rotate for each symmetry slice
    rotateZ(i * angle);

    // Draw the strokes
    drawStroke(drawingPoints, settings.isCrystallized);

    // Apply horizontal reflection (scale Y by -1)
    scale(1, -1);

    // Draw the reflected strokes
    drawStroke(drawingPoints, settings.isCrystallized);

    pop(); // Restore the previous transformation state
  }

  // Draw symmetry lines if enabled
  if (settings.showSymmetryLines) {
    drawSymmetryLines();
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Symmetry Reflection Loop for (let i = 0; i < settings.symmetryCount; i++) {

Rotates the canvas a bit further each pass and draws the stroke plus its vertically-flipped mirror, building the full kaleidoscope pattern

conditional Show Symmetry Lines Toggle if (settings.showSymmetryLines) {

Optionally draws faint guide lines along each symmetry axis when the GUI checkbox is enabled

background(0);
Clears the entire canvas to black every frame - required in WEBGL mode since it doesn't auto-clear like 2D mode
translate(0, 0, -min(width, height) / 4);
Pushes the drawing plane slightly back in 3D space, giving a subtle sense of depth even though we're drawing flat shapes
drawRadialGradient();
Paints the glowing jewel-toned background gradient behind the pattern
rotateZ(frameCount * settings.rotationSpeed + deviceRotationY * 0.005);
Continuously rotates the whole scene around the Z axis based on elapsed frames and rotation speed, with a tiny nudge from device tilt if on mobile
let angle = TWO_PI / settings.symmetryCount;
Divides a full circle (TWO_PI radians) into equal slices - one per symmetry axis
rotateZ(i * angle);
Rotates the canvas to the i-th slice position before drawing, so each copy of the stroke appears at a different angle around the circle
scale(1, -1);
Flips the coordinate system vertically (mirrors across the X axis), so the second drawStroke() call draws a mirrored copy of the same points - this is what creates the kaleidoscope's mirror symmetry within each slice
if (settings.showSymmetryLines) {
Only draws the faint reference lines radiating from the center if the user has enabled that GUI checkbox

drawStroke()

drawStroke() shows how to combine p5.js's beginShape()/vertex()/endShape() shape-building API with raw canvas context properties (shadowColor, shadowBlur) that p5 doesn't wrap directly - a common pattern for adding effects p5 doesn't natively support.

🔬 This branch only runs in 'Crystallize Pattern' mode, drawing dots instead of connected lines. What happens visually if you force this branch to always run by changing the condition to just 'true'?

  if (crystallized || points.length < 2) {
    beginShape(POINTS);
    for (let point of points) {
      let col = palette[floor(frameCount * settings.colorCyclingSpeed + points.indexOf(point)) % palette.length];
function drawStroke(points, crystallized) {
  if (points.length === 0) return;

  // Use beginShape() and endShape() for drawing connected lines or curves
  // WEBGL mode requires specifying a shape type (e.g., LINES, POINTS, TRIANGLES, etc.)
  // For strokes, we'll use a series of short lines or curves.
  // Using POINTS or LINES within a single beginShape is more efficient in WEBGL.

  // Drawing points as individual points is simpler in WEBGL
  if (crystallized || points.length < 2) {
    beginShape(POINTS);
    for (let point of points) {
      let col = palette[floor(frameCount * settings.colorCyclingSpeed + points.indexOf(point)) % palette.length];
      stroke(col);
      // Apply subtle glow effect
      drawingContext.shadowColor = color(hue(col), saturation(col), brightness(col), 50).toString();
      drawingContext.shadowBlur = 10;
      vertex(point.x, point.y);
    }
    endShape();
  } else {
    // For smooth curves, we can draw a series of short lines between points
    // or use curveVertex() but that often looks chunky in WEBGL without custom shaders.
    // Let's try drawing simple lines between points for a smoother appearance.
    beginShape(LINES);
    for (let i = 0; i < points.length - 1; i++) {
      let p1 = points[i];
      let p2 = points[i+1];
      let col = palette[floor(frameCount * settings.colorCyclingSpeed + i) % palette.length];
      stroke(col);
      drawingContext.shadowColor = color(hue(col), saturation(col), brightness(col), 50).toString();
      drawingContext.shadowBlur = 10;
      vertex(p1.x, p1.y);
      vertex(p2.x, p2.y);
    }
    endShape();
  }
  drawingContext.shadowBlur = 0; // Reset shadow blur
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Points vs Lines Mode if (crystallized || points.length < 2) {

Decides whether to draw the stroke as individual glowing dots (crystallized mode) or as connected line segments

for-loop Draw Each Point for (let point of points) {

Colors and places a vertex for every recorded point when in crystallized/point mode

for-loop Draw Line Segments for (let i = 0; i < points.length - 1; i++) {

Connects each consecutive pair of points with a colored, glowing line segment

if (points.length === 0) return;
Exits early if there's nothing drawn yet, avoiding errors from trying to render an empty shape
if (crystallized || points.length < 2) {
beginShape(POINTS);
Starts a WEBGL shape that will render each vertex as a standalone point rather than connecting them with lines
let col = palette[floor(frameCount * settings.colorCyclingSpeed + points.indexOf(point)) % palette.length];
Picks a color from the palette array based on both the current frame and this point's position, so colors slowly shift and cycle over time
drawingContext.shadowColor = color(hue(col), saturation(col), brightness(col), 50).toString();
Reaches into the raw HTML5 canvas context to set a colored shadow, since p5.js doesn't have a built-in glow function
drawingContext.shadowBlur = 10;
Blurs that shadow by 10 pixels, creating the soft neon glow around each point or line
vertex(point.x, point.y);
Adds this point's coordinates as a vertex in the current shape
beginShape(LINES);
Starts a shape mode where every pair of vertices becomes a separate line segment - used for the smoother connected-stroke look
drawingContext.shadowBlur = 0; // Reset shadow blur
Turns off the glow after finishing, so it doesn't accidentally affect other things drawn later in the frame

drawSymmetryLines()

This function demonstrates how push()/pop() lets you rotate temporarily to draw one line, then reset back to the original orientation for the next - essential for drawing multiple rotated copies of anything.

function drawSymmetryLines() {
  let angle = TWO_PI / settings.symmetryCount;
  let radius = min(width, height) / 2; // Radius to draw lines to

  for (let i = 0; i < settings.symmetryCount; i++) {
    push();
    rotateZ(i * angle);
    stroke(255, 50); // Semi-transparent white lines
    line(0, 0, radius, 0); // Draw line from center to edge
    pop();
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Draw Each Guide Line for (let i = 0; i < settings.symmetryCount; i++) {

Rotates to each symmetry axis angle and draws a faint reference line from center to edge

let angle = TWO_PI / settings.symmetryCount;
Same slice-angle calculation as in draw(), used here to space out the guide lines evenly
let radius = min(width, height) / 2;
Uses half the smaller canvas dimension so the lines always reach the edge regardless of window shape
rotateZ(i * angle);
Rotates to this axis's angle before drawing its guide line
stroke(255, 50);
Sets a semi-transparent white stroke color so the lines are visible but subtle
line(0, 0, radius, 0);
Draws a straight line from the center (0,0) out to the edge along the current rotated X axis

generatePalette()

generatePalette() shows a simple algorithmic approach to color theory: pick a base hue, then derive complementary and analogous hues mathematically using modulo arithmetic to wrap around the 360-degree color wheel.

🔬 These three lines create a dark-to-bright gradient of the same hue. What happens if you change the saturation values (the second number) to 100 for all three - does the palette look more vivid or more washed out?

  palette.push(color(baseHue, 80, 90)); // Bright jewel tone
  palette.push(color(baseHue, 60, 70)); // Slightly darker
  palette.push(color(baseHue, 40, 50)); // Even darker
function generatePalette() {
  palette = [];
  // Pick a random base hue (0-360)
  let baseHue = random(360);

  // Generate a harmonious palette (e.g., analogous or complementary)
  // For this example, let's create a triad palette with variations.
  palette.push(color(baseHue, 80, 90)); // Bright jewel tone
  palette.push(color(baseHue, 60, 70)); // Slightly darker
  palette.push(color(baseHue, 40, 50)); // Even darker

  // Add a complementary hue for contrast
  let complementaryHue = (baseHue + 180) % 360;
  palette.push(color(complementaryHue, 80, 90));

  // Add analogous hues
  palette.push(color((baseHue + 30) % 360, 70, 80));
  palette.push(color((baseHue - 30 + 360) % 360, 70, 80));

  // Shuffle the palette for more varied color cycling
  palette = shuffle(palette);

  console.log('New color palette generated:', palette.map(c => c.toString()));
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Build Harmonious Colors palette.push(color(baseHue, 80, 90));

Adds a series of related colors (variations of the base hue plus a complementary and analogous hues) to build a pleasing palette

let baseHue = random(360);
Picks a random starting hue anywhere on the 0-360 color wheel using HSB mode
palette.push(color(baseHue, 80, 90));
Adds a bright, highly saturated version of the base hue to the palette array
let complementaryHue = (baseHue + 180) % 360;
Calculates the hue directly opposite the base hue on the color wheel for visual contrast, wrapping around with % 360
palette.push(color((baseHue + 30) % 360, 70, 80));
Adds a hue 30 degrees away from the base for an analogous (closely related) color
palette = shuffle(palette);
Randomly reorders the palette array using p5's shuffle() so the color cycling doesn't always follow the same hue progression

drawRadialGradient()

This function demonstrates dropping down to the native HTML5 Canvas 2D API (via p5's drawingContext) to achieve effects like gradients that aren't built into p5.js directly - a useful escape hatch for advanced visuals.

function drawRadialGradient() {
  // In WEBGL mode, we can use drawingContext to create gradients on the 2D canvas context
  let gradient = drawingContext.createRadialGradient(0, 0, 0, 0, 0, min(width, height) / 2);

  // Jewel-toned gradient from center to edge
  gradient.addColorStop(0, color(0, 0, 0, 100).toString()); // Center: Dark, fully opaque
  gradient.addColorStop(0.3, color(250, 50, 15, 80).toString()); // Inner: Subtle jewel tone
  gradient.addColorStop(0.7, color(200, 50, 10, 50).toString()); // Middle: Slightly cooler jewel tone
  gradient.addColorStop(1, color(0, 0, 0, 100).toString()); // Edge: Dark, fully opaque

  drawingContext.fillStyle = gradient;
  drawingContext.fillRect(-width / 2, -height / 2, width, height); // Fill the entire canvas area
}
Line-by-line explanation (5 lines)
let gradient = drawingContext.createRadialGradient(0, 0, 0, 0, 0, min(width, height) / 2);
Uses the raw canvas 2D API (not a p5.js function) to create a radial gradient centered at (0,0) spreading out to half the smaller canvas dimension
gradient.addColorStop(0, color(0, 0, 0, 100).toString());
Defines the gradient's color at its very center (position 0) as fully opaque black
gradient.addColorStop(0.3, color(250, 50, 15, 80).toString());
Adds a jewel-toned color stop 30% of the way out from center
drawingContext.fillStyle = gradient;
Tells the canvas context to use this gradient as the fill style for the next shape drawn
drawingContext.fillRect(-width / 2, -height / 2, width, height);
Draws a rectangle covering the whole canvas (offset because WEBGL's origin is at the center) filled with the gradient, painting the glowing background

mousePressed()

mousePressed() is a p5.js event function that runs automatically once whenever the mouse button goes down - perfect for starting a new user action like a stroke.

function mousePressed() {
  // Clear drawingPoints to start a new stroke
  drawingPoints = [];
  // Add the initial point
  drawingPoints.push(createVector(mouseX - width / 2, mouseY - height / 2));
}
Line-by-line explanation (2 lines)
drawingPoints = [];
Empties the array of recorded points, discarding the previous stroke so a fresh one can begin
drawingPoints.push(createVector(mouseX - width / 2, mouseY - height / 2));
Records the first point of the new stroke, offsetting mouseX/mouseY by half the canvas size because WEBGL's coordinate origin (0,0) is at the center of the screen, not the top-left corner

mouseDragged()

mouseDragged() fires continuously while the mouse button is held and moved, making it ideal for freehand drawing input in p5.js.

function mouseDragged() {
  // Add current mouse position to drawingPoints array
  drawingPoints.push(createVector(mouseX - width / 2, mouseY - height / 2));

  // Simple "AI" complexity analysis: Suggest new palette every 500 points
  if (drawingPoints.length % 500 === 0) {
    generatePalette();
    console.log('AI suggested a new palette based on pattern complexity!');
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Auto Palette Refresh if (drawingPoints.length % 500 === 0) {

Automatically generates a fresh color palette every time 500 more points have been drawn

drawingPoints.push(createVector(mouseX - width / 2, mouseY - height / 2));
Adds the current mouse position (re-centered for WEBGL coordinates) to the running stroke every time this function fires during a drag
if (drawingPoints.length % 500 === 0) {
Checks if the total point count is an exact multiple of 500, using the modulo operator - a simple way to trigger something periodically
generatePalette();
Calls the palette generator again, giving the illusion of an 'AI' reacting to how complex your drawing has become

touchStarted()

touchStarted() is p5.js's mobile equivalent of mousePressed(), automatically called when a touch begins on the canvas.

function touchStarted() {
  // Prevent default browser behavior (e.g., scrolling)
  return false;
}
Line-by-line explanation (1 lines)
return false;
Returning false from p5.js touch event functions stops the browser's default behavior, like page scrolling, so drawing on mobile feels natural

touchMoved()

touchMoved() lets the same drawing logic used for mouse input also work on touchscreens, making the sketch usable on phones and tablets.

function touchMoved() {
  // Record touch positions similar to mouseDragged
  drawingPoints.push(createVector(touchX - width / 2, touchY - height / 2));

  // Simple "AI" complexity analysis: Suggest new palette every 500 points
  if (drawingPoints.length % 500 === 0) {
    generatePalette();
    console.log('AI suggested a new palette based on pattern complexity!');
  }

  // Prevent default browser behavior (e.g., scrolling)
  return false;
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

conditional Auto Palette Refresh (Touch) if (drawingPoints.length % 500 === 0) {

Same periodic palette refresh as mouseDragged(), but driven by touch movement on mobile devices

drawingPoints.push(createVector(touchX - width / 2, touchY - height / 2));
Adds the current touch position to the stroke array, mirroring what mouseDragged() does for mouse input
return false;
Prevents the mobile browser from scrolling or zooming while the user draws on the canvas

windowResized()

windowResized() is a p5.js event function that automatically fires when the browser window changes size, letting you keep responsive full-window sketches.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the browser window's new dimensions whenever it's resized, keeping the kaleidoscope full-screen

deviceMoved()

deviceMoved() is a p5.js event that fires when a mobile device's orientation sensors detect movement, letting you add tilt-based interaction on phones and tablets.

function deviceMoved() {
  // Update device rotation variables
  // rotationX: tilt front/back (-180 to 180)
  // rotationY: tilt left/right (-90 to 90)
  deviceRotationX = rotationX;
  deviceRotationY = rotationY;
}
Line-by-line explanation (2 lines)
deviceRotationX = rotationX;
Copies p5.js's built-in rotationX value (device tilt front/back) into our own global variable so draw() can use it
deviceRotationY = rotationY;
Copies p5.js's built-in rotationY value (device tilt left/right) into our own global variable to add a subtle mobile rotation effect

📦 Key Variables

drawingPoints array

Stores every p5.Vector point of the current stroke the user is drawing; this array is read by drawStroke() every frame to render the pattern

let drawingPoints = [];
palette array

Holds the current set of harmonious p5.Color objects used to color strokes and cycle through over time

let palette = [];
gui object

Reference to the dat.GUI instance that renders the on-screen control panel for adjusting settings

let gui;
settings object

Central configuration object holding all adjustable parameters (symmetry count, speeds, toggles) plus button functions, all wired to the dat.GUI panel

let settings = { symmetryCount: 6, rotationSpeed: 0.005 };
deviceRotationX number

Stores the device's front/back tilt angle from motion sensors, used to add a subtle rotation on mobile devices

let deviceRotationX = 0;
deviceRotationY number

Stores the device's left/right tilt angle from motion sensors, used to add a subtle rotation on mobile devices

let deviceRotationY = 0;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE drawStroke()

Inside the points loop, points.indexOf(point) is called for every single point, which is an O(n) search - making the whole loop O(n²) as the stroke grows long, causing noticeable slowdown with long drawings.

💡 Use a standard indexed for-loop (for (let i = 0; i < points.length; i++)) instead of for...of, so the index is already available without needing indexOf().

BUG touchStarted() vs mousePressed()

mousePressed() clears drawingPoints and starts a fresh stroke, but touchStarted() only returns false and never resets the array, so on touch devices new touches keep appending to the old stroke instead of starting a new one.

💡 Add the same drawingPoints = [] and initial point push logic from mousePressed() into touchStarted() for consistent behavior across input types.

STYLE drawRadialGradient()

The gradient's jewel-tone colors are hardcoded (e.g. color(250, 50, 15, 80)) and unrelated to the dynamically generated palette, so the background never matches the AI-suggested colors.

💡 Derive the gradient's colors from palette[0] or the current baseHue so the background gradient stays visually coordinated with the stroke colors.

FEATURE setup() device permission request

DeviceOrientationEvent.requestPermission() is called immediately in setup(), but iOS requires this to be triggered by a direct user gesture (like a tap), so the request will silently fail on most iPhones.

💡 Add a GUI button or on-screen prompt that calls requestPermission() inside a click/touch event handler, giving mobile users a way to explicitly grant motion access.

🔄 Code Flow

Code flow showing setup, draw, drawstroke, drawsymmetrylines, generatepalette, drawradialgradient, mousepressed, mousedragged, touchstarted, touchmoved, windowresized, devicemoved

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> device-permission-check[Device Permission Check] draw --> symmetry-loop[Symmetry Reflection Loop] draw --> symmetry-lines-conditional[Show Symmetry Lines Toggle] draw --> crystallized-branch[Points vs Lines Mode] draw --> auto-palette-check[Auto Palette Refresh] draw --> touch-auto-palette-check[Auto Palette Refresh (Touch)] symmetry-loop --> points-loop[Draw Each Point] symmetry-loop --> lines-loop[Draw Line Segments] symmetry-loop --> guide-line-loop[Draw Each Guide Line] points-loop --> draw lines-loop --> draw guide-line-loop --> draw click setup href "#fn-setup" click draw href "#fn-draw" click device-permission-check href "#sub-device-permission-check" click symmetry-loop href "#sub-symmetry-loop" click symmetry-lines-conditional href "#sub-symmetry-lines-conditional" click crystallized-branch href "#sub-crystallized-branch" click points-loop href "#sub-points-loop" click lines-loop href "#sub-lines-loop" click guide-line-loop href "#sub-guide-line-loop" click auto-palette-check href "#sub-auto-palette-check" click touch-auto-palette-check href "#sub-touch-auto-palette-check"

❓ Frequently Asked Questions

What type of visual patterns does the AI Kaleidoscope Generator create?

The sketch generates mesmerizing symmetrical patterns that change dynamically in real-time, utilizing a color palette and various symmetry axes.

How can users customize their experience with the AI Kaleidoscope Generator?

Users can interact with the sketch by adjusting parameters such as symmetry count, rotation speed, and color cycling speed using a GUI, as well as clearing the canvas or suggesting new color palettes.

What creative coding concepts are showcased in this p5.js sketch?

This sketch demonstrates concepts of symmetry, color theory, and real-time interactivity, utilizing WEBGL for enhanced visual effects and dynamic user input.

Preview

AI Kaleidoscope Generator - Symmetrical Pattern Art Draw mesmerizing symmetrical patterns with real - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Kaleidoscope Generator - Symmetrical Pattern Art Draw mesmerizing symmetrical patterns with real - Code flow showing setup, draw, drawstroke, drawsymmetrylines, generatepalette, drawradialgradient, mousepressed, mousedragged, touchstarted, touchmoved, windowresized, devicemoved
Code Flow Diagram