All articles
20 min read

Moiré Patterns: How to Create Mesmerizing Interference Art With Code

moire patternmoire effectinterference patterncreative codingJavaScriptgenerative artoptical illusion

Move two window screens past each other and you'll see it — rippling, flowing shapes that seem to exist somewhere between the two layers. That's a moiré pattern. It emerges when two regular grids overlap at slightly different angles or frequencies. Neither grid contains the pattern. It exists only in the interference between them.

Moiré effects show up everywhere: in fabric folds, TV screens filmed with cameras, security printing on banknotes, and the quantum-scale electron density maps of twisted graphene. Artists and designers have used them for centuries — sometimes deliberately, sometimes accidentally. In this guide, we'll build 8 interactive moiré programs from scratch using JavaScript and the HTML Canvas API. No libraries. No frameworks. Just lines, circles, and the mathematics of interference.

1. Line interference — the simplest moiré

The most basic moiré pattern comes from overlapping two sets of parallel lines at a slight angle. When the lines are nearly parallel, wide "beats" appear — dark bands that flow across the image. The closer the angles, the wider the bands. This is the visual equivalent of acoustic beat frequencies.

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

function draw() {
  ctx.fillStyle = '#0a0a0a';
  ctx.fillRect(0, 0, 600, 600);

  const spacing = 6;
  const angle2 = 0.04; // slight rotation — try 0.01 to 0.1
  const cx = 300, cy = 300;

  // Layer 1: vertical lines
  ctx.strokeStyle = 'rgba(255,255,255,0.5)';
  ctx.lineWidth = 1;
  for (let x = -100; x < 700; x += spacing) {
    ctx.beginPath();
    ctx.moveTo(x, 0);
    ctx.lineTo(x, 600);
    ctx.stroke();
  }

  // Layer 2: slightly rotated lines
  ctx.save();
  ctx.translate(cx, cy);
  ctx.rotate(angle2);
  ctx.translate(-cx, -cy);
  for (let x = -100; x < 700; x += spacing) {
    ctx.beginPath();
    ctx.moveTo(x, 0);
    ctx.lineTo(x, 600);
    ctx.stroke();
  }
  ctx.restore();
}

draw();

The beat frequency formula is simple: when two gratings with spacing d overlap at angle θ, the moiré fringe spacing is d / (2 sin(θ/2)). At very small angles, sin(θ/2) ≈ θ/2, so fringe spacing ≈ d / θ. A 1° rotation of 6px-spaced lines produces moiré bands about 344px wide.

2. Concentric circle moiré — radial interference

Two sets of concentric circles with slightly different centers produce stunning moiré patterns that look like magnetic field lines, gravitational lenses, or topographic maps. The interference creates cardioid and limaçon shapes that shift as you change the offset between centers.

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

function draw() {
  ctx.fillStyle = '#0a0a0a';
  ctx.fillRect(0, 0, 600, 600);

  const spacing = 8;
  const maxR = 500;
  const cx1 = 280, cy1 = 300; // center 1
  const cx2 = 320, cy2 = 300; // center 2 — offset by 40px

  ctx.lineWidth = 1;

  // Circle set 1
  ctx.strokeStyle = 'rgba(255,255,255,0.4)';
  for (let r = spacing; r < maxR; r += spacing) {
    ctx.beginPath();
    ctx.arc(cx1, cy1, r, 0, Math.PI * 2);
    ctx.stroke();
  }

  // Circle set 2
  ctx.strokeStyle = 'rgba(255,255,255,0.4)';
  for (let r = spacing; r < maxR; r += spacing) {
    ctx.beginPath();
    ctx.arc(cx2, cy2, r, 0, Math.PI * 2);
    ctx.stroke();
  }
}

draw();

The dark bands you see are hyperbolic curves — the locus of points where the path difference from the two centers equals an integer multiple of the spacing. This is identical to the physics of two-source wave interference (think: double-slit experiment). The moiré fringes are literally a macroscopic visualization of wave superposition.

3. Rotational moiré — spinning grid interference

Overlay two identical grids and slowly rotate one. As the angle changes, the moiré pattern morphs through a hypnotic sequence: large cells collapse into smaller ones, then expand again. At 0° you see nothing (identical overlap). At small angles, enormous magnified cells appear. At 30° a hexagonal pattern emerges. At 45° you get a checkerboard. At 90° the pattern repeats.

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

let angle = 0;

