All articles
21 min read

Mandala Art: How to Create Beautiful Symmetrical Patterns With Code

mandala artgeometric artcreative codingJavaScriptgenerative artcanvas

A mandala is a geometric composition that radiates symmetry from a central point. The word comes from Sanskrit, meaning “circle,” but mandala art encompasses far more than circles — it is the practice of building intricate, layered patterns through rotational repetition. Hindu and Buddhist traditions use mandalas as meditation aids and cosmological maps. Tibetan monks spend weeks constructing sand mandalas, only to sweep them away upon completion. Carl Jung saw mandalas as expressions of the unconscious self, recommending mandala drawing as a therapeutic practice. Today, digital mandala art brings these ancient patterns into the world of creative coding, where a few lines of JavaScript can generate symmetries that would take hours to draw by hand.

In this guide, you will learn how to create mandala art with code. We will build eight interactive examples, starting from a basic radial mandala and progressing through dotwork, petal, animated, Islamic geometric, fractal, interactive, and generative mandalas. Every example is self-contained JavaScript using the Canvas API — no libraries, no dependencies. By the end, you will have a deep understanding of how to draw a mandala programmatically and the mathematical principles that make these patterns so compelling.

If you have already explored sacred geometry patterns or geometric art techniques, mandala art is a natural next step. Mandalas combine rotational symmetry, concentric layering, and radial repetition into a single unified form.

What is a mandala? History and mathematics

The mandala has deep roots across cultures. In Hinduism, mandalas represent the universe — the square outer boundary symbolizes the earthly realm, while the inner circles represent the divine. Buddhist mandalas serve as visualization tools for meditation, mapping the path to enlightenment. The Aztec calendar stone, Gothic rose windows, and Aboriginal dot paintings all share the mandala’s fundamental structure: symmetry radiating from a center.

Mathematically, a mandala exhibits dihedral symmetry — it has both rotational symmetry (it looks the same after rotating by 360°/N) and reflective symmetry (it looks the same when mirrored across certain axes). The dihedral group Dn describes all possible symmetries of a regular n-gon, and most mandalas belong to groups like D8, D12, or D16. This mathematical framework is what makes mandala art so satisfying to generate with code: you draw one “sector” and rotate it N times around the center.

1. Basic radial mandala

The simplest approach to mandala art is N-fold rotational symmetry with concentric rings. We divide the circle into N equal sectors (where N is the fold count), then draw shapes at different radii. Each ring can have different elements — circles, diamonds, triangles — creating visual variety through layering.

The core technique is straightforward: for each element, loop from 0 to N, rotate the canvas by 2π/N each time, and draw the element at a fixed offset from the center. By stacking multiple rings with different shapes and sizes, you build up complexity from simple repetition.

const c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
const ctx = c.getContext('2d');
const cx = 250, cy = 250;
const N = 12;
const angle = (Math.PI * 2) / N;

ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, 500, 500);

function drawRing(radius, size, shape, color) {
  ctx.save();
  for (let i = 0; i < N; i++) {
    const a = angle * i;
    const x = cx + Math.cos(a) * radius;
    const y = cy + Math.sin(a) * radius;
    ctx.fillStyle = color;
    ctx.strokeStyle = color;
    ctx.lineWidth = 1.5;
    if (shape === 'circle') {
      ctx.beginPath();
      ctx.arc(x, y, size, 0, Math.PI * 2);
      ctx.fill();
    } else if (shape === 'diamond') {
      ctx.save();
      ctx.translate(x, y);
      ctx.rotate(a);
      ctx.beginPath();
      ctx.moveTo(0, -size);
      ctx.lineTo(size * 0.6, 0);
      ctx.lineTo(0, size);
      ctx.lineTo(-size * 0.6, 0);
      ctx.closePath();
      ctx.fill();
      ctx.restore();
    } else if (shape === 'tri') {
      ctx.save();
      ctx.translate(x, y);
      ctx.rotate(a);
      ctx.beginPath();
      ctx.moveTo(0, -size);
      ctx.lineTo(size * 0.87, size * 0.5);
      ctx.lineTo(-size * 0.87, size * 0.5);
      ctx.closePath();
      ctx.stroke();
      ctx.restore();
    }
  }
  ctx.restore();
}

