Op Art: How to Create Mind-Bending Optical Illusions With Code
Op art — short for optical art — is a movement that emerged in the 1960s with artists like Bridget Riley and Victor Vasarely. The idea is simple but powerful: use precise geometric patterns to trick the human visual system into seeing movement, depth, vibration, and color that isn't actually there. No animation needed — your brain does all the work.
What makes op art perfect for code is that the patterns are deeply mathematical. Repeating lines, concentric circles, phase-shifted grids, and interference patterns are trivial to generate programmatically but almost impossible to draw by hand with the required precision. A few lines of trigonometry can produce images that seem to pulse, breathe, and ripple on a static screen.
This guide builds 8 op art techniques from scratch in JavaScript and Canvas. Every example is self-contained, interactive, and runs in your browser. No libraries, no frameworks — just math and pixels.
The science behind optical illusions
Op art works because your visual cortex constantly tries to interpret patterns. When patterns are highly regular but contain subtle variations — slightly curved lines, gradual phase shifts, overlapping grids at near-identical frequencies — your brain's motion detection, edge detection, and color processing systems get confused. They "see" movement in static images, colors in black-and-white patterns, and depth in flat surfaces.
Key principles we'll exploit:
- Moiré interference: when two regular patterns overlap at slightly different scales or angles, they create a third emergent pattern that appears to shimmer
- Peripheral drift: high-contrast repeating elements at certain spatial frequencies trigger motion signals in peripheral vision
- Hermann grid effect: gray "ghost" dots appear at intersections of white lines on a black grid because of lateral inhibition in your retina
- Chromatic vibration: adjacent colors of equal luminance create a buzzing, unstable boundary that your eyes can't quite focus on
Example 1: Moiré interference rings
Two sets of concentric circles, slightly offset from each other. Where they overlap, moiré patterns emerge — flowing, organic shapes from pure geometry. Move your mouse to shift the offset and watch the interference pattern dance.
var c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
var ctx = c.getContext('2d');
var mx = c.width / 2 + 30, my = c.height / 2 + 30;
c.addEventListener('mousemove', function(e) {
var r = c.getBoundingClientRect();
mx = e.clientX - r.left; my = e.clientY - r.top;
});
function draw() {
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, c.width, c.height);
var img = ctx.createImageData(c.width, c.height);
var cx1 = c.width / 2, cy1 = c.height / 2;
var cx2 = mx, cy2 = my;
var spacing = 6;
for (var y = 0; y < c.height; y++) {
for (var x = 0; x < c.width; x++) {
var d1 = Math.sqrt((x - cx1) * (x - cx1) + (y - cy1) * (y - cy1));
var d2 = Math.sqrt((x - cx2) * (x - cx2) + (y - cy2) * (y - cy2));
var v1 = Math.sin(d1 / spacing * Math.PI) > 0 ? 1 : 0;
var v2 = Math.sin(d2 / spacing * Math.PI) > 0 ? 1 : 0;
var val = (v1 + v2) % 2 === 0 ? 0 : 255;
var i = (y * c.width + x) * 4;
img.data[i] = img.data[i + 1] = img.data[i + 2] = val;
img.data[i + 3] = 255;
}
}
ctx.putImageData(img, 0, 0);
requestAnimationFrame(draw);
}
draw();
The moiré pattern appears because two sets of black-and-white rings interfere. Where both are black or both white, you see one thing; where they disagree, you see another. The result is a fluid pattern that shifts smoothly as you move the mouse — even though every pixel is either pure black or pure white.
Example 2: Concentric ripple illusion
Concentric rings with alternating black and white bands, but with a sinusoidal radial distortion that makes the flat image appear to bulge outward like a sphere. The distortion is subtle — just a few pixels of phase shift — but your brain interprets it as 3D depth.
var c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
var ctx = c.getContext('2d');
var t = 0;
function draw() {
var img = ctx.createImageData(c.width, c.height);
var cx = c.width / 2, cy = c.height / 2;
var maxR = Math.min(cx, cy);
for (var y = 0; y < c.height; y++) {
for (var x = 0; x < c.width; x++) {
var dx = x - cx, dy = y - cy;
var dist = Math.sqrt(dx * dx + dy * dy);
var norm = dist / maxR;
var bulge = Math.sin(norm * Math.PI * 0.5);
var phase = bulge * 28 + t * 0.5;
var ring = Math.sin(dist / 5 - phase) > 0 ? 0 : 255;
var fade = Math.max(0, 1 - norm * 1.1);
var val = Math.round(ring * fade + 255 * (1 - fade));
var i = (y * c.width + x) * 4;
img.data[i] = img.data[i + 1] = img.data[i + 2] = val;
img.data[i + 3] = 255;
}
}
ctx.putImageData(img, 0, 0);
t++;
requestAnimationFrame(draw);
}
draw();
The bulge variable applies a sinusoidal offset to the ring phase based on distance from center. Near the middle the offset is greatest, making rings appear closer together — exactly what happens when a flat pattern is mapped onto a convex surface. Your brain sees a dome even though the canvas is perfectly flat.
Example 3: Rotating spiral — the illusion of spin
A static pattern that appears to rotate when you look at it. This uses logarithmic spirals with alternating black and white segments. The animation adds genuine slow rotation, but even a single still frame triggers the illusion of faster spin in your peripheral vision.
var c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
var ctx = c.getContext('2d');
var t = 0;
function draw() {
var img = ctx.createImageData(c.width, c.height);
var cx = c.width / 2, cy = c.height / 2;
for (var y = 0; y < c.height; y++) {
for (var x = 0; x < c.width; x++) {
var dx = x - cx, dy = y - cy;
var dist = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
var spiral = angle + Math.log(dist + 1) * 1.5 + t * 0.02;
var arms = 6;
var v = Math.sin(spiral * arms) > 0 ? 0 : 255;
var fade = Math.max(0, 1 - dist / (cx * 1.1));
var val = Math.round(v * fade + 255 * (1 - fade));
var i = (y * c.width + x) * 4;
img.data[i] = img.data[i + 1] = img.data[i + 2] = val;
img.data[i + 3] = 255;
}
}
ctx.putImageData(img, 0, 0);
t++;
requestAnimationFrame(draw);
}
draw();
The key is angle + Math.log(dist + 1) * 1.5 — this creates a logarithmic spiral where the angle contribution shifts with distance. The 6 arms create a pinwheel pattern. Because the spiral curves tighten toward the center, your peripheral vision interprets the pattern as rotation even when the animation is paused. The slow actual rotation reinforces the effect.
Example 4: Warping checkerboard
A classic checkerboard pattern with a sinusoidal distortion applied to the coordinates. The result looks like a checkerboard draped over invisible rolling hills — a technique Bridget Riley used extensively in works like Movement in Squares (1961).
var c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
var ctx = c.getContext('2d');
var t = 0;
function draw() {
var img = ctx.createImageData(c.width, c.height);
var cx = c.width / 2, cy = c.height / 2;
for (var y = 0; y < c.height; y++) {
for (var x = 0; x < c.width; x++) {
var nx = (x - cx) / cx, ny = (y - cy) / cy;
var dist = Math.sqrt(nx * nx + ny * ny);
var warp = Math.sin(dist * 6 - t * 0.03) * 0.3;
var wx = nx + nx * warp;
var wy = ny + ny * warp;
var sx = Math.floor((wx + 1) * 8);
var sy = Math.floor((wy + 1) * 8);
var checker = (sx + sy) % 2 === 0 ? 0 : 255;
var i = (y * c.width + x) * 4;
img.data[i] = img.data[i + 1] = img.data[i + 2] = checker;
img.data[i + 3] = 255;
}
}
ctx.putImageData(img, 0, 0);
t++;
requestAnimationFrame(draw);
}
draw();
The warp variable pushes coordinates radially — outward or inward depending on the sine phase. Where the checkerboard squares compress, your brain sees convexity. Where they expand, concavity. The slow animation makes the surface appear to breathe. Bridget Riley spent weeks calculating these distortions by hand; we generate them in a nested loop.
Example 5: Parallel line interference
Two overlapping sets of parallel lines at slightly different angles. The interference between them creates bold moiré bands that sweep across the image. Tilt the angle slightly and the bands shift dramatically — a tiny change in input producing a huge change in output.
var c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
var ctx = c.getContext('2d');
var t = 0;
function draw() {
var img = ctx.createImageData(c.width, c.height);
var angle1 = 0;
var angle2 = 0.08 + Math.sin(t * 0.005) * 0.06;
var freq = 0.08;
for (var y = 0; y < c.height; y++) {
for (var x = 0; x < c.width; x++) {
var u1 = x * Math.cos(angle1) + y * Math.sin(angle1);
var u2 = x * Math.cos(angle2) + y * Math.sin(angle2);
var line1 = Math.sin(u1 * freq * Math.PI * 2) > 0 ? 1 : 0;
var line2 = Math.sin(u2 * freq * Math.PI * 2) > 0 ? 1 : 0;
var val = (line1 + line2) % 2 === 0 ? 0 : 255;
var i = (y * c.width + x) * 4;
img.data[i] = img.data[i + 1] = img.data[i + 2] = val;
img.data[i + 3] = 255;
}
}
ctx.putImageData(img, 0, 0);
t++;
requestAnimationFrame(draw);
}
draw();
When two line grids are nearly parallel, the moiré pattern has very wide bands. As the angle difference increases, the bands narrow. By slowly oscillating the second angle, the moiré bands sweep back and forth like a slow wave. This is pure geometry — two simple patterns creating complex emergent behavior.
Example 6: Chromatic vibration
Op art isn't always black and white. When two colors have similar luminance but different hues — like red and cyan, or blue and orange — their boundary vibrates. Your color-processing neurons fire in conflict, creating a buzzing, electric edge that you can't quite focus on. This technique was central to the work of Richard Anuszkiewicz.
var c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
var ctx = c.getContext('2d');
var t = 0;
function hslToRgb(h, s, l) {
h /= 360; s /= 100; l /= 100;
var r, g, b;
if (s === 0) { r = g = b = l; } else {
function hue2rgb(p, q, t2) {
if (t2 < 0) t2 += 1; if (t2 > 1) t2 -= 1;
if (t2 < 1/6) return p + (q - p) * 6 * t2;
if (t2 < 1/2) return q;
if (t2 < 2/3) return p + (q - p) * (2/3 - t2) * 6;
return p;
}
var q2 = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p2 = 2 * l - q2;
r = hue2rgb(p2, q2, h + 1/3);
g = hue2rgb(p2, q2, h);
b = hue2rgb(p2, q2, h - 1/3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
function draw() {
var img = ctx.createImageData(c.width, c.height);
var cx = c.width / 2, cy = c.height / 2;
var hue1 = (t * 0.3) % 360;
var hue2 = (hue1 + 180) % 360;
var col1 = hslToRgb(hue1, 100, 50);
var col2 = hslToRgb(hue2, 100, 50);
for (var y = 0; y < c.height; y++) {
for (var x = 0; x < c.width; x++) {
var dx = x - cx, dy = y - cy;
var angle = Math.atan2(dy, dx);
var dist = Math.sqrt(dx * dx + dy * dy);
var sectors = 12;
var rings = dist / 15;
var pattern = Math.sin(angle * sectors) * Math.sin(rings * Math.PI);
var pick = pattern > 0 ? col1 : col2;
var i = (y * c.width + x) * 4;
img.data[i] = pick[0]; img.data[i + 1] = pick[1];
img.data[i + 2] = pick[2]; img.data[i + 3] = 255;
}
}
ctx.putImageData(img, 0, 0);
t++;
requestAnimationFrame(draw);
}
draw();
The two complementary colors have roughly equal luminance, so your luminance-based edge detection can't find the boundary clearly. But your color channels disagree intensely. The result is a shimmering, unstable border around every element. As the hue pair slowly rotates through the spectrum, different pairs vibrate with different intensities — red/cyan is strongest, blue/yellow slightly less so.
Example 7: Bulge distortion grid
A uniform grid of dots with a spherical distortion applied. Near the center, dots compress together; near the edges, they spread apart. The result is a convincing illusion of a sphere pushing through a flat surface — a digital version of Vasarely's famous "planetary" compositions.
var c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
var ctx = c.getContext('2d');
var t = 0;
function draw() {
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, c.width, c.height);
var cx = c.width / 2, cy = c.height / 2;
var gridSize = 20;
var maxBulge = 0.6;
var pulseRadius = 120 + Math.sin(t * 0.02) * 30;
for (var gy = 0; gy < c.height; gy += gridSize) {
for (var gx = 0; gx < c.width; gx += gridSize) {
var dx = gx - cx, dy = gy - cy;
var dist = Math.sqrt(dx * dx + dy * dy);
var influence = Math.max(0, 1 - dist / pulseRadius);
var bulge = influence * influence * maxBulge;
var offsetX = dx * bulge;
var offsetY = dy * bulge;
var px = gx + offsetX;
var py = gy + offsetY;
var dotSize = gridSize * 0.3 * (1 + bulge * 2);
var bright = 0.4 + influence * 0.6;
var hue = (dist * 0.5 + t * 0.5) % 360;
ctx.beginPath();
ctx.arc(px, py, dotSize, 0, Math.PI * 2);
ctx.fillStyle = 'hsl(' + hue + ', 70%, ' + (bright * 50) + '%)';
ctx.fill();
}
}
t++;
requestAnimationFrame(draw);
}
draw();
The bulge is quadratic (influence * influence) for a smooth falloff. Dots near the center get pushed outward and grow larger — mimicking perspective foreshortening on a sphere. The pulsing radius makes the sphere appear to breathe in and out. Vasarely spent months computing dot positions on graph paper; this loop does it 60 times per second.
Example 8: Animated tunnel illusion
A classic op art tunnel: concentric rings with alternating sectors that rotate in opposite directions. The competing rotations create a powerful illusion of depth — you feel pulled into the center, even though nothing is actually moving toward you. This is the most hypnotic pattern in our collection.
var c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
var ctx = c.getContext('2d');
var t = 0;
function draw() {
var img = ctx.createImageData(c.width, c.height);
var cx = c.width / 2, cy = c.height / 2;
for (var y = 0; y < c.height; y++) {
for (var x = 0; x < c.width; x++) {
var dx = x - cx, dy = y - cy;
var dist = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
var ringIndex = Math.floor(dist / 12);
var direction = ringIndex % 2 === 0 ? 1 : -1;
var rotatedAngle = angle + direction * t * 0.015 + dist * 0.005;
var sectors = 8 + ringIndex * 2;
var sector = Math.sin(rotatedAngle * sectors);
var ring = Math.sin(dist * 0.26 - t * 0.04);
var combined = sector * ring > 0 ? 1 : 0;
var depthFade = Math.max(0, 1 - dist / (cx * 1.2));
var centerGlow = Math.exp(-dist * dist / 8000);
var brightness = combined * depthFade * 0.9 + centerGlow * 0.3;
var val = Math.round(brightness * 255);
var i = (y * c.width + x) * 4;
img.data[i] = val;
img.data[i + 1] = Math.round(val * 0.95);
img.data[i + 2] = Math.round(val * 1.1 > 255 ? 255 : val * 1.1);
img.data[i + 3] = 255;
}
}
ctx.putImageData(img, 0, 0);
t++;
requestAnimationFrame(draw);
}
draw();
The trick is counter-rotation: even-numbered rings rotate clockwise, odd-numbered rings rotate counter-clockwise. This conflicting motion confuses your brain's flow-field detection, creating the illusion of depth. More sectors in outer rings mimic perspective convergence. The center glow adds a vanishing point that your attention is drawn toward. Stare at the center for 10 seconds, then look at your hand — you'll see it warping. That's the motion aftereffect, a real neurological phenomenon caused by adapted motion-detecting neurons.
The art of visual deception
Op art sits at the intersection of art, mathematics, and neuroscience. Every illusion exploits a specific quirk of human vision — lateral inhibition, motion adaptation, chromatic rivalry, or spatial frequency sensitivity. The patterns are simple. The math is accessible. But the perceptual effects are profound.
What to explore next:
- Combine moiré patterns with color theory — complementary color moirés create the strongest vibration effects
- Use geometric transformations (rotations, reflections, scaling) to build kaleidoscopic op art compositions
- Apply op art distortions to mathematical curves — Lissajous figures and rose curves become mesmerizing when phase-shifted into moiré interference
- Explore anamorphic distortion: transform op art so it looks correct only from a specific viewing angle
- Build interactive installations where webcam input drives the distortion parameters — viewers become part of the art
- Study spatial frequency analysis — the Fourier transform reveals exactly which frequencies trigger which illusions
- Use Perlin noise to add organic variation to otherwise rigid geometric patterns
Op art proves that the simplest tools — black and white, circles and lines, sine and cosine — can produce the most powerful visual experiences. Your brain is the most complex rendering engine in existence, and op art is the art of programming it. Every pattern on this page is a program that runs not on silicon, but on neurons. On Lumitree, some micro-worlds use these same interference patterns to create immersive visual environments that pulse and shimmer with each visitor's interaction — bringing 1960s optical art into the collaborative, generative web.