From 01a5d2153ebd742dac3c0d41fbb1bbbc01f0a431 Mon Sep 17 00:00:00 2001
From: Micah Elizabeth Scott <micah@scanlime.org>
Date: Thu, 13 Feb 2014 10:37:37 -0800
Subject: [PATCH] New "ember" example, contributed by April Arcus

---
 .../processing/triangle16_ember/KtoRGB.pde    |  15 +
 examples/processing/triangle16_ember/OPC.pde  | 349 ++++++++++++++++++
 .../processing/triangle16_ember/Particle.pde  |  28 ++
 .../triangle16_ember/TriangleGrid.pde         | 103 ++++++
 .../triangle16_ember/data/bb-rampcomp.png     | Bin 0 -> 3081 bytes
 .../processing/triangle16_ember/data/dot.png  | Bin 0 -> 2501 bytes
 .../triangle16_ember/triangle16_ember.pde     |  55 +++
 7 files changed, 550 insertions(+)
 create mode 100644 examples/processing/triangle16_ember/KtoRGB.pde
 create mode 100644 examples/processing/triangle16_ember/OPC.pde
 create mode 100644 examples/processing/triangle16_ember/Particle.pde
 create mode 100644 examples/processing/triangle16_ember/TriangleGrid.pde
 create mode 100644 examples/processing/triangle16_ember/data/bb-rampcomp.png
 create mode 100644 examples/processing/triangle16_ember/data/dot.png
 create mode 100644 examples/processing/triangle16_ember/triangle16_ember.pde