// concentric rings with different elements
drawRing(40, 6, 'circle', '#e94560');
drawRing(80, 14, 'diamond', '#0f3460');
drawRing(80, 4, 'circle', '#e94560');
drawRing(120, 10, 'tri', '#16c79a');
drawRing(120, 3, 'circle', '#f5f5f5');
drawRing(160, 18, 'diamond', '#0f3460');
drawRing(160, 5, 'circle', '#16c79a');
drawRing(200, 8, 'circle', '#e94560');
drawRing(200, 12, 'tri', '#f5f5f5');

// center circle
ctx.beginPath();
ctx.arc(cx, cy, 15, 0, Math.PI * 2);
ctx.fillStyle = '#e94560';
ctx.fill();
ctx.beginPath();
ctx.arc(cx, cy, 8, 0, Math.PI * 2);
ctx.fillStyle = '#1a1a2e';
ctx.fill();

The mathematics of symmetry in mandala art

Every mandala is built on the same mathematical foundation: the angle 2π/N where N is the fold count. When N = 6, you get hexagonal symmetry (like snowflakes). When N = 8, you get octagonal symmetry (common in Islamic art). When N = 12, you get the rich, dense patterns typical of traditional mandala drawing.

The key insight for coding mandalas is that you only need to define what happens in one sector — a wedge spanning 2π/N radians. The rotation loop copies that sector around the full circle. This is why mandalas are so satisfying to code: a small amount of definition creates enormous visual complexity. Combining rotation with translation (drawing at different radii) gives you the concentric ring structure that makes mandala art so distinctive.

2. Dotwork mandala

Dotwork is a mandala drawing technique that uses only dots — circles of varying sizes — to create the pattern. No lines, no fills, just points in space. The technique draws inspiration from Aboriginal dot painting and modern tattoo art. Each dot’s size and spacing contributes to the overall form, creating a pointillism effect where density implies shape.

In code, we place dots at calculated positions along concentric rings, varying their size based on radius and position. The result has a meditative, handcrafted quality despite being algorithmically generated. This is a popular style for digital mandala creators because the dot-only constraint produces a distinctive aesthetic.

const c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
const ctx = c.getContext('2d');
const cx = 250, cy = 250;
const N = 16;

ctx.fillStyle = '#fdf6e3';
ctx.fillRect(0, 0, 500, 500);

function dot(x, y, r, color) {
  ctx.beginPath();
  ctx.arc(x, y, r, 0, Math.PI * 2);
  ctx.fillStyle = color;
  ctx.fill();
}

const palette = ['#2d3436', '#636e72', '#b2bec3', '#d63031', '#e17055'];

// central dot
dot(cx, cy, 8, palette[3]);
dot(cx, cy, 3, palette[0]);

const rings = [
  { r: 30, count: N, size: 3, color: 0 },
  { r: 55, count: N, size: 5, color: 3 },
  { r: 55, count: N * 2, size: 1.5, color: 1 },
  { r: 85, count: N, size: 4, color: 0 },
  { r: 85, count: N * 2, size: 2, color: 2 },
  { r: 110, count: N * 3, size: 1.5, color: 1 },
  { r: 120, count: N, size: 6, color: 3 },
  { r: 120, count: N, size: 2.5, color: 0 },
  { r: 150, count: N * 2, size: 3, color: 4 },
  { r: 150, count: N * 4, size: 1, color: 2 },
  { r: 175, count: N, size: 5, color: 0 },
  { r: 175, count: N * 2, size: 2, color: 1 },
  { r: 200, count: N * 3, size: 2.5, color: 3 },
  { r: 200, count: N * 6, size: 0.8, color: 2 },
  { r: 220, count: N * 2, size: 4, color: 4 },
  { r: 220, count: N, size: 7, color: 0 },
];

