"Roy's Mirror" or "Through a Glass, Dithered"


Last winter I went to a great Roy Lichtenstein retrospective at the National Gallery. There were entire rooms of brilliant pieces, especially his Art History pieces inspired by Monet and the "Landscapes in a Chinese Style." This project of mine is inspired by some of Lichtenstein's "Mirrors."

Lichtenstein's choice of mirrors as a subject is one of the artist's references to art history — Jan van Eyck and Diego Velázquez famously depicted mirrors in their work. The convincing representation of a reflecting mirror has long been a sign of technical virtuosity. In 1969 Lichtenstein began his first Mirror and by 1972 he had completed almost 50 variations. Surprisingly, his mirrors reflect nothing except the play of light and reflection; in his Self-Portrait (1978), the artist's face is replaced by such a mirror.
Roy Lichtenstein, "Mirror #1," 1969.

Roy Lichtenstein, "Self Portrait," 1978.

The following Processing-based animation is my attempt to combine his image representing a mirror with an actual mirror. (And by "actual mirror" I really mean "webcam and computer screen.") This should work in Google Chrome and other Webkit browsers. If you're not running one of those, I have a version that runs on the Desktop, which also runs much faster because the browser version needs an awkward and slow JavaScript work-around to get ahold of the webcam stream.

Your browser does not support the canvas tag.

The version above can be a real CPU hog. To pause and re-start the animation, simply click on it and then press space.

If the animation doesn't work in your browser, here's how it looks.

Screen cap of animation, showing the author, with a bonus Lichtenstein poster in the background. I didn't plan it that way, I promise.


Listed below is the version which runs in the browser. You can download it here, and the version that runs in the Processing IDE here. For the web version to work you also need video.js, which gives the Processing code a hook in to the Google webcam API. The only other thing you'll need is either mirror.png (for the web version)mirror2.png (for the desktop version). They're the same, other than size.

/* @pjs preload="mirror.png"; */

PImage mirror;
int numPixels;
boolean useCam = true;
float sigmoidScale = 3.0;
float desatFactor = 0.125;
color erase = color(100,255,0);
color tempC;
int pixNum, pixNumFlip;

var ctx;
var pxData;

void setup() {
  mirror = loadImage("mirror.png");
  numPixels = width*height;
  ctx = externals.context; 
  pxData = ctx.createImageData(width,height);

void draw() {
    ctx.drawImage(video, 0, 0, width, height);
    pxData = ctx.getImageData(0,0,width,height);
    for( int i = 0; i < height; i++) {
      for( int j = 0; j < width; j++ ) {
        pixNum = i*width+j;
        if( pixels[pixNum] == erase ) {
          pixNumFlip = i*width+(width-j);
          tempC = mirror.pixels[pixNum];
          float k = intensity(tempC)/255.0;
          k = sigmoid((k-0.5)*sigmoidScale);
          vidColor = color(pxData.data[pixNumFlip*4+0], ...
              pxData.data[pixNumFlip*4+1], pxData.data[pixNumFlip*4+2]);
          pixels[pixNum] = lerpColor(tempC,desaturate(vidColor,desatFactor),k);

boolean looping = true;
void keyPressed() {
  if (key == ' ') {
    if( looping ) {
    } else {
    looping = !looping;

float cx = 0;
float cy = 0;
float rx = 223;
float ry = 285;

void drawShadow() {
  float offsetX = -8;
  float offsetY = 10;
  for( int i = 1; i < 32; i++ ) {
    stroke(32, (32-i)*4);
    ellipse(width/2+cx+offsetX, height/2+cy+offsetY, rx-32+2*i, ry-32+2*i);

void drawGrnScreen() {
  ellipse(width/2+cx, height/2+cy, rx, ry);

void drawFrame() {
  ellipse(width/2+cx, height/2+cy, rx, ry);

float intensity(color c) {
  return 0.3*red(c)+0.59*green(c)+0.11*blue(c);

color desaturate(color c, float f) {
  float i = intensity(c);
  return lerpColor(c, color(i,i,i), f);

float sigmoid(float f ) {
  return 1.0/(1+exp(-1*f));