diff --git a/examples/processing/grid32x16z_attractor/OPC.pde b/examples/processing/grid32x16z_attractor/OPC.pde
new file mode 100644
index 0000000000000000000000000000000000000000..917035d46c7e60be569b8cec80e21363f2bf88da
--- /dev/null
+++ b/examples/processing/grid32x16z_attractor/OPC.pde
@@ -0,0 +1,349 @@
+/*
+ * Simple Open Pixel Control client for Processing,
+ * designed to sample each LED's color from some point on the canvas.
+ *
+ * Micah Elizabeth Scott, 2013
+ * This file is released into the public domain.
+ */
+
+import java.net.*;
+import java.util.Arrays;
+
+public class OPC
+{
+  Socket socket;
+  OutputStream output;
+  String host;
+  int port;
+
+  int[] pixelLocations;
+  byte[] packetData;
+  byte firmwareConfig;
+  String colorCorrection;
+  boolean enableShowLocations;
+
+  OPC(PApplet parent, String host, int port)
+  {
+    this.host = host;
+    this.port = port;
+    this.enableShowLocations = true;
+    parent.registerDraw(this);
+  }
+
+  // Set the location of a single LED
+  void led(int index, int x, int y)  
+  {
+    // For convenience, automatically grow the pixelLocations array. We do want this to be an array,
+    // instead of a HashMap, to keep draw() as fast as it can be.
+    if (pixelLocations == null) {
+      pixelLocations = new int[index + 1];
+    } else if (index >= pixelLocations.length) {
+      pixelLocations = Arrays.copyOf(pixelLocations, index + 1);
+    }
+
+    pixelLocations[index] = x + width * y;
+  }
+  
+  // Set the location of several LEDs arranged in a strip.
+  // Angle is in radians, measured clockwise from +X.
+  // (x,y) is the center of the strip.
+  void ledStrip(int index, int count, float x, float y, float spacing, float angle, boolean reversed)
+  {
+    float s = sin(angle);
+    float c = cos(angle);
+    for (int i = 0; i < count; i++) {
+      led(reversed ? (index + count - 1 - i) : (index + i),
+        (int)(x + (i - (count-1)/2.0) * spacing * c + 0.5),
+        (int)(y + (i - (count-1)/2.0) * spacing * s + 0.5));
+    }
+  }
+
+  // Set the location of several LEDs arranged in a grid. The first strip is
+  // at 'angle', measured in radians clockwise from +X.
+  // (x,y) is the center of the grid.
+  void ledGrid(int index, int stripLength, int numStrips, float x, float y,
+               float ledSpacing, float stripSpacing, float angle, boolean zigzag)
+  {
+    float s = sin(angle + HALF_PI);
+    float c = cos(angle + HALF_PI);
+    for (int i = 0; i < numStrips; i++) {
+      ledStrip(index + stripLength * i, stripLength,
+        x + (i - (numStrips-1)/2.0) * stripSpacing * c,
+        y + (i - (numStrips-1)/2.0) * stripSpacing * s, ledSpacing,
+        angle, zigzag && (i % 2) == 1);
+    }
+  }
+
+  // Set the location of 64 LEDs arranged in a uniform 8x8 grid.
+  // (x,y) is the center of the grid.
+  void ledGrid8x8(int index, float x, float y, float spacing, float angle, boolean zigzag)
+  {
+    ledGrid(index, 8, 8, x, y, spacing, spacing, angle, zigzag);
+  }
+
+  // Should the pixel sampling locations be visible? This helps with debugging.
+  // Showing locations is enabled by default. You might need to disable it if our drawing
+  // is interfering with your processing sketch, or if you'd simply like the screen to be
+  // less cluttered.
+  void showLocations(boolean enabled)
+  {
+    enableShowLocations = enabled;
+  }
+  
+  // Enable or disable dithering. Dithering avoids the "stair-stepping" artifact and increases color
+  // resolution by quickly jittering between adjacent 8-bit brightness levels about 400 times a second.
+  // Dithering is on by default.
+  void setDithering(boolean enabled)
+  {
+    if (enabled)
+      firmwareConfig &= ~0x01;
+    else
+      firmwareConfig |= 0x01;
+    sendFirmwareConfigPacket();
+  }
+
+  // Enable or disable frame interpolation. Interpolation automatically blends between consecutive frames
+  // in hardware, and it does so with 16-bit per channel resolution. Combined with dithering, this helps make
+  // fades very smooth. Interpolation is on by default.
+  void setInterpolation(boolean enabled)
+  {
+    if (enabled)
+      firmwareConfig &= ~0x02;
+    else
+      firmwareConfig |= 0x02;
+    sendFirmwareConfigPacket();
+  }
+
+  // Put the Fadecandy onboard LED under automatic control. It blinks any time the firmware processes a packet.
+  // This is the default configuration for the LED.
+  void statusLedAuto()
+  {
+    firmwareConfig &= 0x0C;
+    sendFirmwareConfigPacket();
+  }    
+
+  // Manually turn the Fadecandy onboard LED on or off. This disables automatic LED control.
+  void setStatusLed(boolean on)
+  {
+    firmwareConfig |= 0x04;   // Manual LED control
+    if (on)
+      firmwareConfig |= 0x08;
+    else
+      firmwareConfig &= ~0x08;
+    sendFirmwareConfigPacket();
+  } 
+
+  // Set the color correction parameters
+  void setColorCorrection(float gamma, float red, float green, float blue)
+  {
+    colorCorrection = "{ \"gamma\": " + gamma + ", \"whitepoint\": [" + red + "," + green + "," + blue + "]}";
+    sendColorCorrectionPacket();
+  }
+  
+  // Set custom color correction parameters from a string
+  void setColorCorrection(String s)
+  {
+    colorCorrection = s;
+    sendColorCorrectionPacket();
+  }
+
+  // Send a packet with the current firmware configuration settings
+  void sendFirmwareConfigPacket()
+  {
+    if (output == null) {
+      // We'll do this when we reconnect
+      return;
+    }
+ 
+    byte[] packet = new byte[9];
+    packet[0] = 0;          // Channel (reserved)
+    packet[1] = (byte)0xFF; // Command (System Exclusive)
+    packet[2] = 0;          // Length high byte
+    packet[3] = 5;          // Length low byte
+    packet[4] = 0x00;       // System ID high byte
+    packet[5] = 0x01;       // System ID low byte
+    packet[6] = 0x00;       // Command ID high byte
+    packet[7] = 0x02;       // Command ID low byte
+    packet[8] = firmwareConfig;
+
+    try {
+      output.write(packet);
+    } catch (Exception e) {
+      dispose();
+    }
+  }
+
+  // Send a packet with the current color correction settings
+  void sendColorCorrectionPacket()
+  {
+    if (colorCorrection == null) {
+      // No color correction defined
+      return;
+    }
+    if (output == null) {
+      // We'll do this when we reconnect
+      return;
+    }
+
+    byte[] content = colorCorrection.getBytes();
+    int packetLen = content.length + 4;
+    byte[] header = new byte[8];
+    header[0] = 0;          // Channel (reserved)
+    header[1] = (byte)0xFF; // Command (System Exclusive)
+    header[2] = (byte)(packetLen >> 8);
+    header[3] = (byte)(packetLen & 0xFF);
+    header[4] = 0x00;       // System ID high byte
+    header[5] = 0x01;       // System ID low byte
+    header[6] = 0x00;       // Command ID high byte
+    header[7] = 0x01;       // Command ID low byte
+
+    try {
+      output.write(header);
+      output.write(content);
+    } catch (Exception e) {
+      dispose();
+    }
+  }
+
+  // Automatically called at the end of each draw().
+  // This handles the automatic Pixel to LED mapping.
+  // If you aren't using that mapping, this function has no effect.
+  // In that case, you can call setPixelCount(), setPixel(), and writePixels()
+  // separately.
+  void draw()
+  {
+    if (pixelLocations == null) {
+      // No pixels defined yet
+      return;
+    }
+ 
+    if (output == null) {
+      // Try to (re)connect
+      connect();
+    }
+    if (output == null) {
+      return;
+    }
+
+    int numPixels = pixelLocations.length;
+    int ledAddress = 4;
+
+    setPixelCount(numPixels);
+    loadPixels();
+
+    for (int i = 0; i < numPixels; i++) {
+      int pixelLocation = pixelLocations[i];
+      int pixel = pixels[pixelLocation];
+
+      packetData[ledAddress] = (byte)(pixel >> 16);
+      packetData[ledAddress + 1] = (byte)(pixel >> 8);
+      packetData[ledAddress + 2] = (byte)pixel;
+      ledAddress += 3;
+
+      if (enableShowLocations) {
+        pixels[pixelLocation] = 0xFFFFFF ^ pixel;
+      }
+    }
+
+    writePixels();
+
+    if (enableShowLocations) {
+      updatePixels();
+    }
+  }
+  
+  // Change the number of pixels in our output packet.
+  // This is normally not needed; the output packet is automatically sized
+  // by draw() and by setPixel().
+  void setPixelCount(int numPixels)
+  {
+    int numBytes = 3 * numPixels;
+    int packetLen = 4 + numBytes;
+    if (packetData == null || packetData.length != packetLen) {
+      // Set up our packet buffer
+      packetData = new byte[packetLen];
+      packetData[0] = 0;  // Channel
+      packetData[1] = 0;  // Command (Set pixel colors)
+      packetData[2] = (byte)(numBytes >> 8);
+      packetData[3] = (byte)(numBytes & 0xFF);
+    }
+  }
+  
+  // Directly manipulate a pixel in the output buffer. This isn't needed
+  // for pixels that are mapped to the screen.
+  void setPixel(int number, color c)
+  {
+    int offset = 4 + number * 3;
+    if (packetData == null || packetData.length < offset + 3) {
+      setPixelCount(number + 1);
+    }
+
+    packetData[offset] = (byte) (c >> 16);
+    packetData[offset + 1] = (byte) (c >> 8);
+    packetData[offset + 2] = (byte) c;
+  }
+  
+  // Read a pixel from the output buffer. If the pixel was mapped to the display,
+  // this returns the value we captured on the previous frame.
+  color getPixel(int number)
+  {
+    int offset = 4 + number * 3;
+    if (packetData == null || packetData.length < offset + 3) {
+      return 0;
+    }
+    return (packetData[offset] << 16) | (packetData[offset + 1] << 8) | packetData[offset + 2];
+  }
+
+  // Transmit our current buffer of pixel values to the OPC server. This is handled
+  // automatically in draw() if any pixels are mapped to the screen, but if you haven't
+  // mapped any pixels to the screen you'll want to call this directly.
+  void writePixels()
+  {
+    if (packetData == null || packetData.length == 0) {
+      // No pixel buffer
+      return;
+    }
+    if (output == null) {
+      // Try to (re)connect
+      connect();
+    }
+    if (output == null) {
+      return;
+    }
+
+    try {
+      output.write(packetData);
+    } catch (Exception e) {
+      dispose();
+    }
+  }
+
+  void dispose()
+  {
+    // Destroy the socket. Called internally when we've disconnected.
+    if (output != null) {
+      println("Disconnected from OPC server");
+    }
+    socket = null;
+    output = null;
+  }
+
+  void connect()
+  {
+    // Try to connect to the OPC server. This normally happens automatically in draw()
+    try {
+      socket = new Socket(host, port);
+      socket.setTcpNoDelay(true);
+      output = socket.getOutputStream();
+      println("Connected to OPC server");
+    } catch (ConnectException e) {
+      dispose();
+    } catch (IOException e) {
+      dispose();
+    }
+    
+    sendColorCorrectionPacket();
+    sendFirmwareConfigPacket();
+  }
+}
+
diff --git a/examples/processing/grid32x16z_attractor/Particle.pde b/examples/processing/grid32x16z_attractor/Particle.pde
new file mode 100644
index 0000000000000000000000000000000000000000..0bc0a9ca498c9d52378d15db2868a6362950f5e3
--- /dev/null
+++ b/examples/processing/grid32x16z_attractor/Particle.pde
@@ -0,0 +1,44 @@
+class Particle
+{
+  PVector center;
+  PVector velocity;
+  color rgb;
+
+  Particle(float x, float y, color rgb)
+  {
+    center = new PVector(x, y);
+    velocity = new PVector(0, 0); 
+    this.rgb = rgb;
+  }
+  
+  void damp(float factor)
+  {
+    velocity.mult(factor);
+  }
+
+  void integrate()
+  {
+    center.add(velocity);
+  }
+
+  void draw(float opacity) 
+  {
+    float size = height * 0.5;
+    tint(rgb, opacity);
+    blendMode(ADD);
+    image(dot, center.x - size/2, center.y - size/2, size, size);
+  }  
+  
+  void attract(PVector v, float coefficient)
+  {
+    PVector d = PVector.sub(v, center);
+    d.mult(coefficient / max(1, d.magSq()));
+    velocity.add(d);
+  }
+
+  float energy()
+  {
+    return velocity.magSq();
+  }
+}
+
diff --git a/examples/processing/grid32x16z_attractor/data/colors.png b/examples/processing/grid32x16z_attractor/data/colors.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d88b5b8bbdb022915e290bb98786432e2a331c7
Binary files /dev/null and b/examples/processing/grid32x16z_attractor/data/colors.png differ
diff --git a/examples/processing/grid32x16z_attractor/data/dot.png b/examples/processing/grid32x16z_attractor/data/dot.png
new file mode 100644
index 0000000000000000000000000000000000000000..908720a8a24f56f8c0add1c05dfe1ddbad25d6d6
Binary files /dev/null and b/examples/processing/grid32x16z_attractor/data/dot.png differ
diff --git a/examples/processing/grid32x16z_attractor/grid32x16z_attractor.pde b/examples/processing/grid32x16z_attractor/grid32x16z_attractor.pde
new file mode 100644
index 0000000000000000000000000000000000000000..c7b6185fe04e86a24c2eba89cca4749efad998bf
--- /dev/null
+++ b/examples/processing/grid32x16z_attractor/grid32x16z_attractor.pde
@@ -0,0 +1,123 @@
+// Particle system with multiple attraction points.
+// Spawns centered around a random point, lives out a cycle and dies; the cycle repeats.
+
+int numParticles = 30;
+float cornerCoefficient = 0.2;
+int integrationSteps = 20;
+float maxOpacity = 100;
+float stepFast = 1.0 / 40;
+float stepSlow = 1.0 / 1000;
+float energyThreshold = 10.0;
+float brightnessThreshold = 0.8;
+
+OPC opc;
+PImage dot;
+PImage colors;
+Particle[] particles;
+PVector[] corners;
+float epoch = 0;
+
+void setup()
+{
+  size(600, 300, P3D);
+  frameRate(30);
+
+  dot = loadImage("dot.png");
+  colors = loadImage("colors.png");
+  colors.loadPixels();
+
+  // Connect to the local instance of fcserver
+  opc = new OPC(this, "127.0.0.1", 7890);
+
+  opc.ledGrid8x8(0 * 64, width * 1/8, height * 1/4, height/16, 0, true);
+  opc.ledGrid8x8(1 * 64, width * 3/8, height * 1/4, height/16, 0, true);
+  opc.ledGrid8x8(2 * 64, width * 5/8, height * 1/4, height/16, 0, true);
+  opc.ledGrid8x8(3 * 64, width * 7/8, height * 1/4, height/16, 0, true);
+  opc.ledGrid8x8(4 * 64, width * 1/8, height * 3/4, height/16, 0, true);
+  opc.ledGrid8x8(5 * 64, width * 3/8, height * 3/4, height/16, 0, true);
+  opc.ledGrid8x8(6 * 64, width * 5/8, height * 3/4, height/16, 0, true);
+  opc.ledGrid8x8(7 * 64, width * 7/8, height * 3/4, height/16, 0, true);
+
+  // Attraction points
+  corners = new PVector[3];
+  corners[0] = new PVector(width * 1/4, height * 0.5);
+  corners[1] = new PVector(width * 2/4, height * 0.5);
+  corners[2] = new PVector(width * 3/4, height * 0.5);
+ 
+  beginEpoch();
+}
+
+void beginEpoch()
+{
+  epoch = 0;
+ 
+  // Center of bundle
+  float s = 0.5;
+  float cx = width * (0.5 + random(-s, s));
+  float cy = height * (0.5 + random(-s, s));
+ 
+  // Half-width of particle bundle
+  float w = width * 0.2;
+ 
+  particles = new Particle[numParticles];
+  for (int i = 0; i < particles.length; i++) {
+    color rgb = colors.pixels[int(random(0, colors.width * colors.height))];
+    particles[i] = new Particle(
+      cx + random(-w, w),
+      cy + random(-w, w), rgb);
+  }
+}
+
+void draw()
+{
+  background(0);
+  
+  // How much energy is still left?
+  float energy = 0;
+  for (int i = 0; i < particles.length; i++) {
+    energy += particles[i].energy();
+  }
+  
+  // How bright is our brightest pixel?
+  float brightness = 0;
+  for (int i = 0; i < opc.pixelLocations.length; i++) {
+    color rgb = opc.getPixel(i);
+    brightness = max(brightness, max(red(rgb), max(blue(rgb), green(rgb))));
+  }
+  brightness /= 255.0;
+  
+  text("Energy: " + energy, 2, 12);
+  text("Brightness: " + brightness, 2, 25);
+
+  // What's interesting? Can we maintain high brightness and high energy?
+  // These are normally conflicting goals. If we've managed to balance the two,
+  // keep going to see how it turns out.
+  if (energy > energyThreshold && brightness > brightnessThreshold) {
+ 
+    // Time moves slower when we're interested
+    epoch += stepSlow;
+    text("+", 2, 40);
+  } else {
+    epoch += stepFast;
+  }
+  
+  if (epoch > 1) {
+    beginEpoch();
+  }
+    
+  for (int step = 0; step < integrationSteps; step++) {
+    for (int i = 0; i < particles.length; i++) {
+      particles[i].integrate();
+
+      // Each particle is attracted by the corners
+      for (int j = 0; j < corners.length; j++) {
+        particles[i].attract(corners[j], cornerCoefficient);
+      }
+    }
+  }
+
+  for (int i = 0; i < particles.length; i++) {
+    particles[i].draw(sin(epoch * PI) * maxOpacity);
+  }
+}
+