for (const ring of rings) {
  const step = (Math.PI * 2) / ring.count;
  for (let i = 0; i < ring.count; i++) {
    const a = step * i;
    const x = cx + Math.cos(a) * ring.r;
    const y = cy + Math.sin(a) * ring.r;
    dot(x, y, ring.size, palette[ring.color]);
  }
}

// connecting arcs between major dots
ctx.strokeStyle = palette[2];
ctx.lineWidth = 0.5;
for (let i = 0; i < N; i++) {
  const a = (Math.PI * 2 / N) * i;
  for (let r = 30; r <= 200; r += 10) {
    const x = cx + Math.cos(a) * r;
    const y = cy + Math.sin(a) * r;
    dot(x, y, 0.6, palette[2]);
  }
}

Color in mandalas

Color plays a crucial role in mandala art. Traditional mandalas use symbolic colors: white for purity, red for passion, blue for tranquility, gold for wisdom. In code, we can use mathematical color relationships to create harmonious palettes automatically. HSL (Hue, Saturation, Lightness) color space is particularly useful because rotating the hue component by equal increments around the color wheel gives you complementary, triadic, or analogous schemes.

For mandala drawing, three color techniques work especially well: HSL cycling (incrementing hue based on ring radius or sector angle), opacity layering (semi-transparent shapes that create new colors where they overlap), and complementary accents (using a color and its 180° opposite for visual tension). The examples in this guide demonstrate all three approaches.

3. Petal mandala

Flower-like mandalas are perhaps the most recognizable form of mandala art. Each “petal” is drawn with bezier curves, creating soft, organic shapes that contrast with the geometric precision of the underlying rotational symmetry. By layering multiple rings of petals with different counts (for example, 8 large petals, then 16 smaller ones, then 32 tiny ones), you create the rich, nested look of a blooming flower.

The trick to convincing petals is using quadratic or cubic bezier curves with control points offset from the straight radial line. This gives each petal a slight bulge. Rotating the second layer by half a sector (an offset of π/N) means the smaller petals peek out between the larger ones, just like a real flower.

const c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
const ctx = c.getContext('2d');
const cx = 250, cy = 250;

ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, 500, 500);

function drawPetals(n, innerR, outerR, width, color, offset) {
  const step = (Math.PI * 2) / n;
  ctx.fillStyle = color;
  ctx.strokeStyle = color;
  ctx.lineWidth = 1;
  for (let i = 0; i < n; i++) {
    const a = step * i + (offset || 0);
    const x1 = cx + Math.cos(a) * innerR;
    const y1 = cy + Math.sin(a) * innerR;
    const x2 = cx + Math.cos(a) * outerR;
    const y2 = cy + Math.sin(a) * outerR;
    const cpAngle = width / outerR;
    const cp1x = cx + Math.cos(a - cpAngle) * ((innerR + outerR) * 0.55);
    const cp1y = cy + Math.sin(a - cpAngle) * ((innerR + outerR) * 0.55);
    const cp2x = cx + Math.cos(a + cpAngle) * ((innerR + outerR) * 0.55);
    const cp2y = cy + Math.sin(a + cpAngle) * ((innerR + outerR) * 0.55);
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.quadraticCurveTo(cp1x, cp1y, x2, y2);
    ctx.quadraticCurveTo(cp2x, cp2y, x1, y1);
    ctx.closePath();
    ctx.globalAlpha = 0.7;
    ctx.fill();
    ctx.globalAlpha = 1;
    ctx.stroke();
  }
}