function draw() {
  ctx.fillStyle = '#0a0a0a';
  ctx.fillRect(0, 0, 600, 600);

  const spacing = 12;
  const cx = 300, cy = 300;

  // Draw grid function
  function drawGrid(rot) {
    ctx.save();
    ctx.translate(cx, cy);
    ctx.rotate(rot);
    ctx.translate(-cx, -cy);
    ctx.strokeStyle = 'rgba(255,255,255,0.35)';
    ctx.lineWidth = 1;
    // Vertical lines
    for (let x = -200; x < 800; x += spacing) {
      ctx.beginPath();
      ctx.moveTo(x, -200);
      ctx.lineTo(x, 800);
      ctx.stroke();
    }
    // Horizontal lines
    for (let y = -200; y < 800; y += spacing) {
      ctx.beginPath();
      ctx.moveTo(-200, y);
      ctx.lineTo(800, y);
      ctx.stroke();
    }
    ctx.restore();
  }

  drawGrid(0);
  drawGrid(angle);

  angle += 0.0008;
  requestAnimationFrame(draw);
}

draw();

The magnification factor of a rotational moiré is 1 / (2 sin(θ/2)). At 1°, features appear 57× larger than the original grid spacing. This is why moiré patterns have been used for precision measurement — a tiny angular change produces a huge visible shift. Metrologists call this moiré deflectometry.

4. Wave superposition moiré — sinusoidal interference

Instead of hard lines, use smooth sine waves. Two sinusoidal gratings with different frequencies produce amplitude modulation — exactly like AM radio. The "envelope" of the combined signal creates the moiré. This version is softer and more organic than line-based moiré.

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

let t = 0;

function draw() {
  const img = ctx.createImageData(600, 600);
  const d = img.data;

  const f1 = 0.05;  // frequency 1
  const f2 = 0.053; // frequency 2 — slightly different

  for (let y = 0; y < 600; y++) {
    for (let x = 0; x < 600; x++) {
      // Two sine waves at different frequencies
      const wave1 = Math.sin(x * f1 + t);
      const wave2 = Math.sin(x * f2 * Math.cos(0.3) + y * f2 * Math.sin(0.3) + t * 0.7);

      // Multiply: product of two cosines = sum of two cosines (trig identity)
      const combined = (wave1 + 1) * (wave2 + 1) / 4;

      const i = (y * 600 + x) * 4;
      const v = combined * 255;
      d[i] = v * 0.4;      // R — blue-cyan palette
      d[i + 1] = v * 0.7;  // G
      d[i + 2] = v;         // B
      d[i + 3] = 255;
    }
  }

  ctx.putImageData(img, 0, 0);
  t += 0.02;
  requestAnimationFrame(draw);
}

draw();

The math here is a direct application of the product-to-sum trig identity: cos(a) · cos(b) = ½[cos(a-b) + cos(a+b)]. The cos(a-b) term is the low-frequency moiré envelope. The cos(a+b) term is the high-frequency carrier that you can barely see. Your eye naturally groups the slow envelope — that's the moiré.

5. Dot screen halftone moiré — printing artifact as art

In CMYK printing, each ink color uses a dot screen at a different angle. When these angles aren't set correctly, visible moiré rosettes appear. Printers spend enormous effort avoiding this. We're going to create it on purpose. Each dot's size encodes brightness. Overlapping dot screens at different angles create complex rosette moiré patterns.

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

function drawDotScreen(cx, cy, spacing, angle, color, radius) {
  ctx.fillStyle = color;
  const cos = Math.cos(angle);
  const sin = Math.sin(angle);

  for (let gy = -40; gy < 40; gy++) {
    for (let gx = -40; gx < 40; gx++) {
      // Rotate grid position
      const x = cx + (gx * spacing * cos - gy * spacing * sin);
      const y = cy + (gx * spacing * sin + gy * spacing * cos);

      if (x < -20 || x > 620 || y < -20 || y > 620) continue;

      // Vary dot size based on distance from center for visual interest
      const dx = x - 300, dy = y - 300;
      const dist = Math.sqrt(dx * dx + dy * dy);
      const size = radius * (1 - dist / 600) * 1.5;

      if (size > 0.5) {
        ctx.beginPath();
        ctx.arc(x, y, size, 0, Math.PI * 2);
        ctx.fill();
      }
    }
  }
}

ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, 600, 600);
ctx.globalCompositeOperation = 'screen';

// CMYK-inspired dot screens at traditional angles
drawDotScreen(300, 300, 14, 0.261,  'rgba(0,255,255,0.6)', 5);   // Cyan at 15°
drawDotScreen(300, 300, 14, 1.309,  'rgba(255,0,255,0.6)', 5);   // Magenta at 75°
drawDotScreen(300, 300, 14, 0,      'rgba(255,255,0,0.5)', 5);   // Yellow at 0°
drawDotScreen(300, 300, 14, 0.785,  'rgba(255,255,255,0.3)', 4); // Black at 45°

ctx.globalCompositeOperation = 'source-over';

