Moiré patterns occur when two images — usually simple geometric ones — are overlaid and “interfere” with each other. The result is a ghostly third image that can be surprisingly complex. In many graphical processes moiré is an undesirable phenomenon that technicians work hard to avoid, but it’s also been produced deliberately, as in so-called “watered” fabrics. The effect also has applications in the sciences.

In art discourse the term is often abused to refer to any kind of shimmering optical effect; for example, I can’t think of a single Riley or Vasarely that uses moiré, despite the assertions of many writers that it was characteristic of their work. This kinetic sculpture study by contemporary artist Stiliana Alexieva, though, really does use a moiré effect, seeming to dissolve a solid wire ring into a mirage:

Let’s try to make sense of what we’re seeing. First, consider this example, this appears to be an image of blurred vertical red and white lines:

In fact the image is created from two superimposed, identical sets of horizontal lines, one of which has been slightly rotated. Here’s what’s really going on:

As you can see, the apparent darkness of the overall image is a function of the distance between the two sets of lines. This is hardly surprising: where the lines are close together, more of the white background can show through. The lightest-coloured parts of a moiré, which really determine the whole pattern, are often called fringes.

Here is another example: this time two identical grids containing circles are rotated against each other:

The arrangement of the fringes in this case is structurally the same as the arrangement of the original circles: they are stacked in rows. (The light, straight lines at the edges of the image on the right aren’t moiré fringes, they’re an artefact created by my shoddy code). We would say the circles and their fringes have the same symmetry group, but that’s a topic for another post.

Now for some more interesting ones. Here are two identical sets of curved lines, one rotated against the other:

The fringes are curved this time and it’s not obvious what those curves are. Things get even more interesting with sets of concentric circles; of course, rotating them does nothing but you can make one set slightly smaller than the other and/or offset it:

These experiments were inspired by this short video by Tadashi Tokieda where he demonstrates some surprising moiré-type effects with random patterns:

I’m not certain how much of what we’re seeing here is due to the moiré effect; a short note in a 1969 edition of Nature suggested it was an perceptual illusion, but used the term “moiré” anyway. Here’s my attempt to reproduce it:

Here are some sources of more detailed discussions of the effect:

I’m surprised there doesn’t appear to be a single book dedicated to moiré patterns — maybe you should write one.

Finally, I made the red, black and white images in this post using a bit of Processing code I wrote to explore the effects Tadashi Tokieda demonstrates in his video. The code is a horrible mess, but in case it’s of interest (hello, Processing students!) I’ve pasted it below. Just paste it into a new sketch window and use the keys indicated at the top to generate different patterns. It works best running full-screen.

/**************************************************************

A small collection of moire effects.

Keys:
Up, Down, Left, Right: move the upper grid
Q, A: Rotate the upper grid
W, S: Scale the upper grid
[, ]: Change the grid size the pattern is based on (see below)
N, M: Change curvature of lines (Mode 6 only)
R: Toggle the upper grid between red and black
Space: Capture a frame
1-6: Set the mode

Clicking the mouse resets the upper grid to its default
position, rotation and scale.

The modes are:
1: Random
2: Diagonal dots
3: Horizontal lines
4: Tiled circles
5: Triangles
6: Bezier curves
7: Concentric circles

NOTE: The code for resizing the grid contains a bug (perhaps
several) that produces unexpected effects in some modes.
But surprises are good, right?

**************************************************************/

PGraphics pg1;
PGraphics pg2;
color BLACK = color(0);
color WHITE = color(0, 0);
color RED = color(155, 0, 0);
color DARK_GREY = color(20);
color LIGHT_GREY = color(50);
int xOffset = 0;
int yOffset = 0;
float r = 0.0;
float scale = 1.0;
int mode = 8;
int gridsize = 2;
boolean redMode = true;
int curvature = 30;
float kinks = 0.1;

void setup(){
size(displayWidth, displayHeight);
init();
}