// layered petals from outside in
drawPetals(24, 160, 220, 30, '#1a535c', 0);
drawPetals(16, 110, 180, 35, '#4ecdc4', 0);
drawPetals(16, 110, 180, 35, '#4ecdc4', Math.PI / 16);
drawPetals(12, 60, 140, 40, '#ff6b6b', 0);
drawPetals(8, 30, 100, 45, '#ffe66d', 0);
drawPetals(8, 30, 100, 45, '#ffe66d', Math.PI / 8);

// center ornament
ctx.beginPath();
ctx.arc(cx, cy, 25, 0, Math.PI * 2);
ctx.fillStyle = '#ff6b6b';
ctx.fill();
ctx.beginPath();
ctx.arc(cx, cy, 15, 0, Math.PI * 2);
ctx.fillStyle = '#ffe66d';
ctx.fill();
ctx.beginPath();
ctx.arc(cx, cy, 7, 0, Math.PI * 2);
ctx.fillStyle = '#0d1117';
ctx.fill();

Mandala construction principles

Whether you are drawing by hand or coding a digital mandala, the construction follows a consistent set of principles. First, concentric rings: every mandala is built outward from the center, with each ring adding a new layer of complexity. Second, radial elements: shapes within each ring are placed at equal angular intervals, creating the rotational symmetry. Third, bilateral symmetry within sectors: each sector (the wedge between two radial lines) is often symmetric about its own center line, adding mirror symmetry to the rotational symmetry.

A well-designed mandala balances density and openness. Rings of dense detail alternate with rings of open space. Large, bold shapes alternate with fine, intricate ones. This rhythm of expansion and contraction is what draws the eye inward toward the center — or outward toward the edge — creating the meditative quality that mandala art is known for.

4. Animated rotating mandala

Static mandalas are beautiful, but animated ones are hypnotic. By rotating each ring at a different angular velocity, you create a kinetic mandala where layers drift in and out of alignment. The visual effect is mesmerizing — patterns that momentarily align, then dissolve into new configurations.

The animation loop is simple: clear the canvas, then redraw each ring with an angle offset that increases over time. Different rings get different speed multipliers, and alternating the rotation direction (clockwise vs. counter-clockwise) adds visual depth. This is one of the most popular forms of digital mandala art because it transforms a static image into a living, breathing pattern.

const c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
const ctx = c.getContext('2d');
const cx = 250, cy = 250;
let t = 0;

const layers = [
  { r: 50, n: 6, size: 8, speed: 0.3, color: '#ff6b6b' },
  { r: 90, n: 10, size: 6, speed: -0.2, color: '#4ecdc4' },
  { r: 130, n: 14, size: 10, speed: 0.15, color: '#45b7d1' },
  { r: 165, n: 18, size: 5, speed: -0.1, color: '#96ceb4' },
  { r: 200, n: 22, size: 8, speed: 0.08, color: '#ffeaa7' },
  { r: 230, n: 12, size: 12, speed: -0.25, color: '#dfe6e9' },
];

function draw() {
  ctx.fillStyle = 'rgba(15, 15, 35, 0.15)';
  ctx.fillRect(0, 0, 500, 500);

  for (const layer of layers) {
    const step = (Math.PI * 2) / layer.n;
    const offset = t * layer.speed;
    ctx.strokeStyle = layer.color;
    ctx.fillStyle = layer.color;
    ctx.lineWidth = 1.5;

    for (let i = 0; i < layer.n; i++) {
      const a = step * i + offset;
      const x = cx + Math.cos(a) * layer.r;
      const y = cy + Math.sin(a) * layer.r;

      // draw a small mandala element at each point
      ctx.save();
      ctx.translate(x, y);
      ctx.rotate(a + t * layer.speed * 2);
      ctx.globalAlpha = 0.8;
      ctx.beginPath();
      ctx.moveTo(0, -layer.size);
      ctx.lineTo(layer.size * 0.6, 0);
      ctx.lineTo(0, layer.size);
      ctx.lineTo(-layer.size * 0.6, 0);
      ctx.closePath();
      ctx.fill();
      ctx.globalAlpha = 1;
      ctx.restore();
    }

    // connecting ring
    ctx.globalAlpha = 0.2;
    ctx.beginPath();
    ctx.arc(cx, cy, layer.r, 0, Math.PI * 2);
    ctx.stroke();
    ctx.globalAlpha = 1;
  }

  // pulsing center
  const pulse = 10 + Math.sin(t * 0.5) * 5;
  ctx.beginPath();
  ctx.arc(cx, cy, pulse, 0, Math.PI * 2);
  ctx.fillStyle = '#ff6b6b';
  ctx.globalAlpha = 0.6;
  ctx.fill();
  ctx.globalAlpha = 1;

  t += 0.02;
  requestAnimationFrame(draw);
}
draw();