Traditional CMYK screen angles are C=15°, M=75°, Y=0°, K=45°. These angles are chosen to minimize moiré — the 30° separation between CMK produces the least visible rosette pattern. Change these angles even slightly and dramatic moiré artifacts appear. The rosette pattern you see is a three-frequency moiré, which is substantially more complex than two-frequency interference.

6. Animated phase shift moiré — breathing interference

Animate the relative position of two gratings and the moiré bands flow like water. This creates a striking optical illusion of motion — the individual lines barely move, but the interference bands sweep across the canvas. It's the visual equivalent of how two slightly detuned musical notes create a slowly pulsing beat.

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

let t = 0;

function draw() {
  ctx.fillStyle = '#0a0a0a';
  ctx.fillRect(0, 0, 600, 600);

  const n = 80; // number of circles per set
  const maxR = 420;
  const spacing = maxR / n;

  // Set 1: fixed concentric circles
  ctx.strokeStyle = 'rgba(100,200,255,0.4)';
  ctx.lineWidth = 1.5;
  for (let i = 1; i <= n; i++) {
    ctx.beginPath();
    ctx.arc(300, 300, i * spacing, 0, Math.PI * 2);
    ctx.stroke();
  }

  // Set 2: concentric circles with phase-shifted radii
  const shift = Math.sin(t) * spacing * 0.9; // oscillate within one period
  ctx.strokeStyle = 'rgba(255,140,60,0.4)';
  for (let i = 1; i <= n; i++) {
    const r = i * spacing + shift;
    if (r > 0) {
      ctx.beginPath();
      ctx.arc(300, 300, r, 0, Math.PI * 2);
      ctx.stroke();
    }
  }

  t += 0.015;
  requestAnimationFrame(draw);
}

draw();

When the phase shift equals exactly one spacing period, the two sets align perfectly and the moiré disappears. At half-period offset, the moiré bands are at their maximum contrast — every line of one set falls exactly between two lines of the other. The slow oscillation between these states creates a mesmerizing breathing effect.

7. Interactive mouse moiré — real-time interference

Make one set of circles follow the mouse cursor while the other stays fixed. Moving the cursor changes the center offset in real-time, producing continuously morphing moiré patterns. Small movements near the center create slow, dramatic shifts. Large movements produce rapid, complex interference.

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

let mx = 300, my = 300;
canvas.addEventListener('mousemove', e => {
  const r = canvas.getBoundingClientRect();
  mx = e.clientX - r.left;
  my = e.clientY - r.top;
});

// Touch support
canvas.addEventListener('touchmove', e => {
  e.preventDefault();
  const r = canvas.getBoundingClientRect();
  mx = e.touches[0].clientX - r.left;
  my = e.touches[0].clientY - r.top;
}, { passive: false });

function draw() {
  ctx.fillStyle = '#0a0a0a';
  ctx.fillRect(0, 0, 600, 600);

  const spacing = 7;
  const maxR = 500;
  ctx.lineWidth = 1;

  // Fixed circles at center
  ctx.strokeStyle = 'rgba(160,220,255,0.35)';
  for (let r = spacing; r < maxR; r += spacing) {
    ctx.beginPath();
    ctx.arc(300, 300, r, 0, Math.PI * 2);
    ctx.stroke();
  }

  // Circles following mouse
  ctx.strokeStyle = 'rgba(255,180,100,0.35)';
  for (let r = spacing; r < maxR; r += spacing) {
    ctx.beginPath();
    ctx.arc(mx, my, r, 0, Math.PI * 2);
    ctx.stroke();
  }

  requestAnimationFrame(draw);
}

draw();

The moiré fringes you see are confocal conics — specifically, confocal hyperbolas when the centers are separated. The "bright" bands are where circles from both sets nearly coincide (constructive interference). The "dark" bands are where they maximally interleave (destructive interference). Moving the mouse traces out the entire family of two-center interference patterns: from nearly-zero-offset (huge moiré cells) to large-offset (tight, complex fringes).

8. Generative moiré art — layered interference composition

Combine multiple moiré techniques into a single evolving composition. Layer rotating line grids, drifting circle sets, and pulsing frequency shifts. Add color. The result is an endlessly changing abstract artwork driven entirely by interference mathematics.

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

let t = 0;