diff --git a/examples/processing/triangle16_ember/KtoRGB.pde b/examples/processing/triangle16_ember/KtoRGB.pde
new file mode 100644
index 0000000..0285415
--- /dev/null
+++ b/examples/processing/triangle16_ember/KtoRGB.pde
@@ -0,0 +1,15 @@
+class KtoRGB
+{
+  PImage ramp;
+  float min = 1000;
+  float max = 10000;
+  
+  KtoRGB(){
+    ramp = loadImage("bb-rampcomp.png");
+    ramp.loadPixels();
+  }
+  
+  color convert(float Kelvins) {
+    return ramp.pixels[int(constrain(map(Kelvins,min,max,0,ramp.width),0,ramp.width-1))];
+  }
+};
diff --git a/examples/processing/triangle16_ember/OPC.pde b/examples/processing/triangle16_ember/OPC.pde
new file mode 100644
index 0000000..917035d
--- /dev/null
+++ b/examples/processing/triangle16_ember/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/triangle16_ember/Particle.pde b/examples/processing/triangle16_ember/Particle.pde
new file mode 100644
index 0000000..e745b99
--- /dev/null
+++ b/examples/processing/triangle16_ember/Particle.pde
@@ -0,0 +1,28 @@
+class Particle
+{
+  PVector center;
+  float temperature;
+  float swerve = 1.5;
+  float dieoff = 1.005;
+  
+  Particle(float y, float temperature)
+  {
+    center = new PVector(random(width), y);
+    this.temperature = temperature;
+  }
+
+  void draw() 
+  {
+    temperature /= random(dieoff-.001,dieoff+.001);
+    center.y -= random(2*swerve*(temperature/heat));
+    //center.x += random(-swerve,swerve);
+    color rgb = KtoRGB.convert(temperature);
+    int opacity = 255;
+    float size = height * 0.4;
+    tint(rgb, opacity);
+    blendMode(ADD);
+    image(dot, center.x - size/2, center.y - size/2, size, size);
+  }
+
+}
+
diff --git a/examples/processing/triangle16_ember/TriangleGrid.pde b/examples/processing/triangle16_ember/TriangleGrid.pde
new file mode 100644
index 0000000..0515267
--- /dev/null
+++ b/examples/processing/triangle16_ember/TriangleGrid.pde
@@ -0,0 +1,103 @@
+/*
+ * Object for keeping track of the layout of our triangular grid.
+ * The triangle is made of cells, which have information about their
+ * connectedness to nearby cells.
+ */
+
+public class TriangleGrid
+{
+  Cell[] cells;
+  
+  class Cell
+  {
+    PVector center;
+    int[] neighbors;
+
+    Cell(float cx, float cy, int n1, int n2, int n3)
+    {
+      this.center = new PVector(cx, cy);
+      this.neighbors = new int[3];
+      this.neighbors[0] = n1;
+      this.neighbors[1] = n2;
+      this.neighbors[2] = n3;
+    }
+  };
+
+  void grid16()
+  {
+    // Layout for a 16-cell triangular grid.
+
+    // Each triangle side is 1 unit. "h" is the triangle height
+    float h = sin(radians(60));
+
+    cells = new Cell[16];
+
+    // Bottom row, left to right 
+    cells[ 0] = new Cell( 0.0, h*0 + h*1/3,  -1,  1, -1 ); 
+    cells[ 1] = new Cell( 0.5, h*0 + h*2/3,  11,  2,  0 ); 
+    cells[ 2] = new Cell( 1.0, h*0 + h*1/3,  -1,  3,  1 ); 
+    cells[ 3] = new Cell( 1.5, h*0 + h*2/3,   9,  4,  2 ); 
+    cells[ 4] = new Cell( 2.0, h*0 + h*1/3,  -1,  5,  3 ); 
+    cells[ 5] = new Cell( 2.5, h*0 + h*2/3,   7,  6,  4 ); 
+    cells[ 6] = new Cell( 3.0, h*0 + h*1/3,  -1, -1,  5 ); 
+
+    // Second row, right to left
+    cells[ 7] = new Cell( 2.5, h*1 + h*1/3,   5,  8, -1 ); 
+    cells[ 8] = new Cell( 2.0, h*1 + h*2/3,  12,  9,  7 ); 
+    cells[ 9] = new Cell( 1.5, h*1 + h*1/3,   3, 10,  8 ); 
+    cells[10] = new Cell( 1.0, h*1 + h*2/3,  14, 11,  9 ); 
+    cells[11] = new Cell( 0.5, h*1 + h*1/3,   1, -1, 10 ); 
+
+    // Third row, left to right
+    cells[12] = new Cell( 1.0, h*2 + h*1/3,   8, 13, -1 ); 
+    cells[13] = new Cell( 1.5, h*2 + h*2/3,  15, 14, 13 ); 
+    cells[14] = new Cell( 2.0, h*2 + h*1/3,  10, -1, 14 ); 
+
+    // Top
+    cells[15] = new Cell( 1.5, h*3 + h*1/3,  13, -1, -1 );     
+
+    // Move the centroid to the origin
+    translate(-1.5, -h*4/3);
+  }
+
+  void leds(OPC opc, int index)
+  {
+    // Create LED mappings, using the current grid coordinates
+    for (int i = 0; i < cells.length; i++) {
+      opc.led(index + i, int(cells[i].center.x + 0.5), int(cells[i].center.y + 0.5));
+    }
+  }
+  
+  void translate(float x, float y)
+  {
+    // Translate all points by this amount
+    PVector t = new PVector(x, y);
+    for (int i = 0; i < cells.length; i++) {
+      cells[i].center.add(t);
+    }
+  }
+  
+  void mirror()
+  {
+    // Mirror all points left-to-right
+    for (int i = 0; i < cells.length; i++) {
+      cells[i].center.x = -cells[i].center.x;
+    }
+  }
+  
+  void scale(float s)
+  {
+    // Scale all points by this amount
+    for (int i = 0; i < cells.length; i++) {
+      cells[i].center.mult(s);
+    }
+  }
+ 
+  void rotate(float angle)
+  {
+    // Rotate all points around the origin by this angle, in radians
+    for (int i = 0; i < cells.length; i++) {
+      cells[i].center.rotate(angle);
+    }
+  }
+};
diff --git a/examples/processing/triangle16_ember/data/bb-rampcomp.png b/examples/processing/triangle16_ember/data/bb-rampcomp.png
new file mode 100644
index 0000000000000000000000000000000000000000..06354f09761f42e5e1a85b7159488650cb5bf8b0
GIT binary patch
literal 3081
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
XsgJA(%HhP_00000NkvXXu0mjf^ohO(

literal 0
HcmV?d00001

diff --git a/examples/processing/triangle16_ember/data/dot.png b/examples/processing/triangle16_ember/data/dot.png
new file mode 100644
index 0000000000000000000000000000000000000000..908720a8a24f56f8c0add1c05dfe1ddbad25d6d6
GIT binary patch
literal 2501
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
P00000NkvXXu0mjfn%0?+

literal 0
HcmV?d00001

diff --git a/examples/processing/triangle16_ember/triangle16_ember.pde b/examples/processing/triangle16_ember/triangle16_ember.pde
new file mode 100644
index 0000000..07c9f57
--- /dev/null
+++ b/examples/processing/triangle16_ember/triangle16_ember.pde
@@ -0,0 +1,55 @@
+// April Arcus, 2014
+
+int numParticles = 10;
+
+OPC opc;
+PImage dot;
+PImage planck;
+KtoRGB KtoRGB;
+TriangleGrid triangle;
+Particle[] particles;
+float heat;
+
+void setup()
+{
+  size(300, 300, P3D);
+  //frameRate(10);
+  heat = 8000;
+
+  dot = loadImage("dot.png");
+  KtoRGB = new KtoRGB();
+
+  // Connect to the local instance of fcserver
+  opc = new OPC(this, "127.0.0.1", 7890);
+
+  // Map our triangle grid to the center of the window
+  triangle = new TriangleGrid();
+  triangle.grid16();
+  triangle.mirror();
+  triangle.rotate(radians(60));
+  triangle.scale(height * 0.2);
+  triangle.translate(width * 0.5, height * 0.5);
+  triangle.leds(opc, 0);
+  
+  particles = new Particle[numParticles];
+  
+  for (int i=0; i < particles.length; i++) {
+     particles[i] = new Particle(random(height),heat); 
+  }
+
+}
+
+void draw()
+{
+  background(0);
+
+  for (int i = 0; i < particles.length; i++) {
+    if (particles[i].center.x < 0 || particles[i].center.x > width || 
+        particles[i].center.y < 0 || particles[i].center.y > height ||
+        particles[i].temperature < 800) {
+      particles[i] = new Particle(height-50,heat);
+    }
+    particles[i].draw();
+  }
+}
+
-- 
GitLab