5. Islamic geometric mandala

Islamic geometric art is one of the richest traditions of mandala-like pattern making. Based on star-and-polygon constructions, these patterns achieve breathtaking complexity through strict geometric rules. The fundamental technique is to inscribe stars within polygons: start with a regular polygon, connect non-adjacent vertices to form a star, then fill the resulting spaces with smaller polygons.

Islamic geometric mandalas typically use 8-fold, 10-fold, or 12-fold symmetry. The patterns avoid figurative imagery (following Islamic artistic tradition), relying entirely on geometric relationships for their beauty. In code, we construct these patterns by calculating intersection points of extended polygon edges — a process that would be tedious by hand but elegant in an algorithm. This style of mandala art connects directly to the mathematical art tradition.

const c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
const ctx = c.getContext('2d');
const cx = 250, cy = 250;
const N = 10;
const angle = (Math.PI * 2) / N;

ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, 500, 500);

function star(x, y, r, n, innerRatio, rot) {
  const pts = [];
  for (let i = 0; i < n * 2; i++) {
    const a = (Math.PI * 2 / (n * 2)) * i + rot;
    const rad = i % 2 === 0 ? r : r * innerRatio;
    pts.push([x + Math.cos(a) * rad, y + Math.sin(a) * rad]);
  }
  ctx.beginPath();
  ctx.moveTo(pts[0][0], pts[0][1]);
  for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i][0], pts[i][1]);
  ctx.closePath();
}

// outer border stars
ctx.strokeStyle = '#c9a227';
ctx.lineWidth = 1.5;
for (let i = 0; i < N; i++) {
  const a = angle * i;
  const x = cx + Math.cos(a) * 200;
  const y = cy + Math.sin(a) * 200;
  star(x, y, 30, 5, 0.4, a);
  ctx.stroke();
}

// middle ring: interlocking star pattern
ctx.strokeStyle = '#e8d5b7';
ctx.lineWidth = 1;
for (let i = 0; i < N; i++) {
  const a = angle * i;
  const x = cx + Math.cos(a) * 130;
  const y = cy + Math.sin(a) * 130;
  star(x, y, 40, N / 2, 0.5, 0);
  ctx.globalAlpha = 0.15;
  ctx.fillStyle = '#c9a227';
  ctx.fill();
  ctx.globalAlpha = 1;
  ctx.stroke();
}

// inner ring
for (let i = 0; i < N; i++) {
  const a = angle * i + angle / 2;
  const x = cx + Math.cos(a) * 70;
  const y = cy + Math.sin(a) * 70;
  star(x, y, 22, 5, 0.38, a);
  ctx.strokeStyle = '#c9a227';
  ctx.stroke();
}

// central star
star(cx, cy, 45, N, 0.65, 0);
ctx.strokeStyle = '#e8d5b7';
ctx.lineWidth = 2;
ctx.stroke();
ctx.globalAlpha = 0.1;
ctx.fillStyle = '#c9a227';
ctx.fill();
ctx.globalAlpha = 1;