void init(){
xOffset = 0;
yOffset = 0;
r = 0.0;
scale = 1.0;
pg1 = createGraphics(width, height);
pg1.beginDraw();
if(mode == 1 || mode == 2 || mode == 3){
int widthCount = 0;
for(int i = 0; i < width*height; i+=gridsize){
if(
(mode == 1 && random(0, 1) < 0.5)
|| (mode == 2 && i%5 == 0)
|| (mode == 3 && ((i%height)%gridsize == 0 || (i % width)%gridsize == 0))
){
setPixel(pg1, i, BLACK);
} else {
setPixel(pg1, i, WHITE);
}
widthCount += gridsize;
if (widthCount >= width){
i += width*gridsize;
i -= i % width;
widthCount = 0;
}
}
pg1.updatePixels();
} else {
for(int i = 0; i < width*height; i++){
pg1.pixels[i] = WHITE;
}
pg1.updatePixels();
if (mode == 4){
pg1.stroke(BLACK);
pg1.noFill();
int step = gridsize*3;
for(int x = 0; x < width; x+=step){
for(int y = 0; y < height; y+=step){
pg1.ellipse(x, y, step, step);
}
}
} else if (mode == 5){
pg1.stroke(BLACK);
pg1.noFill();
int step = gridsize*3;
for(int x = 0; x < width; x+=step){
for(int y = 0; y < height; y+=step){
pg1.triangle(x, y, x+step, y, x+step/2, y+step);
}
}
} else if (mode == 6){
pg1.stroke(BLACK);
pg1.noFill();
int step = gridsize*3;
for(int y = curvature*gridsize; y < height – curvature*gridsize; y+=step){
pg1.bezier(0, y, width/3, y+step*curvature, 2*width/3, y-step*curvature, width, y);
}
} else if (mode == 7){
pg1.stroke(BLACK);
pg1.noFill();
int step = gridsize*3;
for(int r = 0; r < height; r+=step){
pg1.ellipse(width/2, height/2, r, r);
}
} else if (mode == 8){
pg1.stroke(DARK_GREY);
pg1.noFill();
int block = gridsize * 3;
for(int y = 0; y < height; y+= block){
int offset = 0;
int prevy = y;
for(int x = block; x < width; x+= block){
if(random(0, 1) < kinks){
if(random(0, 1) < 0.5){
offset++;
} else {
offset–;
}
}
pg1.line(x – block, prevy, x, prevy + offset);
prevy = y + offset;
}
}
}
}
pg1.endDraw();

pg2 = createGraphics(width, height);
pg2.beginDraw();
for(int i = 0; i < pg1.pixels.length; i++){
if(pg1.pixels[i] == WHITE){
pg2.pixels[i] = WHITE;
} else {
if (mode == 8){
pg2.pixels[i] = LIGHT_GREY;
} else {
pg2.pixels[i] = redMode? RED : BLACK;
}
}
}
pg2.updatePixels();
pg2.endDraw();
}

void setPixel(PGraphics pg, int i, color c){
for(int x = 0; x < gridsize; x++){
for(int y = 0; y < gridsize; y++){
int px = i + x + (width*y);
if(px < pg.pixels.length – 1){
pg.pixels[px] = c;
}
}
}
}

void draw(){
background(255);
image(pg1, 0, 0);
pushMatrix();
translate(width/2, height/2);
rotate(r);
translate(width/-2, height/-2);
int scw = round(pg2.width*scale);
int sch = round(pg2.height*scale);
int baseX = round(xOffset + (width – scw)/2);
int baseY = round(yOffset + (height – pg2.height*scale)/2);

image(pg2, baseX, baseY, scw, sch);
image(pg2, baseX + scw, baseY, scw, sch);
image(pg2, baseX – scw, baseY, scw, sch);
image(pg2, baseX, baseY + sch, scw, sch);
image(pg2, baseX + scw, baseY + sch, scw, sch);
image(pg2, baseX – scw, baseY + sch, scw, sch);
image(pg2, baseX, baseY – sch, scw, sch);
image(pg2, baseX + scw, baseY – sch, scw, sch);
image(pg2, baseX – scw, baseY – sch, scw, sch);

popMatrix();
}

void keyPressed(){
if(keyCode ==38){
yOffset–;
} else if(keyCode ==40){
yOffset++;
} else if(keyCode ==37){
xOffset–;
} else if(keyCode ==39){
xOffset++;
} else if(keyCode ==81){
r += 0.001;
} else if(keyCode ==65){
r -= 0.001;
} else if(keyCode ==87){
scale += 0.0005;
} else if(keyCode ==83){
scale -= 0.0005;
} else if(keyCode ==93){
gridsize++;
init();
} else if(keyCode ==91){
if(gridsize > 1){
gridsize–;
init();
}
} else if(keyCode ==83){
scale -= 0.0002;
} else if(keyCode ==77){
curvature++;
if(kinks < 1){
kinks+=0.01;
}
init();
} else if(keyCode ==78){
curvature–;
if(kinks > 0){
kinks-=0.01;
}
init();
} else if(keyCode ==49){
mode = 1;
init();
} else if(keyCode ==50){
mode = 2;
init();
} else if(keyCode ==51){
mode = 3;
init();
} else if(keyCode ==52){
mode = 4;
init();
} else if(keyCode ==53){
mode = 5;
init();
} else if(keyCode ==54){
mode = 6;
init();
} else if(keyCode ==55){
mode = 7;
init();
} else if(keyCode ==56){
mode = 8;
init();
} else if (keyCode == 32){
// capture frame
saveFrame(“moire-#####.png”);
} else if (keyCode == 82){
redMode = !redMode;
init();
}  else {
println(keyCode);
}
}

void mousePressed(){
init();
}