function draw() {
  // Fade trail
  ctx.fillStyle = 'rgba(5,5,15,0.15)';
  ctx.fillRect(0, 0, 600, 600);

  const cx = 300, cy = 300;

  // Layer 1: rotating line grid — warm color
  ctx.save();
  ctx.translate(cx, cy);
  ctx.rotate(t * 0.1);
  ctx.translate(-cx, -cy);
  ctx.strokeStyle = 'rgba(255,100,60,0.15)';
  ctx.lineWidth = 1;
  const sp1 = 8;
  for (let x = -200; x < 800; x += sp1) {
    ctx.beginPath();
    ctx.moveTo(x, -200);
    ctx.lineTo(x, 800);
    ctx.stroke();
  }
  ctx.restore();

  // Layer 2: counter-rotating line grid — cool color
  ctx.save();
  ctx.translate(cx, cy);
  ctx.rotate(-t * 0.1 + 0.05);
  ctx.translate(-cx, -cy);
  ctx.strokeStyle = 'rgba(60,140,255,0.15)';
  for (let x = -200; x < 800; x += sp1) {
    ctx.beginPath();
    ctx.moveTo(x, -200);
    ctx.lineTo(x, 800);
    ctx.stroke();
  }
  ctx.restore();

  // Layer 3: drifting concentric circles
  const drift = Math.sin(t * 0.3) * 40;
  ctx.strokeStyle = 'rgba(100,255,180,0.1)';
  const sp2 = 10;
  for (let r = sp2; r < 500; r += sp2) {
    ctx.beginPath();
    ctx.arc(cx + drift, cy + drift * 0.7, r, 0, Math.PI * 2);
    ctx.stroke();
  }

  // Layer 4: fixed circles for interference with layer 3
  ctx.strokeStyle = 'rgba(255,200,100,0.08)';
  for (let r = sp2; r < 500; r += sp2) {
    ctx.beginPath();
    ctx.arc(cx - drift * 0.5, cy - drift * 0.3, r, 0, Math.PI * 2);
    ctx.stroke();
  }

  // Layer 5: radial lines — slowly spinning
  ctx.strokeStyle = 'rgba(200,160,255,0.06)';
  const nRays = 60;
  for (let i = 0; i < nRays; i++) {
    const a = (i / nRays) * Math.PI * 2 + t * 0.05;
    ctx.beginPath();
    ctx.moveTo(cx, cy);
    ctx.lineTo(cx + Math.cos(a) * 400, cy + Math.sin(a) * 400);
    ctx.stroke();
  }

  // Layer 6: second radial set — opposite spin
  ctx.strokeStyle = 'rgba(255,255,150,0.05)';
  for (let i = 0; i < nRays; i++) {
    const a = (i / nRays) * Math.PI * 2 - t * 0.05 + 0.02;
    ctx.beginPath();
    ctx.moveTo(cx, cy);
    ctx.lineTo(cx + Math.cos(a) * 400, cy + Math.sin(a) * 400);
    ctx.stroke();
  }

  t += 0.01;
  requestAnimationFrame(draw);
}

draw();

This layered composition creates what physicists call multi-frequency interference. Each pair of layers produces its own moiré pattern. But these moiré patterns themselves interfere with each other, creating second-order moiré — moiré of moiré. The visual complexity grows combinatorially: 6 layers produce 15 pair-wise interactions, each generating its own family of fringes.

The physics of moiré

Moiré patterns are not just visual curiosities — they're a fundamental phenomenon in wave physics:

  • Acoustics: Two guitar strings tuned almost identically produce a slowly pulsing "beat" — the auditory moiré. Beat frequency = |f₁ - f₂|.
  • Optics: Newton's rings, thin-film iridescence (soap bubbles, oil on water), and anti-reflective coatings all work through the same interference mathematics.
  • Materials science: Twisted bilayer graphene — two sheets of graphene rotated by exactly 1.1° — creates a moiré superlattice that becomes a superconductor at 1.7 Kelvin. This discovery (2018, Pablo Jarillo-Herrero at MIT) launched an entire field called twistronics.
  • Metrology: Moiré deflectometry measures surface flatness to nanometer precision. Two gratings amplify tiny deformations into visible fringe shifts.
  • Security: Banknotes, passports, and ID cards embed moiré patterns that are impossible to reproduce with standard printers — the interference pattern only appears at the correct viewing angle or with the correct overlay.

Going further

  • Explore geometric art for related visual techniques: Islamic patterns, Penrose tilings, kaleidoscopes, and op art — several of these overlap with moiré aesthetics
  • Learn about mathematical art for more pattern families: rose curves, superformula, strange attractors, and phyllotaxis
  • Try tessellation art for tiling patterns that can serve as the base layers for moiré interference
  • Combine moiré with Perlin noise — warp the grid lines with noise for organic, flowing interference that looks like draped fabric
  • Add color theory — use complementary or triadic color schemes for the overlapping layers to create additive color mixing in the interference zones
  • Use moiré as a particle emitter — spawn particles along the interference fringes for a living, breathing moiré field
  • Export high-resolution renders for print — moiré patterns at 300 DPI on large format paper are genuinely stunning gallery pieces
  • On Lumitree, several micro-worlds use overlapping geometric layers that produce moiré interference — every slight parameter change creates entirely different visual mathematics

Related articles