star(cx, cy, 20, N / 2, 0.4, Math.PI / N);
ctx.strokeStyle = '#c9a227';
ctx.lineWidth = 1.5;
ctx.stroke();

// radial lines
ctx.strokeStyle = 'rgba(201,162,39,0.25)';
ctx.lineWidth = 0.5;
for (let i = 0; i < N; i++) {
  const a = angle * i;
  ctx.beginPath();
  ctx.moveTo(cx, cy);
  ctx.lineTo(cx + Math.cos(a) * 230, cy + Math.sin(a) * 230);
  ctx.stroke();
}

6. Fractal mandala

A fractal mandala takes the concept of self-similarity — the hallmark of fractals — and applies it to mandala construction. Each arm of the mandala contains a smaller copy of the whole mandala, which in turn contains even smaller copies, recursing down to a minimum size. The result is a pattern with extraordinary depth and detail that emerges from a very simple recursive rule.

The implementation uses a recursive function that draws N elements around a center point, then calls itself at each element’s position with a smaller radius and size. A depth limit prevents infinite recursion. This technique connects mandala art to the broader world of Fibonacci spirals and fractal geometry, where simple rules generate complex emergent patterns.

const c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
const ctx = c.getContext('2d');

ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 500, 500);

function fractalMandala(x, y, radius, n, depth, maxDepth) {
  if (depth > maxDepth || radius < 2) return;
  const step = (Math.PI * 2) / n;
  const hue = (depth * 60 + 200) % 360;
  const alpha = 1 - depth * 0.15;

  // draw ring at this level
  ctx.strokeStyle = `hsla(${hue}, 70%, 60%, ${alpha})`;
  ctx.lineWidth = Math.max(0.5, 3 - depth * 0.5);
  ctx.beginPath();
  ctx.arc(x, y, radius * 0.3, 0, Math.PI * 2);
  ctx.stroke();

  for (let i = 0; i < n; i++) {
    const a = step * i - Math.PI / 2;
    const nx = x + Math.cos(a) * radius;
    const ny = y + Math.sin(a) * radius;

    // element at this position
    ctx.fillStyle = `hsla(${hue}, 80%, 65%, ${alpha * 0.6})`;
    ctx.beginPath();
    ctx.arc(nx, ny, radius * 0.12, 0, Math.PI * 2);
    ctx.fill();

    // line from center to element
    ctx.strokeStyle = `hsla(${hue}, 50%, 50%, ${alpha * 0.4})`;
    ctx.lineWidth = Math.max(0.3, 2 - depth * 0.4);
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(nx, ny);
    ctx.stroke();

    // recurse with fewer arms and smaller radius
    fractalMandala(nx, ny, radius * 0.38, Math.max(3, n - 1), depth + 1, maxDepth);
  }
}

fractalMandala(250, 250, 140, 7, 0, 4);

// central glow
const grad = ctx.createRadialGradient(250, 250, 0, 250, 250, 40);
grad.addColorStop(0, 'rgba(200, 160, 255, 0.4)');
grad.addColorStop(1, 'rgba(200, 160, 255, 0)');
ctx.beginPath();
ctx.arc(250, 250, 40, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();

7. Interactive mouse mandala

One of the joys of digital mandala art is interactivity. By mapping mouse position to mandala parameters, you create a mandala maker that responds to the viewer in real time. Moving the mouse left and right controls the fold count (how many sectors), while moving up and down controls the radius. The result is a living mandala that the viewer co-creates through their movements.

This example uses requestAnimationFrame for smooth updates and maps mouse coordinates to parameters using simple linear interpolation. The color shifts based on the current fold count, so each configuration has its own palette. This is how to draw a mandala that feels alive — the viewer becomes part of the creative process.

const c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
const ctx = c.getContext('2d');
const cx = 250, cy = 250;
let mx = 250, my = 250;

c.addEventListener('mousemove', e => {
  const rect = c.getBoundingClientRect();
  mx = e.clientX - rect.left;
  my = e.clientY - rect.top;
});

function draw() {
  ctx.fillStyle = 'rgba(10, 10, 25, 0.2)';
  ctx.fillRect(0, 0, 500, 500);

  const folds = Math.floor(3 + (mx / 500) * 20);
  const maxR = 50 + (my / 500) * 180;
  const step = (Math.PI * 2) / folds;
  const hueBase = (mx + my) * 0.3;

  // draw 5 concentric rings
  for (let ring = 1; ring <= 5; ring++) {
    const r = maxR * (ring / 5);
    const hue = (hueBase + ring * 40) % 360;

    ctx.strokeStyle = `hsl(${hue}, 75%, 55%)`;
    ctx.lineWidth = 1;

    for (let i = 0; i < folds; i++) {
      const a = step * i;
      const x = cx + Math.cos(a) * r;
      const y = cy + Math.sin(a) * r;
      const sz = 4 + ring * 2;

      ctx.save();
      ctx.translate(x, y);
      ctx.rotate(a);

      ctx.fillStyle = `hsla(${hue}, 80%, 60%, 0.5)`;
      ctx.beginPath();
      ctx.moveTo(0, -sz);
      ctx.quadraticCurveTo(sz, 0, 0, sz);
      ctx.quadraticCurveTo(-sz, 0, 0, -sz);
      ctx.fill();
      ctx.stroke();

      ctx.restore();
    }

    // ring circle
    ctx.globalAlpha = 0.15;
    ctx.beginPath();
    ctx.arc(cx, cy, r, 0, Math.PI * 2);
    ctx.stroke();
    ctx.globalAlpha = 1;
  }

  // center
  ctx.beginPath();
  ctx.arc(cx, cy, 6, 0, Math.PI * 2);
  ctx.fillStyle = `hsl(${hueBase % 360}, 80%, 70%)`;
  ctx.fill();

  // label
  ctx.fillStyle = '#aaa';
  ctx.font = '12px monospace';
  ctx.fillText(`folds: ${folds}  radius: ${Math.round(maxR)}`, 10, 490);

  requestAnimationFrame(draw);
}
draw();

8. Generative mandala art

The ultimate mandala maker generates unique mandalas from random seeds. Each click produces a new mandala with randomized parameters: fold count, number of rings, element types, colors, and sizes. Despite the randomness, the underlying rotational symmetry guarantees that every result is a coherent, beautiful mandala. This is generative mandala art — the algorithm is the artist, and every output is a one-of-a-kind creation.

The generator uses a seeded approach: it randomizes high-level parameters (how many rings, what shapes, which colors) but keeps the fundamental mandala structure intact. The rotational symmetry is never random — it is always exact. This constraint is what separates generative mandala art from noise: the structure is deterministic, but the content is stochastic.

const c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
const ctx = c.getContext('2d');
const cx = 250, cy = 250;

function generate() {
  ctx.fillStyle = '#111';
  ctx.fillRect(0, 0, 500, 500);

  const N = [6, 8, 10, 12, 16][Math.floor(Math.random() * 5)];
  const ringCount = 4 + Math.floor(Math.random() * 5);
  const hueStart = Math.random() * 360;
  const step = (Math.PI * 2) / N;

  for (let ring = 1; ring <= ringCount; ring++) {
    const r = 30 + (ring / ringCount) * 190;
    const hue = (hueStart + ring * 35) % 360;
    const sat = 50 + Math.random() * 30;
    const shape = Math.floor(Math.random() * 4);
    const count = N * (1 + Math.floor(Math.random() * 3));
    const angleStep = (Math.PI * 2) / count;
    const sz = 3 + Math.random() * 12;

    for (let i = 0; i < count; i++) {
      const a = angleStep * i;
      const x = cx + Math.cos(a) * r;
      const y = cy + Math.sin(a) * r;

      ctx.fillStyle = `hsla(${hue}, ${sat}%, 60%, 0.7)`;
      ctx.strokeStyle = `hsla(${hue}, ${sat}%, 75%, 0.9)`;
      ctx.lineWidth = 1;

      if (shape === 0) {
        ctx.beginPath();
        ctx.arc(x, y, sz * 0.4, 0, Math.PI * 2);
        ctx.fill();
      } else if (shape === 1) {
        ctx.save();
        ctx.translate(x, y); ctx.rotate(a);
        ctx.beginPath();
        ctx.moveTo(0, -sz); ctx.lineTo(sz * 0.5, 0);
        ctx.lineTo(0, sz); ctx.lineTo(-sz * 0.5, 0);
        ctx.closePath(); ctx.fill(); ctx.stroke();
        ctx.restore();
      } else if (shape === 2) {
        ctx.save();
        ctx.translate(x, y); ctx.rotate(a);
        ctx.beginPath();
        ctx.moveTo(0, -sz);
        ctx.quadraticCurveTo(sz * 0.7, 0, 0, sz);
        ctx.quadraticCurveTo(-sz * 0.7, 0, 0, -sz);
        ctx.fill();
        ctx.restore();
      } else {
        ctx.save();
        ctx.translate(x, y); ctx.rotate(a);
        ctx.fillRect(-sz * 0.3, -sz * 0.3, sz * 0.6, sz * 0.6);
        ctx.restore();
      }
    }

    // decorative ring
    ctx.globalAlpha = 0.1;
    ctx.beginPath();
    ctx.arc(cx, cy, r, 0, Math.PI * 2);
    ctx.strokeStyle = `hsl(${hue}, 60%, 60%)`;
    ctx.lineWidth = 0.5;
    ctx.stroke();
    ctx.globalAlpha = 1;
  }

  // center
  const cHue = hueStart;
  ctx.beginPath();
  ctx.arc(cx, cy, 12, 0, Math.PI * 2);
  ctx.fillStyle = `hsl(${cHue}, 80%, 65%)`;
  ctx.fill();
  ctx.beginPath();
  ctx.arc(cx, cy, 5, 0, Math.PI * 2);
  ctx.fillStyle = '#111';
  ctx.fill();
}

generate();
c.addEventListener('click', generate);
c.style.cursor = 'pointer';

Going further with mandala art

The eight examples above cover the fundamentals of mandala art with code, but there is much more to explore:

  • SVG export — Convert your canvas mandala to SVG for infinite resolution. SVG mandalas can be laser-cut, vinyl-cut, or printed at any size without pixelation.
  • Audio-reactive mandalas — Use the Web Audio API to analyze music in real time and map frequency bands to mandala parameters. Bass controls the center, mids control middle rings, treble controls the outer edge.
  • 3D mandalas — Extrude the 2D mandala into 3D using WebGL or Three.js. Each ring becomes a disc at a different height, creating a layered sculpture you can rotate and explore.
  • Mandala coloring books — Generate outline-only mandalas and export them as printable pages. Randomize the designs so every page is unique — an infinite coloring book.
  • Collaborative mandalas — Use WebSockets to let multiple people contribute to the same mandala in real time. Each person controls one sector, and the symmetry is maintained by the server.

Mandala art sits at the intersection of mathematics, meditation, and visual beauty. The rotational symmetry that defines every mandala is the same symmetry that governs crystals, flowers, and galaxies. When you code a mandala, you are working with patterns that are as fundamental to nature as they are to human artistic expression. The mandala maker you build today could generate a pattern that no human has ever seen before — and yet it will feel familiar, because its symmetry is universal.

Explore more generative art on Lumitree, where every branch is a unique micro-world built from code. For more pattern techniques, try the sacred geometry guide, the geometric art guide, the math art guide, or the Fibonacci spiral guide.

Related articles