modified mappings to render more cubes, modified internals
authorAlexander Green <alexandergreen22@gmail.com>
Thu, 7 Nov 2013 11:13:52 +0000 (03:13 -0800)
committerAlexander Green <alexandergreen22@gmail.com>
Thu, 7 Nov 2013 11:13:52 +0000 (03:13 -0800)
_Internals.pde
_Mappings.pde
build-tmp/source/SugarCubes.java [new file with mode: 0644]

index 61f6c7d08d40009b22dbf8da487ed20c2ade8140..cdd4ba5ac64d755b4665fc4f5070e2b15b48fb72 100644 (file)
@@ -40,8 +40,8 @@ final float TRAILER_WIDTH = 240;
 final float TRAILER_DEPTH = 97;
 final float TRAILER_HEIGHT = 33;
 
-final int MaxCubeHeight = 5;
-final int NumBackTowers = 11;
+final int MaxCubeHeight = 7;
+final int NumBackTowers = 18;
 
 int targetFramerate = 60;
 int startMillis, lastMillis;
index 0824cc6f236f3c7924124f9c27409c086daccef7..7ace1a679244e1e16a3492ce0d2f0133fd885a60 100644 (file)
@@ -75,13 +75,18 @@ public Model buildModel() {
   ////////////////////////////////////////////////////////////////////////
   // dan's proposed lattice
         ArrayList<StaggeredTower> scubes = new ArrayList<StaggeredTower>();
-        if (NumBackTowers != 11) exit();
-        // for (int i=0; i<NumBackTowers; i++) scubes.add(new StaggeredTower(
-        //           (i+1)*CW,                                                                 // x
-        //           (i % 2 == 0) ? 0 : CH * 2./3.                ,   // y
-        //          - ((i % 2 == 0) ? 0 : 11) + 97          ,   // z
-        //          225, (i % 2 == 0) ? MaxCubeHeight : MaxCubeHeight-1) );         // num cubes
+        //if (NumBackTowers != 25) exit();
+        for (int i=0; i<NumBackTowers/2; i++) scubes.add(new StaggeredTower(
+                  (i+1)*CW,                                                                 // x
+                  (i % 2 == 0) ? 0 : CH * 2./3.                ,   // y
+                 - ((i % 2 == 0) ? 0 : 11) + 97          ,   // z
+                 225, (i % 2 == 0) ? MaxCubeHeight : MaxCubeHeight-1) );         // num cubes
         
+        for (int i=0; i<NumBackTowers/2; i++) scubes.add(new StaggeredTower(
+                  (i+1)*CW,                                                                 // x
+                  (i % 2 == 0) ? 0 : CH * 2./3.                ,   // y
+                 - ((i % 2 == 0) ? 0 : 11) + 97 -pow(CH*CH + CW*CW, .5),   // z
+                 225, (i % 2 == 0) ? MaxCubeHeight : MaxCubeHeight-1) ); 
         ArrayList<Cube> dcubes = new ArrayList<Cube>();
         // for (int i=1; i<6; i++) {
         //         if (i>1) dcubes.add(new Cube(-6+CW*4/3*i             , 0, 0, 0, 0, 0, WRR));        
@@ -89,82 +94,82 @@ public Model buildModel() {
         // }
 
 float current_x_position = 0;
-scubes.add(new StaggeredTower(//tower 1
-      current_x_position,               // x
-       15   ,   // y
-       0  ,   // z
-     45, 6, new Cube.Wiring[] { WFL, WRR, WFL, WRR, WFL, WRR}) );
-current_x_position += 25.25;
-scubes.add(new StaggeredTower(// tower 2
-      current_x_position,               // x
-       0  ,   // y
-       -10.5   ,   // z
-     45, 6, new Cube.Wiring[] { WFR, WFL, WRR, WRR, WFL, WRR}) );
-current_x_position += 25.25;
-scubes.add(new StaggeredTower(//tower 3
-      current_x_position,               // x
-       15   ,   // y
-       0,   // z
-     45, 6, new Cube.Wiring[] { WRR, WFL, WRR, WRR, WFL, WRR}) );
-current_x_position += 25.25;
-scubes.add(new StaggeredTower(//tower 4
-    current_x_position,               // x
-       0,   // y
-       -10.5  ,   // z
-     45, 6, new Cube.Wiring[] { WFL, WRR, WFL, WRR, WFL, WRR}) );
-current_x_position += 28;
-scubes.add(new StaggeredTower(//tower 5
-      current_x_position,               // x
-       15   ,   // y
-       -4.5 ,   // z
-     45, 6, new Cube.Wiring[] { WRR, WFL, WRR, WFL, WRR, WFL}) );
-current_x_position += 28;
-scubes.add(new StaggeredTower(//tower 6
-      current_x_position,               // x
-       0 ,   // y
-       -10.5,   // z
-     45, 6, new Cube.Wiring[] { WFL, WRR, WFL, WRR, WFL, WRR}) );
-current_x_position += 25.25;
-scubes.add(new StaggeredTower(// tower 7
-      current_x_position,               // x
-       15   ,   // y
-      0,   // z
-     45, 6, new Cube.Wiring[] { WRR, WFL, WRR, WFL, WRR, WFL}) );
-current_x_position += 25.25;     
-scubes.add(new StaggeredTower(//tower 8
-      current_x_position,               // x
-       0  ,   // y
-       -10.5 ,   // z
-     45, 6, new Cube.Wiring[] { WFL, WRR, WFL, WRR, WFL, WRR}) );
-current_x_position += 25.25;
-scubes.add(new StaggeredTower(//tower 9
-      current_x_position,               // x
-       15   ,   // y
-       0,   // z
-     45, 6, new Cube.Wiring[] { WFL, WRR, WFL, WRR, WFL, WRR}) );
-current_x_position += 25.25;
+// scubes.add(new StaggeredTower(//tower 1
+//       current_x_position,               // x
+//        15   ,   // y
+//        0  ,   // z
+//      45, 6, new Cube.Wiring[] { WFL, WRR, WFL, WRR, WFL, WRR}) );
+// current_x_position += 25.25;
+// scubes.add(new StaggeredTower(// tower 2
+//       current_x_position,               // x
+//        0  ,   // y
+//        -10.5   ,   // z
+//      45, 6, new Cube.Wiring[] { WFR, WFL, WRR, WRR, WFL, WRR}) );
+// current_x_position += 25.25;
+// scubes.add(new StaggeredTower(//tower 3
+//       current_x_position,               // x
+//        15   ,   // y
+//        0,   // z
+//      45, 6, new Cube.Wiring[] { WRR, WFL, WRR, WRR, WFL, WRR}) );
+// current_x_position += 25.25;
+// scubes.add(new StaggeredTower(//tower 4
+//     current_x_position,               // x
+//        0,   // y
+//        -10.5  ,   // z
+//      45, 6, new Cube.Wiring[] { WFL, WRR, WFL, WRR, WFL, WRR}) );
+// current_x_position += 28;
+// scubes.add(new StaggeredTower(//tower 5
+//       current_x_position,               // x
+//        15   ,   // y
+//        -4.5 ,   // z
+//      45, 6, new Cube.Wiring[] { WRR, WFL, WRR, WFL, WRR, WFL}) );
+// current_x_position += 28;
+// scubes.add(new StaggeredTower(//tower 6
+//       current_x_position,               // x
+//        0 ,   // y
+//        -10.5,   // z
+//      45, 6, new Cube.Wiring[] { WFL, WRR, WFL, WRR, WFL, WRR}) );
+// current_x_position += 25.25;
+// scubes.add(new StaggeredTower(// tower 7
+//       current_x_position,               // x
+//        15   ,   // y
+//       0,   // z
+//      45, 6, new Cube.Wiring[] { WRR, WFL, WRR, WFL, WRR, WFL}) );
+// current_x_position += 25.25;     
+// scubes.add(new StaggeredTower(//tower 8
+//       current_x_position,               // x
+//        0  ,   // y
+//        -10.5 ,   // z
+//      45, 6, new Cube.Wiring[] { WFL, WRR, WFL, WRR, WFL, WRR}) );
+// current_x_position += 25.25;
+// scubes.add(new StaggeredTower(//tower 9
+//       current_x_position,               // x
+//        15   ,   // y
+//        0,   // z
+//      45, 6, new Cube.Wiring[] { WFL, WRR, WFL, WRR, WFL, WRR}) );
+// current_x_position += 25.25;
 
-//TOWERS ON DANCE FLOOR
-scubes.add(new StaggeredTower(//tower 10
-      83.75+39+43-124.5,   // x
-      0,   // y
-       -47.5-43,   // z
-     45,  4, new Cube.Wiring[]{ WRR, WFL, WFL, WRR})  ); 
-scubes.add(new StaggeredTower(//tower 11
-      83.75,   // x
-       0,   // y
-       -47.5,   // z
-     45,  4, new Cube.Wiring[]{ WFL, WRR, WRR, WFL})  );  
-scubes.add(new StaggeredTower(//tower 12
-      83.75+39,   // x
-       0,   // y
-       -47.5,   // z
-     45,  4, new Cube.Wiring[]{ WRR, WFL, WFL, WRR})  ); 
-scubes.add(new StaggeredTower(//tower 13
-       83.75+39+43,   // x
-       0,   // y
-       -47.5-43,   // z
-     45,  4, new Cube.Wiring[]{ WFL, WRR, WFL, WRR})  ); 
+// //TOWERS ON DANCE FLOOR
+// scubes.add(new StaggeredTower(//tower 10
+//       83.75+39+43-124.5,   // x
+//       0,   // y
+//        -47.5-43,   // z
+//      45,  4, new Cube.Wiring[]{ WRR, WFL, WFL, WRR})  ); 
+// scubes.add(new StaggeredTower(//tower 11
+//       83.75,   // x
+//        0,   // y
+//        -47.5,   // z
+//      45,  4, new Cube.Wiring[]{ WFL, WRR, WRR, WFL})  );  
+// scubes.add(new StaggeredTower(//tower 12
+//       83.75+39,   // x
+//        0,   // y
+//        -47.5,   // z
+//      45,  4, new Cube.Wiring[]{ WRR, WFL, WFL, WRR})  ); 
+// scubes.add(new StaggeredTower(//tower 13
+//        83.75+39+43,   // x
+//        0,   // y
+//        -47.5-43,   // z
+//      45,  4, new Cube.Wiring[]{ WFL, WRR, WFL, WRR})  ); 
 
 // scubes.add(new StaggeredTower(// Single cube on top of tower 4
 //       42,               // x
@@ -185,7 +190,7 @@ scubes.add(new StaggeredTower(//tower 13
   // These guts just convert the shorthand mappings into usable objects
   ArrayList<Tower> towerList = new ArrayList<Tower>();
   ArrayList<Cube> tower;
-  Cube[] cubes = new Cube[100];
+  Cube[] cubes = new Cube[200];
   int cubeIndex = 1;  
   float px, pz, ny;
   for (TowerMapping tm : towerCubes) {
diff --git a/build-tmp/source/SugarCubes.java b/build-tmp/source/SugarCubes.java
new file mode 100644 (file)
index 0000000..edd72f4
--- /dev/null
@@ -0,0 +1,6279 @@
+import processing.core.*; 
+import processing.data.*; 
+import processing.event.*; 
+import processing.opengl.*; 
+
+import netP5.*; 
+import oscP5.*; 
+import processing.serial.*; 
+import java.util.LinkedHashMap; 
+import toxi.geom.Vec3D; 
+import toxi.geom.Matrix4x4; 
+
+import heronarts.lx.font.*; 
+import heronarts.lx.transition.*; 
+import glucose.transform.*; 
+import netP5.*; 
+import heronarts.lx.pattern.*; 
+import glucose.pattern.*; 
+import heronarts.lx.model.*; 
+import toxi.geom.mesh2d.*; 
+import heronarts.lx.client.*; 
+import glucose.*; 
+import toxi.util.datatypes.*; 
+import toxi.math.waves.*; 
+import heronarts.lx.kinet.*; 
+import oscP5.*; 
+import toxi.geom.*; 
+import toxi.util.events.*; 
+import heronarts.lx.modulator.*; 
+import rwmidi.*; 
+import glucose.transition.*; 
+import glucose.effect.*; 
+import glucose.model.*; 
+import toxi.math.conversion.*; 
+import heronarts.lx.effect.*; 
+import heronarts.lx.control.*; 
+import glucose.control.*; 
+import toxi.math.noise.*; 
+import toxi.util.*; 
+import heronarts.lx.*; 
+import toxi.math.*; 
+import heronarts.lx.audio.*; 
+
+import java.util.HashMap; 
+import java.util.ArrayList; 
+import java.io.File; 
+import java.io.BufferedReader; 
+import java.io.PrintWriter; 
+import java.io.InputStream; 
+import java.io.OutputStream; 
+import java.io.IOException; 
+
+public class SugarCubes extends PApplet {
+
+/**
+ *           +-+-+-+-+-+               +-+-+-+-+-+
+ *          /         /|               |\         \
+ *         /         / +               + \         \
+ *        +-+-+-+-+-+  |   +-+-+-+-+   |  +-+-+-+-+-+
+ *        |         |  +  /         \  +  |         |
+ *        +   THE   + /  /           \  \ +  CUBES  +
+ *        |         |/  +-+-+-+-+-+-+-+  \|         |
+ *        +-+-+-+-+-+   |             |   +-+-+-+-+-+
+ *                      +             +
+ *                      |    SUGAR    |
+ *                      +             +
+ *                      |             |
+ *                      +-+-+-+-+-+-+-+
+ *
+ * Welcome to the Sugar Cubes! This Processing sketch is a fun place to build
+ * animations, effects, and interactions for the platform. Most of the icky
+ * code guts are embedded in the GLucose library extension. If you're an
+ * artist, you shouldn't need to worry about any of that.
+ *
+ * Below, you will find definitions of the Patterns, Effects, and Interactions.
+ * If you're an artist, create a new tab in the Processing environment with
+ * your name. Implement your classes there, and add them to the list below.
+ */ 
+
+public LXPattern[] patterns(GLucose glucose) {
+  return new LXPattern[] {
+
+    
+    // Slee
+    new Cathedrals(glucose),
+    new MidiMusic(glucose),
+    new Pulley(glucose),
+    new Swarm(glucose),
+    new ViolinWave(glucose),
+    new BouncyBalls(glucose),
+    new SpaceTime(glucose),
+    new ShiftingPlane(glucose),
+    new AskewPlanes(glucose),
+    new Blinders(glucose),
+    new CrossSections(glucose),
+    new Psychedelia(glucose),
+    
+    new Traktor(glucose).setEligible(false),
+    new BassPod(glucose).setEligible(false),
+    new CubeEQ(glucose).setEligible(false),
+    new PianoKeyPattern(glucose).setEligible(false),
+
+    // DanH
+    new Noise(glucose),
+    new Play (glucose),
+    new Pong (glucose),
+    new Worms(glucose),
+
+    // Alex G
+     new SineSphere(glucose),
+//     new CubeCurl(glucose),
+
+    // Shaheen
+    new HelixPattern(glucose).setEligible(false),
+    
+    // Toby
+    new GlitchPlasma(glucose),
+    new FireEffect(glucose).setEligible(false),
+    new StripBounce(glucose),
+    new SoundRain(glucose).setEligible(false),
+    new SoundSpikes(glucose).setEligible(false),
+    new FaceSync(glucose),
+
+    // Jack
+    new Swim(glucose),
+    new Balance(glucose),
+
+    // Tim
+    new TimPlanes(glucose),
+    new TimPinwheels(glucose),
+    new TimRaindrops(glucose),
+    new TimCubes(glucose),
+    // new TimTrace(glucose),
+    new TimSpheres(glucose),
+
+    // Ben
+    // new Sandbox(glucose),
+    new TowerParams(glucose),
+    new DriveableCrossSections(glucose),
+    new GranimTestPattern2(glucose),
+    
+    //JR
+    new Gimbal(glucose),
+    
+    // Sam
+    new JazzRainbow(glucose),
+    
+    // Arjun
+    new TelevisionStatic(glucose),
+    new AbstractPainting(glucose),
+    new Spirality(glucose),
+
+    // Basic test patterns for reference, not art    
+    new TestCubePattern(glucose),
+    new TestTowerPattern(glucose),
+    new TestProjectionPattern(glucose),
+    new TestStripPattern(glucose),
+    new TestBassMapping(glucose),
+    new TestFloorMapping(glucose),
+    new TestSpeakerMapping(glucose),    
+    new TestPerformancePattern(glucose),
+    // new TestHuePattern(glucose),
+    // new TestXPattern(glucose),
+    // new TestYPattern(glucose),
+    // new TestZPattern(glucose),
+
+  };
+}
+
+public LXTransition[] transitions(GLucose glucose) {
+  return new LXTransition[] {
+    new DissolveTransition(lx),
+    new AddTransition(glucose),
+    new MultiplyTransition(glucose),
+    new OverlayTransition(glucose),
+    new DodgeTransition(glucose),
+    new SwipeTransition(glucose),
+    new FadeTransition(lx),
+//  new SubtractTransition(glucose),   // similar to multiply - dh
+//  new BurnTransition(glucose),               // similar to multiply - dh
+//  new ScreenTransition(glucose),             // same as add -dh
+//  new SoftLightTransition(glucose),  // same as overlay -dh
+  };
+}
+
+// Handles to globally triggerable effects 
+class Effects {
+  FlashEffect flash = new FlashEffect(lx);
+  BoomEffect boom = new BoomEffect(glucose);
+  BlurEffect blur = new BlurEffect(glucose);
+  QuantizeEffect quantize = new QuantizeEffect(glucose);
+  ColorFuckerEffect colorFucker = new ColorFuckerEffect(glucose);
+  
+  Effects() {
+    blur.enable();
+    quantize.enable();
+    colorFucker.enable();
+  }
+}
+
+class SineSphere extends SCPattern {
+  private SinLFO yrot = new SinLFO(0, TWO_PI, 2000);
+  public final Projection sinespin; 
+ float modelrad = sqrt((model.xMax)*(model.xMax) + (model.yMax)*(model.yMax) + (model.zMax)*(model.zMax));
+  Pick Sshape; 
+
+  class Sphery {
+  float f1xcenter, f1ycenter, f1zcenter, f2xcenter , f2ycenter, f2zcenter; //second three are for an ellipse with two foci
+  private  SinLFO vibration; 
+  private  SinLFO surface;
+  private  SinLFO vx;
+  private SinLFO xbounce;
+  public SinLFO ybounce;
+  private SinLFO zbounce;
+  float vibration_min, vibration_max, vperiod;
+  public BasicParameter widthparameter;
+  public BasicParameter huespread;
+  public BasicParameter bouncerate;
+  public BasicParameter bounceamp;
+  
+  
+  public Sphery(float f1xcenter, float f1ycenter, float f1zcenter, float vibration_min, float vibration_max, float vperiod) 
+  {
+   this.f1xcenter = f1xcenter;
+   this.f1ycenter = f1ycenter;
+   this.f1zcenter = f1zcenter;
+   this.vibration_min = vibration_min;
+   this.vibration_max = vibration_max;
+   this.vperiod = vperiod;
+   addParameter(bounceamp = new BasicParameter("Amp", .5f));
+   addParameter(bouncerate = new BasicParameter("Rate", .5f));  //ybounce.modulateDurationBy(bouncerate);
+   addParameter(widthparameter = new BasicParameter("Width", .1f));
+   addParameter(huespread = new BasicParameter("Hue", .2f));
+   
+   addModulator( vx = new SinLFO(-4000, 10000, 100000)).trigger() ;
+   //addModulator(xbounce = new SinLFO(model.xMax/3, 2*model.yMax/3, 2000)).trigger(); 
+   addModulator(ybounce= new SinLFO(model.yMax/3, 2*model.yMax/3, 240000.f/lx.tempo.bpm())).trigger(); //ybounce.modulateDurationBy
+    
+   //addModulator(bounceamp); //ybounce.setMagnitude(bouncerate);
+   addModulator( vibration = new SinLFO(vibration_min , vibration_max, 240000.f/lx.tempo.bpm())).trigger(); //vibration.modulateDurationBy(vx);
+   
+  }
+ public Sphery(float f1xcenter, float f1ycenter, float f1zcenter, float f2xcenter, float f2ycenter, float f2zcenter, 
+  float vibration_min, float vibration_max, float vperiod)  
+ {
+    this.f1xcenter = f1xcenter;
+   this.f1ycenter = f1ycenter;
+   this.f1zcenter = f1zcenter;
+   this.f2xcenter = f2xcenter;
+   this.f2ycenter = f2ycenter;
+   this.f2zcenter = f2zcenter;
+   this.vibration_min = vibration_min;
+   this.vibration_max = vibration_max;
+   this.vperiod = vperiod;
+   //addModulator(xbounce = new SinLFO(model.xMax/3, 2*model.yMax/3, 2000)).trigger(); 
+   addModulator(ybounce).trigger(); 
+   addModulator( vibration = new SinLFO(vibration_min , vibration_max, lx.tempo.rampf())).trigger(); //vibration.modulateDurationBy(vx);
+   addParameter(widthparameter = new BasicParameter("Width", .1f));
+   addParameter(huespread = new BasicParameter("Hue", .2f));
+  
+}
+
+
+
+
+
+public float distfromcirclecenter(float px, float py, float pz, float f1x, float f1y, float f1z) 
+{
+   return dist(px, py, pz, f1x, f1y, f1z);
+    }
+ //void updatespherey(deltaMs, )
+ public int spheryvalue (float px, float py, float pz , float f1xc, float f1yc, float f1zc) 
+ {
+//switch(sShpape.cur() ) {}  
+   return lx.hsb(constrain(huespread.getValuef()*5*px, 0, 360) , dist(px, py, pz, f1xc, f1yc, f1zc) , 
+    max(0, 100 - 100*widthparameter.getValuef()*abs(dist(px, py, pz, f1xcenter, ybounce.getValuef(), f1zcenter)
+      - vibration.getValuef() ) ) ); 
+ }
+ public int ellipsevalue(float px, float py, float pz , float f1xc, float f1yc, float f1zc, float f2xc, float f2yc, float f2zc)
+  {
+//switch(sShpape.cur() ) {}  
+   return lx.hsb(huespread.getValuef()*5*px, dist(model.xMax-px, model.yMax-py, model.zMax-pz, f1xc, f1yc, f1zc) , 
+    max(0, 100 - 100*widthparameter.getValuef() *
+      abs( (dist(px, py, pz, f1xc, ybounce.getValuef(), f1zc) + 
+        (dist(px, py , pz, f2xc, ybounce.getValuef(), f2zc) ) )/2  
+      - 1.2f*vibration.getValuef() ) ) ) ; 
+  }
+
+public void run(double deltaMs) {
+      float vv = vibration.getValuef();
+      float ybv = ybounce.getValuef();
+      
+    }
+  
+}  
+
+
+final Sphery[] spherys;
+  SineSphere(GLucose glucose) 
+  {
+    super(glucose);
+    sinespin = new Projection(model);
+    addModulator(yrot).trigger();
+    //Sshape = addPick("Shape", , 1);
+    spherys = new Sphery[] {
+      new Sphery(model.xMax/4, model.yMax/2, model.zMax/2, modelrad/16, modelrad/8, 3000),
+      new Sphery(.75f*model.xMax, model.yMax/2, model.zMax/2, modelrad/20, modelrad/10, 2000),
+      new Sphery(model.xMax/2, model.yMax/2, model.zMax/2,  modelrad/4, modelrad/8, 2300),
+    };
+  
+  }
+
+// public void onParameterChanged(LXParameter parameter)
+// {
+
+
+//     for (Sphery s : spherys) {
+//       if (s == null) continue;
+//       double bampv = s.bounceamp.getValue();
+//       double brv = s.bouncerate.getValue();
+//       double tempobounce = lx.tempo.bpm();
+//       if (parameter == s.bounceamp) 
+//       {
+//         s.ybounce.setRange(bampv*model.yMax/3 , bampv*2*model.yMax/3, brv);
+//       }
+//       else if ( parameter == s.bouncerate )   
+//       {
+//         s.ybounce.setDuration(120000./tempobounce);
+//       }
+//     }
+//   }
+
+     public void run( double deltaMs) {
+     float t = lx.tempo.rampf();
+     float bpm = lx.tempo.bpmf();
+     //spherys[1].run(deltaMs);
+     //spherys[2].run(deltaMs);
+     //spherys[3].run(deltaMs);]
+     sinespin.reset(model)
+
+     // Translate so the center of the car is the origin, offset by yPos
+      .translateCenter(model, 0, 0, 0)
+
+      // Rotate around the origin (now the center of the car) about an X-vector
+      .rotate(yrot.getValuef(), 0, 1, 0);
+
+
+
+     for (Point p: model.points){
+    int c = 0;
+    c = blendColor(c, spherys[1].spheryvalue(p.x, p.y, p.z, .75f*model.xMax, model.yMax/2, model.zMax/2), ADD);
+    c = blendColor(c, spherys[0].spheryvalue(p.x, p.y, p.z, model.xMax/4, model.yMax/4, model.zMax/2), ADD);
+    c = blendColor(c, spherys[2].spheryvalue(p.x, p.y, p.z, model.xMax/2, model.yMax/2, model.zMax/2),ADD);
+     
+      colors[p.index] = lx.hsb(lx.h(c), lx.s(c), lx.b(c));
+
+               }
+      
+
+
+  }
+  int spheremode = 0;
+  
+   // void keyPressed() {
+   //   spheremode++;
+   //     }
+
+  // color CalcPoint(PVector Px) 
+  // { 
+  //      // if (spheremode == 0 )
+              //{
+            
+             //}
+      //   else if (spheremode == 1)
+      // {
+
+      //   color c = 0;
+      //   c = blendColor(c, spherys[3].ellipsevalue(Px.x, Px.y, Px.z, model.xMax/4, model.yMax/4, model.zMax/4, 3*model.xMax/4, 3*model.yMax/4, 3*model.zMax/4),ADD);
+      //   return c; 
+      // }
+      // return lx.hsb(0,0,0);
+      //  // else if(spheremode ==2)
+       // { color c = 0;
+       //   return lx.hsb(CalcCone( (xyz by = new xyz(0,spherys[2].ybounce.getValuef(),0) ), Px, mid) );
+
+       // }
+
+  
+       //   } 
+        
+  }
+
+class CubeCurl extends SCPattern{
+float CH, CW, diag;
+ArrayList<PVector> cubeorigin = new ArrayList<PVector>();
+ArrayList<PVector> centerlist = new ArrayList<PVector>();
+private SinLFO curl = new SinLFO(0, Cube.EDGE_HEIGHT, 5000 ); 
+
+private SinLFO bg = new SinLFO(180, 220, 3000);
+
+CubeCurl(GLucose glucose){
+super(glucose);
+addModulator(curl).trigger();
+addModulator(bg).trigger();
+ this.CH = Cube.EDGE_HEIGHT;
+ this.CW = Cube.EDGE_WIDTH;
+ this.diag = sqrt(CW*CW + CW*CW);
+
+
+ArrayList<PVector> centerlistrelative = new ArrayList<PVector>();
+for (int i = 0; i < model.cubes.size(); i++){
+  Cube a = model.cubes.get(i);
+  cubeorigin.add(new PVector(a.x, a.y, a.z));
+  centerlist.add(centerofcube(i));
+  
+} 
+
+}
+//there is definitely a better way of doing this!
+public PVector centerofcube(int i) { 
+Cube c = model.cubes.get(i);
+
+println(" cube #:  " + i + " c.x  "  +  c.x  + "  c.y   "  + c.y   + "  c.z  "  +   c.z  );
+PVector cubeangle = new PVector(c.rx, c.ry, c.rz);
+//println("raw x" + cubeangle.x + "raw y" + cubeangle.y + "raw z" + cubeangle.z);
+PVector cubecenter = new PVector(c.x + CW/2, c.y + CH/2, c.z + CW/2);
+println("cubecenter unrotated:  "  + cubecenter.x + "  "  +cubecenter.y + "  " +cubecenter.z );
+PVector centerrot = new PVector(cos(c.rx)*CW/2 - sin(c.rx)*CW/2, 0, cos(c.rz)*CW/2 + sin(c.rz)*CW/2);
+ // nCos*(y-o.y) - nSin*(z-o.z) + o.y
+cubecenter = PVector.add(cubecenter, centerrot);
+println( "  cubecenter.x  " + cubecenter.x  + " cubecenter.y  " +  cubecenter.y + " cubecenter.z  "   +  cubecenter.z  + "   ");
+
+
+return cubecenter;
+}
+
+
+public void run(double deltaMs){
+for (int i =0; i < model.cubes.size(); i++)  {
+Cube c = model.cubes.get(i);
+float cfloor = c.y;
+
+// if (i%3 == 0){
+
+// for (Point p : c.points ){
+//  // colors[p.index]=color(0,0,0);
+//   //float dif = (p.y - c.y);
+//   //colors[p.index] = color( bg.getValuef() , 80 , dif < curl.getValuef() ? 80 : 0, ADD);
+//    }
+//  }
+
+// else if (i%3 == 1) {
+  
+//  for (Point p: c.points){
+//   colors[p.index]=color(0,0,0);
+//   float dif = (p.y - c.y);
+//   // colors[p.index] = 
+//   // color(bg.getValuef(),
+//   //   map(curl.getValuef(), 0, Cube.EDGE_HEIGHT, 20, 100), 
+//   //   100 - 10*abs(dif - curl.getValuef()), ADD );
+//      }
+//     }
+// else if (i%3 == 2){
+ // centerlist[i].sub(cubeorigin(i);
+   for (Point p: c.points) {
+    PVector pv = new PVector(p.x, p.y, p.z);
+     colors[p.index] =color( constrain(4* pv.dist(centerlist.get(i)), 0, 360)  , 50, 100 );
+   // colors[p.index] =color(constrain(centerlist[i].x, 0, 360), constrain(centerlist[i].y, 0, 100),  );
+
+
+    }
+
+
+  //}
+
+   }
+  }
+ }
+
+ class HueTestHSB extends SCPattern{
+  BasicParameter HueT = new BasicParameter("Hue", .5f);
+  BasicParameter SatT = new BasicParameter("Sat", .5f);
+  BasicParameter BriT = new BasicParameter("Bright", .5f);
+
+HueTestHSB(GLucose glucose) {
+  super(glucose);
+  addParameter(HueT);
+  addParameter(SatT);
+  addParameter(BriT);
+}
+  public void run(double deltaMs){
+
+  for (Point p : model.points) {
+    int c = 0;
+    c = blendColor(c, lx.hsb(360*HueT.getValuef(), 100*SatT.getValuef(), 100*BriT.getValuef()), ADD);
+    colors[p.index]= c;
+  }
+   int now= millis();
+   if (now % 1000 <= 20)
+   {
+   println("Hue: " + 360*HueT.getValuef() + "Sat: " + 100*SatT.getValuef() + "Bright:  " + 100*BriT.getValuef());
+   }
+  }
+
+ }
+
+class TelevisionStatic extends SCPattern {
+  BasicParameter brightParameter = new BasicParameter("BRIGHT", 1.0f);
+  BasicParameter saturationParameter = new BasicParameter("SAT", 1.0f);
+  BasicParameter hueParameter = new BasicParameter("HUE", 1.0f);
+  SinLFO direction = new SinLFO(0, 10, 3000);
+  
+  public TelevisionStatic(GLucose glucose) {
+    super(glucose);
+    addModulator(direction).trigger();
+    addParameter(brightParameter);
+    addParameter(saturationParameter);
+    addParameter(hueParameter);
+  }
+
+ public void run(double deltaMs) {
+    boolean d = direction.getValuef() > 5.0f;
+    for (Point p : model.points) {             
+      colors[p.index] = lx.hsb((lx.getBaseHuef() + random(hueParameter.getValuef() * 360))%360, random(saturationParameter.getValuef() * 100), random(brightParameter.getValuef() * 100));
+    }
+  }
+}
+
+class AbstractPainting extends SCPattern {
+  
+  PImage img;
+  
+  SinLFO colorMod = new SinLFO(0, 360, 5000);
+  SinLFO brightMod = new SinLFO(0, model.zMax, 2000);
+    
+  public AbstractPainting(GLucose glucose) {
+    super(glucose);
+    addModulator(colorMod).trigger();
+    addModulator(brightMod).trigger();
+    
+    img = loadImage("abstract.jpg");
+    img.loadPixels();    
+  } 
+  public void run(double deltaMs) {    
+    for (Point p : model.points) {
+      int c = img.get((int)((p.x / model.xMax) * img.width), img.height - (int)((p.y / model.yMax) * img.height));
+      colors[p.index] = lx.hsb(hue(c) + colorMod.getValuef()%360, saturation(c), brightness(c) - ((p.z - brightMod.getValuef())/p.z));
+    }    
+  }       
+}
+
+class Spirality extends SCPattern {
+  final BasicParameter r = new BasicParameter("RADIUS", 0.5f);
+  
+  float angle = 0;
+  float rad = 0;
+  int direction = 1;
+  
+  Spirality(GLucose glucose) {
+    super(glucose);   
+    addParameter(r);
+    for (Point p : model.points) {  
+      colors[p.index] = lx.hsb(0, 0, 0);
+    }
+  }
+    
+  public void run(double deltaMs) {
+    angle += deltaMs * 0.007f;
+    rad += deltaMs * .025f * direction;
+    float x = model.xMax / 2 + cos(angle) * rad;
+    float y = model.yMax / 2 + sin(angle) * rad;
+    for (Point p : model.points) {    
+      float b = dist(x,y,p.x,p.y);
+      if (b < 90) {
+        colors[p.index] = blendColor(
+          colors[p.index],
+          lx.hsb(lx.getBaseHuef() + 25, 10, map(b, 0, 10, 100, 0)),
+          ADD);        
+        } else {
+      colors[p.index] = blendColor(
+        colors[p.index],
+        lx.hsb(25, 10, map(b, 0, 10, 0, 15)),
+        SUBTRACT); 
+      }
+    }
+    if (rad > model.xMax / 2 || rad <= .001f) {
+      direction *= -1;
+    }
+  }
+}
+
+
+
+
+/**
+ * This is a reusable equalizer class that lets you get averaged
+ * bands with dB scaling and smoothing.
+ */
+public static class GraphicEQ {
+  
+  private final LX lx;
+  
+  public final BasicParameter level = new BasicParameter("LVL", 0.5f);
+  public final BasicParameter range = new BasicParameter("RNGE", 0.5f);
+  public final BasicParameter slope = new BasicParameter("SLOP", 0.5f);
+  public final BasicParameter attack = new BasicParameter("ATK", 0.5f);
+  public final BasicParameter release = new BasicParameter("REL", 0.5f);    
+
+  private final FFT fft;
+  private final int numBands;
+
+  private final LinearEnvelope[] bandVals;
+  
+  public final static int DEFAULT_NUM_BANDS = 16;
+
+  public GraphicEQ(LX lx) {
+    this(lx, DEFAULT_NUM_BANDS);
+  }
+  
+  /**
+   * Note that the number of bands is a suggestion. Due to the FFT implementation
+   * the actual number may be slightly different.
+   */
+  public GraphicEQ(LX lx, int num) {
+    this.lx = lx;
+    fft = new FFT(lx.audioInput().bufferSize(), lx.audioInput().sampleRate());
+    fft.window(FFT.HAMMING);
+    fft.logAverages(50, num/8);
+    numBands = this.fft.avgSize();
+    bandVals = new LinearEnvelope[numBands];
+    for (int i = 0; i < bandVals.length; ++i) {
+      (bandVals[i] = new LinearEnvelope(0, 0, 500)).trigger();
+    }
+  }
+  
+  static final float logTen = log(10);
+  public static float log10(float val) {
+    return log(val) / logTen;
+  }
+  
+  public float getLevel(int band) {
+    return bandVals[band].getValuef();
+  }
+  
+  public float getAverageLevel(int minBand, int numBands) {
+    float avg = 0;
+    for (int i = minBand; i < minBand + numBands; ++i) {
+      avg += bandVals[i].getValuef();
+    }
+    avg /= numBands;
+    return avg;
+  }
+  
+  public void run(double deltaMs) {
+    fft.forward(lx.audioInput().mix);
+    float zeroDBReference = pow(10, 100*(1-level.getValuef())/20.f);
+    float decibelRange = 12 + range.getValuef() * 60;
+    float decibelSlope = slope.getValuef() * 60.f / numBands;
+    for (int i = 0; i < numBands; ++i) {
+      float raw = fft.getAvg(i);
+      float decibels = 20*log10(raw / zeroDBReference);
+      float positiveDecibels = decibels + decibelRange;
+      positiveDecibels += i*decibelSlope;
+      float value = constrain(positiveDecibels / decibelRange, 0, 1);
+      
+      if (value > bandVals[i].getValuef()) {
+        bandVals[i].setRangeFromHereTo(value, attack.getValuef() * 20).trigger();
+      }
+    }
+    for (LinearEnvelope band : bandVals) {
+      band.run(deltaMs);
+      if (!band.isRunning() && band.getValuef() > 0) {
+        band.setRangeFromHereTo(0, release.getValuef() * 1600).trigger();
+      }
+    }    
+  }
+}
+
+
+class TowerParams extends SCPattern
+{
+       BasicParameter hueoff = new BasicParameter("Hueoff", 0.0f);
+       BasicParameter hueSpan = new BasicParameter("HueRange", 0.0f);
+       BasicParameter t1 = new BasicParameter("T1", 0.0f);
+       BasicParameter t2 = new BasicParameter("T2", 0.0f);
+       BasicParameter t3 = new BasicParameter("T3", 0.0f);
+       BasicParameter t4 = new BasicParameter("T4", 0.0f);
+       BasicParameter t5 = new BasicParameter("T5", 0.0f);
+       BasicParameter t6 = new BasicParameter("T6", 0.0f);
+       BasicParameter t7 = new BasicParameter("T7", 0.0f);
+       BasicParameter t8 = new BasicParameter("T8", 0.0f);
+       BasicParameter t9 = new BasicParameter("T9", 0.0f);
+       BasicParameter t10 = new BasicParameter("T10", 0.0f);
+       BasicParameter t11 = new BasicParameter("T11", 0.0f);
+       BasicParameter t12 = new BasicParameter("T12", 0.0f);
+       BasicParameter t13 = new BasicParameter("T13", 0.0f);
+       BasicParameter t14 = new BasicParameter("T14", 0.0f);
+       BasicParameter t15 = new BasicParameter("T15", 0.0f);
+       BasicParameter t16 = new BasicParameter("T16", 0.0f);
+
+       ArrayList<BasicParameter> towerParams;
+       int towerSize;
+       int colorSpan;
+       TowerParams(GLucose glucose) {
+               super(glucose);
+
+               towerParams = new ArrayList<BasicParameter>();
+               addParameter(hueoff);
+               addParameter(hueSpan);
+               towerParams.add(t1);
+               towerParams.add(t2);
+               towerParams.add(t3);
+               towerParams.add(t4);
+               towerParams.add(t5);
+               towerParams.add(t6);
+               towerParams.add(t7);
+               towerParams.add(t8);
+               towerParams.add(t9);
+               towerParams.add(t10);
+               towerParams.add(t11);
+               towerParams.add(t12);
+               towerParams.add(t13);
+               towerParams.add(t14);
+               towerParams.add(t15);
+               towerParams.add(t16);
+               for(BasicParameter p : towerParams)
+               {
+                       addParameter(p);
+               }
+               towerSize = model.towers.size();
+               colorSpan = 255 / towerSize;
+       }
+
+       public void run(double deltaMs)
+       {
+               clearALL();
+               Tower t;
+               for(int i=0; i<towerSize ;i++)
+               {       
+                       t= model.towers.get(i);
+                       for(Point p : t.points)
+                       {
+                               if(p.y<towerParams.get(i).getValuef()*200)
+                               {
+                                       colors[p.index]=lx.hsb(255 * hueoff.getValuef()+colorSpan * hueSpan.getValuef() * i, 255, 255);
+                               }
+                       }
+               }
+
+       }
+
+       public void clearALL()
+       {
+               for(Point p : model.points)
+               {
+                       colors[p.index] = 0;
+               }
+       }
+
+}
+class Sandbox extends SCPattern
+{
+       int c=0;
+       int prevC=0;
+       int huerange=255;
+       int pointrange= model.points.size();
+       int striprange= model.strips.size();
+       int facerange= model.faces.size();
+       int cuberange = model.cubes.size();
+       int towerrange = model.towers.size();
+       int counter=0;
+
+       Sandbox(GLucose glucose) {
+               super(glucose);
+               println("points "+pointrange);
+               println("strips "+striprange);
+               println("faces "+facerange);
+               println("cubes "+cuberange);
+               println("towers "+towerrange);
+       }
+       
+       public void run(double deltaMs) {
+               
+
+               if(counter % 10 ==0)
+               {
+                       doDraw(c,0);
+                       c = (c + 1) % towerrange;
+                       long col = lx.hsb(Math.round(Math.random()*255),255,255) ;
+                       doDraw(c,col);
+               }
+               counter++;
+
+       }
+
+       public void doDraw(int c,long col)
+       {
+                       Tower t= model.towers.get((int) c);
+                       for(Point p : t.points)
+                       {
+                               colors[p.index] = (int) col;
+                       }
+       }
+};
+
+class GranimTestPattern extends GranimPattern
+{
+       GranimTestPattern(GLucose glucose)
+       {
+               super(glucose);
+               addGraphic("myReds",new RedsGraphic(100));
+               int[] dots = {0,128,0,128,0,128,0,128,0,128,0,128};
+               addGraphic("myOtherColors",new ColorDotsGraphic(dots));
+
+               getGraphicByName("myOtherColors").position=100;
+       }
+       int counter=0;
+       public void run(double deltaMs) 
+       {
+               clearALL();
+               super.run(deltaMs);
+               
+               if(counter % 3 ==0)
+               {
+                       Graphic reds = getGraphicByName("myReds");
+                       Graphic others = getGraphicByName("myOtherColors");
+                       reds.position = reds.position + 1 % 19000;
+                       others.position = others.position + 10 % 19000;
+               }
+       }
+       public void clearALL()
+       {
+               for(int i = 0; i < colors.length; i++)
+               {
+                       colors[i] = 0;
+               }
+       }
+
+
+}
+
+class GranimTestPattern2 extends GranimPattern
+{
+       GranimTestPattern2(GLucose glucose)
+       {
+               super(glucose);
+               /*for(int i = 0;i < 100; i++)
+               {
+                       Graphic g = addGraphic("myReds_"+i,new RedsGraphic(Math.round(Math.random() * 100)));
+
+               }*/
+               Graphic g = addGraphic("myRandoms",new RandomsGranim(50));
+               g.position = 200;
+               
+       }
+       int counter=0;
+       float count=0;
+       public void run(double deltaMs) 
+       {
+               clearALL();
+               super.run(deltaMs);
+               Graphic randomsGraphic = getGraphicByName("myRandoms");
+               randomsGraphic.position = Math.round(sin(count)*1000)+5000;
+               count+= 0.005f;
+       }
+       public void clearALL()
+       {
+               for(Point p : model.points)
+               {
+                       colors[p.index] = 0;
+               }
+       }
+
+
+};
+
+class DriveableCrossSections extends CrossSections
+{
+       BasicParameter xd; 
+       BasicParameter yd;
+       BasicParameter zd;
+       BasicParameter mode; 
+
+       DriveableCrossSections(GLucose glucose) {
+               super(glucose); 
+       }
+
+       public void addParams()
+       {
+               mode = new BasicParameter("Mode", 0.0f);
+               xd = new BasicParameter("XD", 0.0f);
+               yd = new BasicParameter("YD", 0.0f);
+               zd = new BasicParameter("ZD", 0.0f);
+               addParameter(mode);
+               addParameter(xd);
+           addParameter(yd);
+           addParameter(zd);
+
+          super.addParams();
+       }
+
+       public void onParameterChanged(LXParameter p) {
+                       if(p == mode)
+                       {
+                               if(interactive())
+                               {
+                                       copyValuesToKnobs();
+                               }else{
+                                       copyKnobsToValues();
+                               }
+                       }
+       }
+
+       public void copyValuesToKnobs()
+       {
+               xd.setValue(x.getValue()/200);
+               yd.setValue(y.getValue()/115);
+               zd.setValue(z.getValue()/100);
+       }
+
+       public void copyKnobsToValues()
+       {
+               x.setValue(xd.getValue()*200);
+               y.setValue(yd.getValue()*115);
+               z.setValue(zd.getValue()*100);
+       }
+
+       public boolean interactive()
+       {
+               return Math.round(mode.getValuef())>0.5f;
+       }
+
+       public void updateXYZVals()
+       {
+               if(interactive())
+               {
+                       xv = xd.getValuef()*200;
+                   yv = yd.getValuef()*115;
+                   zv = zd.getValuef()*100;
+               }else{
+                       super.updateXYZVals();
+                       copyValuesToKnobs();
+               }
+       }
+
+}
+//----------------------------------------------------------------------------------------------------------------------------------
+public class Pong extends DPat {
+       SinLFO x,y,z,dx,dy,dz;
+       float cRad;     BasicParameter pSize;
+       Pick    pChoose;
+       PVector v = new PVector(), vMir =  new PVector();
+
+       Pong(GLucose glucose) {
+               super(glucose);
+               cRad = mMax.x/10;
+               addModulator(dx = new SinLFO(6000,  500, 30000  )).trigger();
+               addModulator(dy = new SinLFO(3000,  500, 22472  )).trigger();
+               addModulator(dz = new SinLFO(1000,  500, 18420  )).trigger();
+               addModulator(x  = new SinLFO(cRad, mMax.x - cRad, 0)).trigger();        x.modulateDurationBy(dx);
+               addModulator(y  = new SinLFO(cRad, mMax.y - cRad, 0)).trigger();        y.modulateDurationBy(dy);
+               addModulator(z  = new SinLFO(cRad, mMax.z - cRad, 0)).trigger();        z.modulateDurationBy(dz);
+           pSize       = addParam      ("Size"                 , 0.4f  );
+           pChoose = addPick   ("Animiation"   , 2, 2, new String[] {"Pong", "Ball", "Cone"}   );
+       }
+
+       public void     StartRun(double deltaMs)        { cRad = mMax.x*val(pSize)/6; }
+       public int      CalcPoint(PVector p)            {
+               v.set(x.getValuef(), y.getValuef(), z.getValuef());
+               v.z=0;p.z=0;// ignore z dimension
+               switch(pChoose.Cur()) {
+               case 0: vMir.set(mMax); vMir.sub(p);
+                               return lx.hsb(lxh(),100,c1c(1 - min(v.dist(p), v.dist(vMir))*.5f/cRad));                // balls
+               case 1: return lx.hsb(lxh(),100,c1c(1 - v.dist(p)*.5f/cRad));                                                   // ball
+               case 2: vMir.set(mMax.x/2,0,mMax.z/2);
+                               return lx.hsb(lxh(),100,c1c(1 - calcCone(p,v,vMir) * max(.02f,.45f-val(pSize))));       // spot
+               }
+               return lx.hsb(0,0,0);
+       }
+}
+//----------------------------------------------------------------------------------------------------------------------------------
+public class NDat {
+       float   xz, yz, zz, hue, speed, angle, den;
+       float   xoff,yoff,zoff;
+       float   sinAngle, cosAngle;
+       boolean isActive;
+       NDat              () { isActive=false; }
+       public boolean  Active() { return isActive; }
+       public void     set     (float _hue, float _xz, float _yz, float _zz, float _den, float _speed, float _angle) {
+               isActive = true;
+               hue=_hue; xz=_xz; yz=_yz; zz =_zz; den=_den; speed=_speed; angle=_angle;
+               xoff = random(100e3f); yoff = random(100e3f); zoff = random(100e3f);
+       }
+}
+
+public class Noise extends DPat
+{
+       int                             CurAnim, iSymm;
+       int                     XSym=1,YSym=2,RadSym=3;
+       float                   zTime , zTheta=0, zSin, zCos, rtime, ttime;
+       BasicParameter  pSpeed , pDensity, pSharp;
+       Pick                    pChoose, pSymm;
+       int                             _ND = 4;
+       NDat                    N[] = new NDat[_ND];
+
+       Noise(GLucose glucose) {
+               super(glucose);
+               pSpeed          = addParam("Fast"       , .55f);
+               pDensity        = addParam("Dens"        , .5f);
+               pSharp          = addParam("Shrp"        ,  0);
+               pSymm           = addPick("Symmetry" , 0, 3, new String[] {"None", "X", "Y", "Radial"}  );
+               pChoose         = addPick("Animation", 6, 7, new String[] {"Drip", "Cloud", "Rain", "Fire", "Machine", "Spark","VWave", "Wave"} );
+               for (int i=0; i<_ND; i++) N[i] = new NDat();
+       }
+
+       public void onActive() { zTime = random(500); zTheta=0; rtime = 0; ttime = 0; }
+
+       public void StartRun(double deltaMs) {
+               zTime   += deltaMs*(val(pSpeed)-.5f)*.002f      ;
+               zTheta  += deltaMs*(spin()-.5f)*.01f    ;
+               rtime   += deltaMs;
+               iSymm    = pSymm.Cur();
+               zSin    = sin(zTheta);
+               zCos    = cos(zTheta);
+
+               if (pChoose.Cur() != CurAnim) {
+                       CurAnim = pChoose.Cur(); ttime = rtime;
+                       pSpin           .reset();       zTheta          = 0;
+                       pDensity        .reset();       pSpeed          .reset();
+                       for (int i=0; i<_ND; i++) { N[i].isActive = false; }
+                       
+                       switch(CurAnim) {
+                       //                          hue xz yz zz den mph angle
+                       case 0: N[0].set(0  ,75 ,75 ,150,45 ,3  ,0  ); pSharp.setValue(1 ); break;      // drip
+                       case 1: N[0].set(0  ,100,100,200,45 ,3  ,180); pSharp.setValue(0 ); break;      // clouds
+                       case 2: N[0].set(0  ,2  ,400,2  ,20 ,3  ,0  ); pSharp.setValue(.5f); break;     // rain
+                       case 3: N[0].set(40 ,100,100,200,10 ,1  ,180); 
+                                       N[1].set(0  ,100,100,200,10 ,5  ,180); pSharp.setValue(0 ); break;      // fire 1
+                       case 4: N[0].set(0  ,40 ,40 ,40 ,15 ,2.5f,180);
+                                       N[1].set(20 ,40 ,40 ,40 ,15 ,4  ,0  );
+                                       N[2].set(40 ,40 ,40 ,40 ,15 ,2  ,90 );
+                                       N[3].set(60 ,40 ,40 ,40 ,15 ,3  ,-90); pSharp.setValue(.5f); break; // machine
+                       case 5: N[0].set(0  ,400,100,2  ,15 ,3  ,90 );
+                                       N[1].set(20 ,400,100,2  ,15 ,2.5f,0  );
+                                       N[2].set(40 ,100,100,2  ,15 ,2  ,180);
+                                       N[3].set(60 ,100,100,2  ,15 ,1.5f,270); pSharp.setValue(.5f); break; // spark
+                       }
+               }
+               
+               for (int i=0; i<_ND; i++) if (N[i].Active()) {
+                       N[i].sinAngle = sin(radians(N[i].angle));
+                       N[i].cosAngle = cos(radians(N[i].angle));
+               }
+       }
+
+       public int CalcPoint(PVector p) {
+               int c = 0;
+               rotateZ(p, mCtr, zSin, zCos);
+
+               if (CurAnim == 6 || CurAnim == 7) {
+                       setNorm(p);
+                       return lx.hsb(lxh(),100, 100 * (
+                                                       constrain(1-50*(1-val(pDensity))*abs(p.y-sin(zTime*10  + p.x*(300))*.5f - .5f),0,1) + 
+                       (CurAnim == 7 ? constrain(1-50*(1-val(pDensity))*abs(p.x-sin(zTime*10  + p.y*(300))*.5f - .5f),0,1) : 0))
+                       );
+               }                       
+
+               if (iSymm == XSym && p.x > mMax.x/2) p.x = mMax.x-p.x;
+               if (iSymm == YSym && p.y > mMax.y/2) p.y = mMax.y-p.y;
+
+               for (int i=0;i<_ND; i++) if (N[i].Active()) {
+                       NDat  n     = N[i];
+                       float zx    = zTime * n.speed * n.sinAngle,
+                                 zy    = zTime * n.speed * n.cosAngle;
+
+                       float b     = (iSymm==RadSym ? noise(zTime*n.speed+n.xoff-p.dist(mCtr)/n.xz)
+                                                                                : noise(p.x/n.xz+zx+n.xoff,p.y/n.yz+zy+n.yoff,p.z/n.zz+n.zoff))
+                                                       *1.8f;
+
+                       b +=    n.den/100 -.4f + val(pDensity) -1;
+                       c =     blendColor(c,lx.hsb(lxh()+n.hue,100,c1c(b)),ADD);
+               }
+               return c;
+       }
+}
+//----------------------------------------------------------------------------------------------------------------------------------
+public class Play extends DPat
+{
+       public class rAngle {
+               float   prvA, dstA, c;
+               float   prvR, dstR, r;          
+               float   _cos, _sin, x, y;
+               public float    fixAngle        (float a, float b) { return a<b ?
+                                                                               (abs(a-b) > abs(a+2*PI-b) ? a : a+2*PI) :
+                                                                               (abs(a-b) > abs(a-2*PI-b) ? a : a-2*PI) ; }
+               public float    getX(float r)   {       return mCtr.x + _cos*r; }
+               public float    getY(float r)   {       return mCtr.y + _sin*r; }
+               public void     move()                  {       c               = interp(t,prvA,dstA); 
+                                                                       r               = interp(t,prvR,dstR);
+                                                                       _cos    = cos(c);       _sin    = sin(c);
+                                      x                = getX(r);      y               = getY(r);              }               
+               public void     set()                   {       prvA    = dstA;         dstA    = random(2*PI);         prvA = fixAngle(prvA, dstA);
+                                                                       prvR    = dstR;         dstR    = random(mCtr.y);                                                                       }
+       }
+
+       BasicParameter  pAmp, pRadius, pBounce;
+       Pick                    pTimePattern, pTempoMult, pShape;
+
+       ArrayList<rWave> waves = new ArrayList<rWave>(10);
+
+       int             nBeats  =       0;
+       float   t,amp,rad,bnc,zTheta=0;
+
+       rAngle  a1              = new rAngle(), a2                      = new rAngle(),
+                       a3              = new rAngle(), a4                      = new rAngle();
+       PVector cPrev   = new PVector(), cRand          = new PVector(),
+                       cMid    = new PVector(), V                      = new PVector(),
+                       theta   = new PVector(), tSin           = new PVector(),
+                       tCos    = new PVector(), cMidNorm       = new PVector(),
+                       Pn              = new PVector();
+       float   LastBeat=3, LastMeasure=3;
+       int             curRandTempo = 1, curRandTPat = 1;
+
+       Play(GLucose glucose) {
+               super(glucose);
+           pRadius             = addParam("Rad"        , .1f   );
+               pBounce         = addParam("Bnc"        , .2f   );
+           pAmp                = addParam("Amp"        , .2f   );
+               pTempoMult      = addPick ("TMult"      , 5 , 5         , new String[] {"1x", "2x", "4x", "8x", "16x", "Rand"   }       );
+               pTimePattern= addPick ("TPat"   , 7 , 7         , new String[] {"Bounce", "Sin", "Roll", "Quant", "Accel", "Deccel", "Slide", "Rand"}   );
+               pShape          = addPick ("Shape"      , 7 , 15        , new String[] {"Line", "Tap", "V", "RandV",
+                                                                                                                                       "Pyramid", "Wings", "W2", "Clock",
+                                                                                                                                       "Triangle", "Quad", "Sphere", "Cone",
+                                                                                                                                       "Noise", "Wave", "?", "?"}                                              );
+       }
+
+       public class rWave {
+               float v0, a0, x0, t,damp,a;
+               boolean bDone=false;
+               final float len=8;
+               rWave(float _x0, float _a0, float _v0, float _damp) { x0=_x0*len; a0=_a0; v0=_v0; t=0; damp = _damp; }
+               public void move(double deltaMs) {
+                       t += deltaMs*.001f;
+                       if (t>4) bDone=true;
+               }
+               public float val(float _x) {
+                       _x*=len;
+                       float dist = t*v0 - abs(_x-x0);
+                       if (dist<0) { a=1; return 0; }
+                       a  = a0*exp(-dist*damp) * exp(-abs(_x-x0)/(.2f*len)); // * max(0,1-t/dur)
+                       return  -a*sin(dist);
+               }
+       }
+
+       public void onReset()  { zTheta=0; super.onReset(); }
+       public void onActive() { 
+               zTheta=0; 
+               while (lx.tempo.bpm() > 40) lx.tempo.setBpm(lx.tempo.bpm()/2);
+       }
+
+       int KeyPressed = -1;
+       public boolean noteOn(Note note) {
+               int row = note.getPitch(), col = note.getChannel();
+               if (row == 57) {KeyPressed = col; return true; }
+               return super.noteOn(note);
+       }
+
+       public void StartRun(double deltaMs) {
+               t       = lx.tempo.rampf();
+               amp = pAmp              .getValuef();
+               rad     = pRadius       .getValuef();
+               bnc     = pBounce       .getValuef();           
+               zTheta  += deltaMs*(val(pSpin)-.5f)*.01f;
+
+               theta   .set(val(pRotX)*PI*2, val(pRotY)*PI*2, val(pRotZ)*PI*2 + zTheta);
+               tSin    .set(sin(theta.x), sin(theta.y), sin(theta.z));
+               tCos    .set(cos(theta.x), cos(theta.y), cos(theta.z));
+
+               if (t<LastMeasure) {
+                       if (random(3) < 1) { curRandTempo = PApplet.parseInt(random(4)); if (curRandTempo == 3) curRandTempo = PApplet.parseInt(random(4));     }
+                       if (random(3) < 1) { curRandTPat  = pShape.Cur() > 6 ? 2+PApplet.parseInt(random(5)) : PApplet.parseInt(random(7));                                     }
+               } LastMeasure = t;
+                       
+               int nTempo = pTempoMult  .Cur(); if (nTempo == 5) nTempo = curRandTempo;
+               int nTPat  = pTimePattern.Cur(); if (nTPat  == 7) nTPat  = curRandTPat ;
+
+               switch (nTempo) {
+                       case 0:         t = t;                                                          break;
+                       case 1:         t = (t*2.f )%1.f;                                               break;
+                       case 2:         t = (t*4.f )%1.f;                                               break;
+                       case 3:         t = (t*8.f )%1.f;                                               break;
+                       case 4:         t = (t*16.f)%1.f;                                               break;
+               }
+
+               int i=0; while (i< waves.size()) {
+                       rWave w = waves.get(i);
+                       w.move(deltaMs); if (w.bDone) waves.remove(i); else i++;
+               }
+
+               if ((t<LastBeat && pShape.Cur()!=14) || KeyPressed>-1) {
+                       waves.add(new rWave(
+                                               KeyPressed>-1 ? map(KeyPressed,0,7,0,1) : random(1),            // location
+                                               bnc*10,                 // bounciness
+                                               7,                              // velocity
+                                               2*(1-amp)));    // dampiness
+                       KeyPressed=-1;
+                       if (waves.size() > 5) waves.remove(0);
+               }
+               
+               if (t<LastBeat) {
+                       cPrev.set(cRand); setRand(cRand);
+                       a1.set(); a2.set(); a3.set(); a4.set();
+               } LastBeat = t;
+
+               switch (nTPat) {
+                       case 0:         t = sin(PI*t);                                                  break;  // bounce
+                       case 1:         t = norm(sin(2*PI*(t+PI/2)),-1,1);              break;  // sin
+                       case 2:         t = t;                                                                  break;  // roll
+                       case 3:         t = constrain(PApplet.parseInt(t*8)/7.f,0,1);                   break;  // quant
+                       case 4:         t = t*t*t;                                                              break;  // accel
+                       case 5:         t = sin(PI*t*.5f);                                              break;  // deccel
+                       case 6:         t = .5f*(1-cos(PI*t));                                  break;  // slide
+               }
+               
+               cMid.set                (cPrev);        interpolate(t,cMid,cRand);
+               cMidNorm.set    (cMid);         setNorm(cMidNorm);
+               a1.move(); a2.move(); a3.move(); a4.move();
+       }
+
+       public int CalcPoint(PVector Px) {
+               if (theta.x != 0) rotateX(Px, mCtr, tSin.x, tCos.x);
+               if (theta.y != 0) rotateY(Px, mCtr, tSin.y, tCos.y);
+               if (theta.z != 0) rotateZ(Px, mCtr, tSin.z, tCos.z);
+               
+               Pn.set(Px); setNorm(Pn);
+
+               float mp        = min(Pn.x, Pn.z);
+               float yt        = map(t,0,1,.5f-bnc/2,.5f+bnc/2);
+               float r,d;
+
+               switch (pShape.Cur()) {
+               case 0:         V.set(Pn.x, yt                                                          , Pn.z);                                                        break;  // bouncing line
+               case 1:         V.set(Pn.x, map(cos(PI*t * Pn.x),-1,1,0,1)  , Pn.z);                                                    break;  // top tap
+               case 2:         V.set(Pn.x, bnc*map(Pn.x<.5f?Pn.x:1-Pn.x,0,.5f ,0,t-.5f)+.5f, Pn.z);                            break;  // V shape
+               case 3:         V.set(Pn.x, Pn.x < cMidNorm.x ? map(Pn.x,0,cMidNorm.x, .5f,yt) :
+                                                                                               map(Pn.x,cMidNorm.x,1, yt,.5f), Pn.z);                          break;  //  Random V shape
+
+               case 4:         V.set(Pn.x,     .5f*(Pn.x < cMidNorm.x ?        map(Pn.x,0,cMidNorm.x, .5f,yt) :
+                                                                                                               map(Pn.x,cMidNorm.x,1, yt,.5f)) +
+                                                       .5f*(Pn.z < cMidNorm.z ?        map(Pn.z,0,cMidNorm.z, .5f,yt) :
+                                                                                                               map(Pn.z,cMidNorm.z,1, yt,.5f)), Pn.z);                 break;  //  Random Pyramid shape
+                                                                                                       
+               case 5:         V.set(Pn.x, bnc*map((Pn.x-.5f)*(Pn.x-.5f),0,.25f,0,t-.5f)+.5f, Pn.z);                           break;  // wings
+               case 6:         V.set(Pn.x, bnc*map((mp  -.5f)*(mp  -.5f),0,.25f,0,t-.5f)+.5f, Pn.z);                           break;  // wings
+
+               case 7:         d = min(
+                                               distToSeg(Px.x, Px.y, a1.getX(70),a1.getY(70), mCtr.x, mCtr.y),
+                                               distToSeg(Px.x, Px.y, a2.getX(40),a2.getY(40), mCtr.x, mCtr.y));
+                                       d = constrain(30*(rad*40-d),0,100);
+                                       return lx.hsb(lxh(),100, d); // clock
+
+               case 8:         r = amp*200 * map(bnc,0,1,1,sin(PI*t));
+                                       d = min(
+                                               distToSeg(Px.x, Px.y, a1.getX(r),a1.getY(r), a2.getX(r),a2.getY(r)),
+                                               distToSeg(Px.x, Px.y, a2.getX(r),a2.getY(r), a3.getX(r),a3.getY(r)),
+                                               distToSeg(Px.x, Px.y, a3.getX(r),a3.getY(r), a1.getX(r),a1.getY(r))                             // triangle
+                                               );
+                                       d = constrain(30*(rad*40-d),0,100);
+                                       return lx.hsb(lxh(),100, d); // clock
+
+               case 9:         r = amp*200 * map(bnc,0,1,1,sin(PI*t));
+                                       d = min(
+                                               distToSeg(Px.x, Px.y, a1.getX(r),a1.getY(r), a2.getX(r),a2.getY(r)),
+                                               distToSeg(Px.x, Px.y, a2.getX(r),a2.getY(r), a3.getX(r),a3.getY(r)),
+                                               distToSeg(Px.x, Px.y, a3.getX(r),a3.getY(r), a4.getX(r),a4.getY(r)),
+                                               distToSeg(Px.x, Px.y, a4.getX(r),a4.getY(r), a1.getX(r),a1.getY(r))                             // quad
+                                       );
+                                       d = constrain(30*(rad*40-d),0,100);
+                                       return lx.hsb(lxh(),100, d); // clock
+
+               case 10:
+                                       r = map(bnc,0,1,a1.r,amp*200*sin(PI*t));
+                                       return lx.hsb(lxh(),100,c1c(.9f+2*rad - dist(Px.x,Px.y,a1.getX(r),a1.getY(r))*.03f) );          // sphere
+
+               case 11:
+                                       Px.z=mCtr.z; cMid.z=mCtr.z;
+                                       return lx.hsb(lxh(),100,c1c(1 - calcCone(Px,cMid,mCtr) * 0.02f > .5f?1:0));                             // cone
+
+               case 12:        return lx.hsb(lxh() + noise(Pn.x,Pn.y,Pn.z + (NoiseMove+50000)/1000.f)*200,
+                                               85,c1c(Pn.y < noise(Pn.x + NoiseMove/2000.f,Pn.z)*(1+amp)-amp/2.f-.1f ? 1 : 0));        // noise
+
+               case 13:        
+               case 14:        float y=0; for (rWave w : waves) y += .5f*w.val(Pn.x);  // wave
+                                       V.set(Pn.x, .7f+y, Pn.z);
+                                       break;
+
+               default:        return lx.hsb(0,0,0);
+               }
+
+               return lx.hsb(lxh(), 100, c1c(1 - V.dist(Pn)/rad));
+       }
+}
+//----------------------------------------------------------------------------------------------------------------------------------
+boolean dDebug = false;
+class dCursor {
+       dVertex vCur, vNext, vDest;
+       float   destSpeed;
+       int     posStop, pos,posNext;   // 0 - 65535
+       int     clr;
+
+       dCursor() {}
+
+       public boolean isDone   ()                                                                      { return pos==posStop;                                                                           }
+       public boolean atDest  ()                                                                       { return vCur.s==vDest.s || 
+                                                                                                                                xyDist(vCur.getPoint(0), vDest.getPoint(0)) < 12 || 
+                                                                                                                                xyDist(vCur.getPoint(0), vDest.getPoint(15))< 12;}
+       public void     setCur  (dVertex _v, int _p)                            { p2=null; vCur=_v; pos=_p; pickNext();                                          }
+       public void     setCur  (dPixel  _p)                                            { setCur(_p.v, _p.pos);                                                                          }
+       public void     setNext (dVertex _v, int _p, int _s)            { vNext = _v; posNext = _p<<12; posStop = _s<<12;                        }
+       public void     setDest (dVertex _v, float _speed)                      { vDest = _v; destSpeed = _speed;                                                        }
+       public void     onDone  ()                                                                      { setCur(vNext, posNext); pickNext();                                            }
+
+       float   minDist;
+       int     nTurns;
+       boolean bRandEval;
+
+       public void     evaluate(dVertex v, int p, int s) {
+               if (v == null) return; ++nTurns;
+               if (bRandEval) {
+                       if (random(nTurns) < 1) setNext(v,p,s); return; }
+               else {
+                       float d = xyDist(v.getPoint(15), vDest.getPoint(0));
+                       if (d <  minDist)                                       { minDist=d; setNext(v,p,s); }
+                       if (d == minDist && random(2)<1)        { minDist=d; setNext(v,p,s); }
+               }
+       }
+
+       public void     evalTurn(dTurn t) { 
+               if (t == null || t.pos0<<12 <= pos) return; 
+               evaluate(t.v    ,    t.pos1, t.pos0);
+               evaluate(t.v.opp, 16-t.pos1, t.pos0);
+       }
+
+       public void     pickNext()      {
+               bRandEval = random(.05f+destSpeed) < .05f; minDist=500; nTurns=0;
+               evaluate(vCur.c0, 0, 16);       evaluate(vCur.c1, 0, 16);
+               evaluate(vCur.c2, 0, 16);       evaluate(vCur.c3, 0, 16);
+               evalTurn(vCur.t0);                      evalTurn(vCur.t1);
+               evalTurn(vCur.t2);                      evalTurn(vCur.t3);
+       }
+
+       Point   p1, p2; int i2;
+
+       public int draw(int nAmount, SCPattern pat) {
+               int nFrom       = (pos    ) >> 12;
+               int     nMv     = min(nAmount, posStop-pos);
+               int     nTo     = min(15,(pos+nMv) >> 12);
+               dVertex v       = vCur;
+
+               if (dDebug) {   p1 = v.getPoint(nFrom); float d = (p2 == null ? 0 : pointDist(p1,p2)); if (d>5) { println("too wide! quitting: " + d); exit(); }}
+                                                               for (int i = nFrom; i <= nTo; i++) { pat.getColors()[v.ci          + v.dir*i     ] = clr; }
+               if (v.same != null)             for (int i = nFrom; i <= nTo; i++) { pat.getColors()[v.same.ci + v.same.dir*i] = clr; }
+
+               if (dDebug) {   p2 = v.getPoint(nTo); i2 = nTo; }
+
+               pos += nMv; return nAmount - nMv;
+                       }       
+}
+
+//----------------------------------------------------------------------------------------------------------------------------------
+class Worms extends SCPattern {
+       float   StripsPerSec    = 10;
+       float   TrailTime               = 3000;
+       int     numCursors              = 50;
+       ArrayList<dCursor> cur  = new ArrayList<dCursor>(30);
+
+       private GraphicEQ eq = null;
+
+       private BasicParameter pBeat      = new BasicParameter("BEAT",  0);
+       private BasicParameter pSpeed     = new BasicParameter("FAST", .2f);
+       private BasicParameter pBlur      = new BasicParameter("BLUR", .3f);
+       private BasicParameter pWorms     = new BasicParameter("WRMS", .3f);
+       private BasicParameter pConfusion = new BasicParameter("CONF", .1f);
+       private BasicParameter pEQ        = new BasicParameter("EQ"  ,  0);
+       private BasicParameter pSpawn     = new BasicParameter("DIR" ,  0);
+       private BasicParameter pColor     = new BasicParameter("CLR" ,  .1f);
+
+       float   zMidLat = 82.f;
+       float   nConfusion;
+       private final Click moveChase = new Click(1000);
+
+       PVector middle;
+       public int      AnimNum() { return floor(pSpawn.getValuef()*(4-.01f));  }
+       public float   randX() { return random(model.xMax-model.xMin)+model.xMin; }
+       public float   randY() { return random(model.yMax-model.yMin)+model.yMin; }
+       public PVector  randEdge() { 
+               return random(2) < 1 ?  new PVector(random(2)<1 ? model.xMin:model.xMax, randY(), zMidLat)      :
+                                                               new PVector(randX(), random(2)<1 ? model.yMin:model.yMax, zMidLat)      ;
+       }
+
+       Worms(GLucose glucose) {
+               super(glucose); 
+           addModulator(moveChase).start();
+           addParameter(pBeat);    addParameter(pSpeed);
+           addParameter(pBlur);    addParameter(pWorms);
+           addParameter(pEQ);      addParameter(pConfusion);
+               addParameter(pSpawn);   addParameter(pColor);
+
+           middle = new PVector(1.5f*model.cx, 1.5f*model.cy, 71);
+               if (lattice == null) lattice = new dLattice();
+               for (int i=0; i<numCursors; i++) { dCursor c = new dCursor(); reset(c); cur.add(c); }
+               onParameterChanged(pEQ); setNewDest();
+       }
+
+       public void onParameterChanged(LXParameter parameter) {
+               super.onParameterChanged(parameter);
+               nConfusion = 1-pConfusion.getValuef();
+               for (int i=0; i<numCursors; i++) {
+                       if (parameter==pSpawn) reset(cur.get(i));
+                       cur.get(i).destSpeed = nConfusion;
+               }
+       }
+
+       public float getClr() { return lx.getBaseHuef() + random(pColor.getValuef()*300); }
+       public void reset(dCursor c) {
+               switch(AnimNum()) {
+                       case 0: c.clr = lx.hsb(getClr(),100,100);                       // middle to edges
+                                       c.setDest(lattice.getClosest(randEdge()).v, nConfusion);
+                                       c.setCur (lattice.getClosest(middle));
+                                       break;
+
+                       case 1: c.clr = lx.hsb(getClr(),100,100);                               // top to bottom
+                                       float xLin = randX();
+                                       c.setDest(lattice.getClosest(new PVector(xLin, 0         , zMidLat)).v, nConfusion);
+                                       c.setCur (lattice.getClosest(new PVector(xLin, model.yMax, zMidLat)));
+                                       break;
+
+                       case 2: c.clr = lx.hsb(getClr(),100,100); break;                // chase a point around
+
+                       case 3: boolean bLeft = random(2)<1;
+                                       c.clr = lx.hsb(getClr()+random(120),100,100);                           // sideways
+                                       float yLin = randX();
+                                       c.setDest(lattice.getClosest(new PVector(bLeft ? 0 : model.xMax,yLin,zMidLat)).v, nConfusion);
+                                       c.setCur (lattice.getClosest(new PVector(bLeft ? model.xMax : 0,yLin,zMidLat)));
+                                       break;
+               }
+               if (pBlur.getValuef() == 1 && random(2)<1) c.clr = lx.hsb(0,0,0);
+       }
+
+       public void setNewDest() {
+               if (AnimNum() != 2) return;
+               PVector dest = new PVector(randX(), randY(), zMidLat);
+               for (int i=0; i<numCursors; i++) {
+                       cur.get(i).setDest(lattice.getClosest(dest).v, nConfusion);
+                       cur.get(i).clr = lx.hsb(getClr()+75,100,100);   // chase a point around
+               }
+       }
+
+       public void run(double deltaMs) { 
+               if (deltaMs > 100) return;
+           if (moveChase.click()) setNewDest();
+
+           float fBass=0, fTreble=0;
+           if (pEQ.getValuef()>0) {            // EQ
+                   eq.run(deltaMs);
+                   fBass       = eq.getAverageLevel(0, 4);
+                   fTreble = eq.getAverageLevel(eq.numBands-7, 7);
+               }
+
+               if (pBlur.getValuef() < 1) {    // trails
+                       for (int i=0,s=model.points.size(); i<s; i++) {
+                               int c = colors[i]; float b = lx.b(c); 
+                               if (b>0) colors[i] = lx.hsb(lx.h(c), lx.s(c), constrain((float)(b-100*deltaMs/(pBlur.getValuef()*TrailTime)),0,100));
+                       }
+               }
+
+               int nWorms = floor(pWorms.getValuef() * numCursors * 
+                                        map(pEQ.getValuef(),0,1,1,constrain(2*fTreble,0,1)));
+
+               for (int i=0; i<nWorms; i++) {
+                       dCursor c = cur.get(i);
+                       int nLeft = floor((float)deltaMs*.001f*StripsPerSec * 65536 * (5*pSpeed.getValuef()));
+                       nLeft *= (1 - lx.tempo.rampf()*pBeat.getValuef());
+                       while(nLeft > 0) { 
+                               nLeft = c.draw(nLeft,this); if (!c.isDone()) continue;
+                               c.onDone(); if (c.atDest()) reset(c);
+                       }
+               }
+       }
+
+
+       public void onActive() { if (eq == null) {
+               eq = new GraphicEQ(lx, 16);             eq.slope.setValue(0.6f);
+               eq.level.setValue(0.65f);               eq.range.setValue(0.35f);
+               eq.release.setValue(0.4f);
+       }}
+}
+//----------------------------------------------------------------------------------------------------------------------------------
+class GenericController {
+    GenericController(){}
+    public void RotateKnob(int type, int num, float val){
+      LXParameter p = null;
+      if(type==0) {
+        p = glucose.patternKnobs.get(num);
+        if(p!=null) { p.setValue(val); }
+      }
+      if(type==1) {
+        p = glucose.transitionKnobs.get(num);
+        if(p!=null) { p.setValue(val); }
+      }
+      if(type==2) {
+        p = glucose.effectKnobs.get(num);
+        if(p!=null) { p.setValue(val); }
+      }
+    }
+}
+
+class MidiController extends GenericController {
+  MidiController() {
+     super();
+  }  
+}
+//PApplet xparent;  // be sure to set
+
+
+
+OscP5 listener;
+// Setup OSC
+//listener = new OscP5(this,7022);
+
+//boolean[] noteState = new boolean[16];
+//
+//void controllerChangeReceived(rwmidi.Controller cc) {
+//  if (debugMode) {
+//    println("CC: " + cc.toString());
+//  }
+//  if(cc.getCC()==1){
+//    for(int i=0; i<16; i++){
+//      if(noteState[i] && i<8)  { LXParameter p = glucose.patternKnobs.get(i); p.setValue(cc.getValue()/127.0); }
+//      else if(noteState[i] && i<12) { LXParameter p = glucose.transitionKnobs.get(i-8); p.setValue(cc.getValue()/127.0); }
+//      else if(noteState[i] && i<16) { LXParameter p = glucose.effectKnobs.get(i-12); p.setValue(cc.getValue()/127.0); }
+//    }
+//  }
+//}
+//
+//void noteOnReceived(Note note) {
+//  if (debugMode) {
+//    println("Note On: " + note.toString());
+//  }
+//  int pitch = note.getPitch();
+//  if(pitch>=36 && pitch <36+16){
+//    noteState[pitch-36]=true;
+//  }
+//}
+//
+//void noteOffReceived(Note note) {
+//  if (debugMode) {
+//    println("Note Off: " + note.toString());
+//  }
+//  int pitch = note.getPitch();
+//  if(pitch>=36 && pitch <36+16){
+//    noteState[pitch-36]=false;
+//  }
+//}
+//
+//void oscEvent(OscMessage theOscMessage) {
+//  println(theOscMessage);
+//  LXPattern currentPattern = lx.getPattern();
+//  if (currentPattern instanceof OSCPattern) {
+//    ((OSCPattern)currentPattern).oscEvent(theOscMessage);
+//  }
+//}
+//
+
+
+class ObjectMuckerEffect extends SCEffect {
+  ObjectMuckerEffect(GLucose glucose) {
+    super(glucose);
+  }
+  public void apply(int[] colors){
+    /*for(Strip s: model.strips){
+      for(int i=0; i<s.points.size(); i++){
+         int index = s.points.get(i).index;
+         color c = colors[index];
+         colors[index] = lx.hsb((i*22.5), saturation(c), brightness(c));
+      }
+    }*/
+  }
+}
+
+class BlendFrames extends SCEffect {
+  int fcount;
+  int frames[][];
+  int maxfbuf;
+  int blendfactor;
+  BlendFrames(GLucose glucose) {
+    super(glucose);
+    maxfbuf = 30;
+    blendfactor=30;
+    fcount=0;
+    frames = new int[maxfbuf][];
+    for(int i=0; i<maxfbuf; i++){
+       frames[i] = new int[model.points.size()];       
+    }
+  }
+  public void apply(int[] colors) {
+    if(fcount<maxfbuf){
+      for(int i=0; i<colors.length; i++){
+        frames[(maxfbuf-1)-fcount][i]=colors[i];
+      }
+      fcount++;
+      return;
+    } else {
+      for(int i=maxfbuf-1; i>0; i--){
+        frames[i] = frames[i-1];
+      }
+      frames[0] = new int[model.points.size()];
+      
+      for(int i=0; i<colors.length; i++){
+        int r,g,b;
+        r=g=b=0;
+        for(int j=0; j<blendfactor; j++){          
+          if(j==0) { frames[0][i] = colors[i]; }
+          r += ((frames[j][i] >> 16) & 0xFF);
+          g += ((frames[j][i] >> 8) & 0xFF);
+          b += ((frames[j][i] >> 0) & 0xFF);
+        }
+        r/=blendfactor;
+        g/=blendfactor;
+        b/=blendfactor;
+        colorMode(ARGB);
+        colors[i] = (0xFF << 24) | (r << 16) | (g << 8) | b;
+        colorMode(HSB);
+      }
+            
+    }
+  }
+}
+
+
+
+
+
+
+
+abstract class OSCPattern extends SCPattern {
+  public OSCPattern(GLucose glucose){super(glucose);}
+  public abstract void oscEvent(OscMessage msg);
+}
+
+class Ball {
+  public int lastSeen;
+  public float x,y;
+  public Ball(){
+    x=y=lastSeen=0;  
+  }
+}
+
+class OSC_Balls extends OSCPattern {
+  Ball[] balls;
+  public OSC_Balls(GLucose glucose){
+    super(glucose);
+    balls = new Ball[20];
+    for(int i=0; i<balls.length; i++) { balls[i] = new Ball(); }    
+  }
+  public void oscEvent(OscMessage msg){
+    String pattern[] = split(msg.addrPattern(), "/");    
+    int ballnum = PApplet.parseInt(pattern[3]);
+    balls[ballnum].lastSeen=millis();
+    balls[ballnum].x = msg.get(0).floatValue();
+    balls[ballnum].y = msg.get(1).floatValue();    
+  }
+  
+  public void run(double deltaMs){
+    for(Point p: model.points){ colors[p.index]=0; }
+    for(int i=1; i<balls.length; i++){
+      if(millis() - balls[i].lastSeen < 1000) {
+        for(Point p: model.points){
+          int x = PApplet.parseInt(balls[i].x * 255.0f);
+          int y = PApplet.parseInt(balls[i].y * 127.0f);
+          if(p.x < x+4 && p.x > x-4 && p.y < y+4 && p.y > y-4) { colors[p.index] = 0xffFF0000; } 
+        }
+      }
+    }
+  }
+}
+
+
+
+
+/*class ScreenScrape extends SCPattern {
+  PImage pret;
+  ScreenShot ss;
+  public ScreenScrape(GLucose glucose) {
+    super(glucose);
+    System.loadLibrary("ScreenShot");
+    pret = new PImage(8, 128, ARGB);
+    ss = new ScreenShot();
+  }
+  void run(double deltaMs){
+     int x=(1366/2)+516;
+     int y=768-516;
+     int w=8;
+     int h=128;
+     pret.pixels = ss.getScreenShotJNI2(x, y, w, h);
+     //for(int i=0; i<px.length; i++){ pret.pixels[i] = px[i]; }
+     //println(pret.get(10,10));
+     for(Point p: model.points){
+       colors[p.index] = pret.get((int(p.x)/8)*8, 128-int(p.y));
+     }     
+  }
+}*/
+
+//----------------------------------------------------------------------------------------------------------------------------------
+int                    NumApcRows=4, NumApcCols=8;
+
+public boolean btwn    (int            a,int    b,int          c)              { return a >= b && a <= c;      }
+public boolean btwn    (double         a,double b,double       c)              { return a >= b && a <= c;      }
+public float   interp  (float a, float b, float c) { return (1-a)*b + a*c; }
+public float   randctr (float a) { return random(a) - a*.5f; }
+public float   min             (float a, float b, float c, float d) { return min(min(a,b),min(c,d));   }
+public float   pointDist(Point p1, Point p2) { return dist(p1.x,p1.y,p1.z,p2.x,p2.y,p2.z);     }
+public float   xyDist   (Point p1, Point p2) { return dist(p1.x,p1.y,p2.x,p2.y);                               }
+public float   distToSeg(float x, float y, float x1, float y1, float x2, float y2) {
+       float A                         = x - x1, B = y - y1, C = x2 - x1, D = y2 - y1;
+       float dot                       = A * C + B * D, len_sq = C * C + D * D;
+       float xx, yy,param      = dot / len_sq;
+       
+       if (param < 0 || (x1 == x2 && y1 == y2)) {      xx = x1; yy = y1; }
+       else if (param > 1) {                                           xx = x2; yy = y2; }
+       else {                                                                          xx = x1 + param * C;
+                                                                                               yy = y1 + param * D; }
+       float dx = x - xx, dy = y - yy;
+       return sqrt(dx * dx + dy * dy);
+}
+
+public class Pick {
+       int     NumPicks, Default       ,       
+                       CurRow  , CurCol        ,
+                       StartRow, EndRow        ;
+       String  tag             , Desc[]        ;
+
+       Pick    (String label, int _Def, int _Num,      int nStart, String d[]) {
+               NumPicks        = _Num;         Default = _Def; 
+               StartRow        = nStart;       EndRow  = StartRow + floor((NumPicks-1) / NumApcCols);
+               tag                     = label;        Desc    = d;
+               reset();
+       }
+
+       public int              Cur()                   { return (CurRow-StartRow)*NumApcCols + CurCol;                                 }
+       public String   CurDesc()               { return Desc[Cur()]; }
+       public void     reset()                 { CurCol = Default % NumApcCols; CurRow = StartRow + Default / NumApcCols; }
+
+       public boolean set(int r, int c)        {
+               if (!btwn(r,StartRow,EndRow) || !btwn(c,0,NumApcCols-1) ||
+                       !btwn((r-StartRow)*NumApcCols + c,0,NumPicks-1))        return false;
+               CurRow=r; CurCol=c;                                                                     return true;
+       }
+}
+
+public class DBool {
+       boolean def, b;
+       String  tag;
+       int             row, col;
+       public void     reset() { b = def; }
+       public boolean set      (int r, int c, boolean val) { if (r != row || c != col) return false; b = val; return true; }
+       DBool(String _tag, boolean _def, int _row, int _col) {
+               def = _def; b = _def; tag = _tag; row = _row; col = _col;
+       }
+}
+//----------------------------------------------------------------------------------------------------------------------------------
+public class DPat extends SCPattern
+{
+       ArrayList<Pick>   picks  = new ArrayList<Pick>  ();
+       ArrayList<DBool>  bools  = new ArrayList<DBool> ();
+
+       PVector         mMax, mCtr, mHalf;
+
+       MidiOutput  APCOut;
+       int                     nMaxRow         = 53;
+       float           LastJog = -1;
+       float[]         xWaveNz, yWaveNz;
+       int             nPoint  , nPoints;
+       PVector         xyzJog = new PVector(), modmin;
+
+       float                   NoiseMove       = random(10000);
+       BasicParameter  pSpark, pWave, pRotX, pRotY, pRotZ, pSpin, pTransX, pTransY;
+       DBool                   pXsym, pYsym, pRsym, pXdup, pXtrip, pJog, pGrey;
+
+       public float            lxh             ()                                                                      { return lx.getBaseHuef();                                                                                      }
+       public int                      c1c              (float a)                                                      { return round(100*constrain(a,0,1));                                                           }
+       public float            interpWv(float i, float[] vals)                         { return interp(i-floor(i), vals[floor(i)], vals[ceil(i)]);             }
+       public void             setNorm (PVector vec)                                           { vec.set(vec.x/mMax.x, vec.y/mMax.y, vec.z/mMax.z);                            }
+       public void             setRand (PVector vec)                                           { vec.set(random(mMax.x), random(mMax.y), random(mMax.z));                      }
+       public void             setVec  (PVector vec, Point p)                          { vec.set(p.x, p.y, p.z);                                                                                       }
+       public void             interpolate(float i, PVector a, PVector b)      { a.set(interp(i,a.x,b.x), interp(i,a.y,b.y), interp(i,a.z,b.z));       }
+       public void             StartRun(double deltaMs)                                        { }
+       public float            val             (BasicParameter p)                                      { return p.getValuef();                                                                                         }
+       public int              CalcPoint(PVector p)                                            { return lx.hsb(0,0,0);                                                                                         }
+       public int              blend3(int c1, int c2, int c3)          { return blendColor(c1,blendColor(c2,c3,ADD),ADD);                                      }
+
+       public void     rotateZ (PVector p, PVector o, float nSin, float nCos) { p.set(    nCos*(p.x-o.x) - nSin*(p.y-o.y) + o.x    , nSin*(p.x-o.x) + nCos*(p.y-o.y) + o.y,p.z); }
+       public void     rotateX (PVector p, PVector o, float nSin, float nCos) { p.set(p.x,nCos*(p.y-o.y) - nSin*(p.z-o.z) + o.y    , nSin*(p.y-o.y) + nCos*(p.z-o.z) + o.z    ); }
+       public void     rotateY (PVector p, PVector o, float nSin, float nCos) { p.set(    nSin*(p.z-o.z) + nCos*(p.x-o.x) + o.x,p.y, nCos*(p.z-o.z) - nSin*(p.x-o.x) + o.z    ); }
+
+       public BasicParameter   addParam(String label, double value)    { BasicParameter p = new BasicParameter(label, value); addParameter(p); return p; }
+
+       PVector         vT1 = new PVector(), vT2 = new PVector();
+       public float            calcCone (PVector v1, PVector v2, PVector c)    {       vT1.set(v1); vT2.set(v2); vT1.sub(c); vT2.sub(c);
+                                                                                                                                       return degrees(PVector.angleBetween(vT1,vT2)); }
+
+       public Pick             addPick(String name, int def, int _max, String[] desc) {
+               Pick P          = new Pick(name, def, _max+1, nMaxRow, desc); 
+               nMaxRow         = P.EndRow + 1;
+               picks.add(P);
+               return P;
+       }
+
+    public boolean     noteOff(Note note) {
+               int row = note.getPitch(), col = note.getChannel();
+               for (int i=0; i<bools.size(); i++) if (bools.get(i).set(row, col, false)) { presetManager.dirty(this); return true; }
+               updateLights(); return false;
+       }
+
+    public boolean     noteOn(Note note) {
+               int row = note.getPitch(), col = note.getChannel();
+               for (int i=0; i<picks.size(); i++) if (picks.get(i).set(row, col))                      { presetManager.dirty(this); return true; }
+               for (int i=0; i<bools.size(); i++) if (bools.get(i).set(row, col, true))        { presetManager.dirty(this); return true; }
+               println("row: " + row + "  col:   " + col); return false;
+       }
+
+       public void             onInactive()                    { uiDebugText.setText(""); }
+       public void             onReset()                               {
+               for (int i=0; i<bools .size(); i++) bools.get(i).reset();
+               for (int i=0; i<picks .size(); i++) picks.get(i).reset();
+               presetManager.dirty(this); 
+               updateLights(); 
+       }
+
+       DPat(GLucose glucose) {
+               super(glucose);
+
+               pSpark          =       addParam("Sprk",  0);
+               pWave           =       addParam("Wave",  0);
+               pTransX         =       addParam("TrnX", .5f);
+               pTransY         =       addParam("TrnY", .5f);
+               pRotX           =       addParam("RotX", .5f);
+               pRotY           =       addParam("RotY", .5f);
+               pRotZ           =       addParam("RotZ", .5f);
+               pSpin           =       addParam("Spin", .5f);
+
+               nPoints         =       model.points.size();
+               pXsym           =       new DBool("X-SYM", false, 48, 0);       bools.add(pXsym );
+               pYsym           =       new DBool("Y-SYM", false, 48, 1);       bools.add(pYsym );
+               pRsym           =       new DBool("R-SYM", false, 48, 2);       bools.add(pRsym );
+               pXdup           =       new DBool("X-DUP", false, 48, 3);       bools.add(pXdup );
+               pJog            =       new DBool("JOG"  , false, 48, 4);       bools.add(pJog  );
+               pGrey           =       new DBool("GREY" , false, 48, 5);       bools.add(pGrey );
+
+               modmin          =       new PVector(model.xMin, model.yMin, model.zMin);
+               mMax            =       new PVector(model.xMax, model.yMax, model.zMax); mMax.sub(modmin);
+               mCtr            =       new PVector(); mCtr.set(mMax); mCtr.mult(.5f);
+               mHalf           =       new PVector(.5f,.5f,.5f);
+               xWaveNz         =       new float[ceil(mMax.y)+1];
+               yWaveNz         =       new float[ceil(mMax.x)+1];
+
+               //println (model.xMin + " " + model.yMin + " " +  model.zMin);
+               //println (model.xMax + " " + model.yMax + " " +  model.zMax);
+         //for (MidiOutputDevice o: RWMidi.getOutputDevices()) { if (o.toString().contains("APC")) { APCOut = o.createOutput(); break;}}
+       }
+
+       public float spin() {
+         float raw = val(pSpin);
+         if (raw <= 0.45f) {
+           return raw + 0.05f;
+         } else if (raw >= 0.55f) {
+           return raw - 0.05f;
+    }
+    return 0.5f;
+       }
+       
+       public void setAPCOutput(MidiOutput output) {
+         APCOut = output;
+       }
+
+       public void updateLights() { if (APCOut == null) return;
+           for (int i = 0; i < NumApcRows; ++i) 
+               for (int j = 0; j < 8; ++j)             APCOut.sendNoteOn(j, 53+i,  0);
+               for (int i=0; i<picks .size(); i++)     APCOut.sendNoteOn(picks.get(i).CurCol, picks.get(i).CurRow, 3);
+               for (int i=0; i<bools .size(); i++)     if (bools.get(i).b)     APCOut.sendNoteOn       (bools.get(i).col, bools.get(i).row, 1);
+                                                                                               else                                    APCOut.sendNoteOff      (bools.get(i).col, bools.get(i).row, 0);
+       }
+
+       public void run(double deltaMs)
+       {
+               if (deltaMs > 100) return;
+
+               if (this == midiEngine.getFocusedDeck().getActivePattern()) {
+                       String Text1="", Text2="";
+                       for (int i=0; i<bools.size(); i++) if (bools.get(i).b) Text1 += " " + bools.get(i).tag       + "   ";
+                       for (int i=0; i<picks.size(); i++) Text1 += picks.get(i).tag + ": " + picks.get(i).CurDesc() + "   ";
+                       uiDebugText.setText(Text1, Text2);
+               }
+
+               NoiseMove       += deltaMs; NoiseMove = NoiseMove % 1e7f;
+               StartRun                (deltaMs);
+               PVector P               = new PVector(), tP = new PVector(), pSave = new PVector();
+               PVector pTrans  = new PVector(val(pTransX)*200-100, val(pTransY)*100-50,0);
+               nPoint  = 0;
+
+               if (pJog.b) {
+                       float tRamp     = (lx.tempo.rampf() % .25f);
+                       if (tRamp < LastJog) xyzJog.set(randctr(mMax.x*.2f), randctr(mMax.y*.2f), randctr(mMax.z*.2f));
+                       LastJog = tRamp; 
+               }
+
+               // precalculate this stuff
+               float wvAmp = val(pWave), sprk = val(pSpark);
+               if (wvAmp > 0) {
+                       for (int i=0; i<ceil(mMax.x)+1; i++)
+                               yWaveNz[i] = wvAmp * (noise(i/(mMax.x*.3f)-(2e3f+NoiseMove)/1500.f) - .5f) * (mMax.y/2.f);
+
+                       for (int i=0; i<ceil(mMax.y)+1; i++)
+                               xWaveNz[i] = wvAmp * (noise(i/(mMax.y*.3f)-(1e3f+NoiseMove)/1500.f) - .5f) * (mMax.x/2.f);
+               }
+
+               for (Point p : model.points) { nPoint++;
+                       setVec(P,p);
+                       P.sub(modmin);
+                       P.sub(pTrans);
+                       if (sprk  > 0) {P.y += sprk*randctr(50); P.x += sprk*randctr(50); P.z += sprk*randctr(50); }
+                       if (wvAmp > 0)  P.y += interpWv(p.x-modmin.x, yWaveNz);
+                       if (wvAmp > 0)  P.x += interpWv(p.y-modmin.y, xWaveNz);
+                       if (pJog.b)             P.add(xyzJog);
+
+
+                       int cNew, cOld = colors[p.index];
+                                                       { tP.set(P);                                                                    cNew = CalcPoint(tP);                                                   }
+                       if (pXsym.b)    { tP.set(mMax.x-P.x,P.y,P.z);                                   cNew = blendColor(cNew, CalcPoint(tP), ADD);    }
+                       if (pYsym.b)    { tP.set(P.x,mMax.y-P.y,P.z);                                   cNew = blendColor(cNew, CalcPoint(tP), ADD);    }
+                       if (pRsym.b)    { tP.set(mMax.x-P.x,mMax.y-P.y,mMax.z-P.z);             cNew = blendColor(cNew, CalcPoint(tP), ADD);    }
+                       if (pXdup.b)    { tP.set((P.x+mMax.x*.5f)%mMax.x,P.y,P.z);              cNew = blendColor(cNew, CalcPoint(tP), ADD);    }
+                       if (pGrey.b)    { cNew = lx.hsb(0, 0, lx.b(cNew)); }
+                       colors[p.index] = cNew;
+               }
+       }
+}
+//----------------------------------------------------------------------------------------------------------------------------------
+class dTurn { 
+       dVertex v; 
+       int pos0, pos1;
+       dTurn(int _pos0, dVertex _v, int _pos1) { v = _v; pos0 = _pos0; pos1 = _pos1; }
+}
+
+class dVertex {
+       dVertex c0, c1, c2, c3,         // connections on the cube
+                       opp, same;                      // opp - same strip, opp direction
+                                                               // same - same strut, diff strip, dir
+       dTurn   t0, t1, t2, t3;
+       Strip   s;
+       int     dir, ci;                // dir -- 1 or -1.
+                                                       // ci  -- color index
+
+       dVertex(Strip _s, Point _p)  { s = _s; ci  = _p.index; }
+       public Point    getPoint(int i)          { return s.points.get(dir>0 ? i : 15-i);  }
+       public void     setOpp(dVertex _opp) { opp = _opp; dir = (ci < opp.ci ? 1 : -1); }
+}
+//----------------------------------------------------------------------------------------------------------------------------------
+class dPixel   { dVertex v; int pos; dPixel(dVertex _v, int _pos) { v=_v; pos=_pos; } }
+class dLattice {
+       public void     addTurn  (dVertex v0, int pos0, dVertex v1, int pos1) { dTurn t = new dTurn(pos0, v1, pos1); 
+                                                                                                                                       if (v0.t0 == null) { v0.t0=t; return; }
+                                                                                                                                       if (v0.t1 == null) { v0.t1=t; return; }
+                                                                                                                                       if (v0.t2 == null) { v0.t2=t; return; }
+                                                                                                                                       if (v0.t3 == null) { v0.t3=t; return; }
+                                                                                                                               }
+       public float   dist2     (Strip s1, int pos1, Strip s2, int pos2)       {       return pointDist(s1.points.get(pos1), s2.points.get(pos2)); }
+       public float   pd2       (Point p1, float x, float y, float z)          {       return dist(p1.x,p1.y,p1.z,x,y,z); }
+       public boolean sameSame (Strip s1, Strip s2)                                            {       return max(dist2(s1, 0, s2, 0), dist2(s1,15, s2,15)) < 5 ;      }       // same strut, same direction
+       public boolean sameOpp  (Strip s1, Strip s2)                                            {       return max(dist2(s1, 0, s2,15), dist2(s1,15, s2,0 )) < 5 ;      }       // same strut, opp direction
+       public boolean sameBar  (Strip s1, Strip s2)                                            {       return sameSame(s1,s2) || sameOpp(s1,s2);                                       }       // 2 strips on same strut
+
+
+       public void     addJoint (dVertex v1, dVertex v2) {
+               // should probably replace parallel but further with the new one
+               if (v1.c0 != null && sameBar(v2.s, v1.c0.s)) return;
+               if (v1.c1 != null && sameBar(v2.s, v1.c1.s)) return;
+               if (v1.c2 != null && sameBar(v2.s, v1.c2.s)) return;
+               if (v1.c3 != null && sameBar(v2.s, v1.c3.s)) return;
+
+               if              (v1.c0 == null) v1.c0 = v2; 
+               else if (v1.c1 == null) v1.c1 = v2; 
+               else if (v1.c2 == null) v1.c2 = v2; 
+               else if (v1.c3 == null) v1.c3 = v2;
+       }
+
+       public dVertex v0(Strip s) { return (dVertex)s.obj1; }
+       public dVertex v1(Strip s) { return (dVertex)s.obj2; }
+
+       public dPixel getClosest(PVector p) {
+               dVertex v = null; int pos=0; float d = 500;
+
+               for (Strip s : glucose.model.strips) {
+                       float nd = pd2(s.points.get(0),p.x,p.y,p.z); if (nd < d) { v=v0(s); d=nd; pos=0; }
+                       if (nd > 30) continue;
+                       for (int k=0; k<=15; k++) {
+                               nd = pd2(s.points.get(k),p.x,p.y,p.z); if (nd < d) { v =v0(s); d=nd; pos=k; }
+                       }
+               }
+               return random(2) < 1 ? new dPixel(v,pos) : new dPixel(v.opp,15-pos);
+       }
+
+       dLattice() {
+               lattice=this;
+
+               for (Strip s  : glucose.model.strips) {
+                       dVertex vrtx0 = new dVertex(s,s.points.get(0 )); s.obj1=vrtx0;
+                       dVertex vrtx1 = new dVertex(s,s.points.get(15)); s.obj2=vrtx1;
+                       vrtx0.setOpp(vrtx1); vrtx1.setOpp(vrtx0);
+               }
+
+               for (Strip s1 : glucose.model.strips) { for (Strip s2 : glucose.model.strips) {
+                       if (s1.points.get(0).index < s2.points.get(0).index) continue;
+                       int c=0;
+                       if (sameSame(s1,s2))    {       v0(s1).same = v0(s2); v1(s1).same = v1(s2);
+                                                                               v0(s2).same = v0(s1); v1(s2).same = v1(s1); continue; } // parallel
+                       if (sameOpp (s1,s2))    {       v0(s1).same = v1(s2); v1(s1).same = v0(s2);
+                                                                               v0(s2).same = v1(s1); v1(s2).same = v0(s1); continue; } // parallel
+                       if (dist2(s1, 0, s2, 0) < 5) { c++; addJoint(v1(s1), v0(s2)); addJoint(v1(s2), v0(s1)); }
+                       if (dist2(s1, 0, s2,15) < 5) { c++; addJoint(v1(s1), v1(s2)); addJoint(v0(s2), v0(s1)); }
+                       if (dist2(s1,15, s2, 0) < 5) { c++; addJoint(v0(s1), v0(s2)); addJoint(v1(s2), v1(s1)); }
+                       if (dist2(s1,15, s2,15) < 5) { c++; addJoint(v0(s1), v1(s2)); addJoint(v0(s2), v1(s1)); }
+                       if (c>0) continue;
+
+                       // Are they touching at all?
+                       int pos1=0, pos2=0; float d = 100;
+
+                       while (pos1 < 15 || pos2 < 15) {
+                               float oldD = d;
+                               if (pos1<15) { float d2 = dist2(s1, pos1+1, s2, pos2+0); if (d2 < d) { d=d2; pos1++; } }
+                               if (pos2<15) { float d2 = dist2(s1, pos1+0, s2, pos2+1); if (d2 < d) { d=d2; pos2++; } }
+                               if (d > 50  || oldD == d) break ;
+                       }
+
+                       if (d>5) continue;
+                       addTurn(v0(s1), pos1, v0(s2), pos2); addTurn(v1(s1), 15-pos1, v0(s2), pos2); 
+                       addTurn(v0(s2), pos2, v0(s1), pos1); addTurn(v1(s2), 15-pos2, v0(s1), pos1);
+               }}
+       }
+}
+
+dLattice lattice;
+//----------------------------------------------------------------------------------------------------------------------------------
+
+class Graphic
+{
+       public boolean changed = false;
+       public int position  = 0;
+       public ArrayList<Integer> graphicBuffer;
+       Graphic()
+       {       
+               graphicBuffer = new ArrayList<Integer>();
+       }
+       public int width()
+       {
+               return graphicBuffer.size();
+       }
+
+       
+};
+class Granim extends Graphic
+{
+       HashMap<String,Graphic> displayList;
+       
+       Granim()
+       {
+               displayList = new HashMap<String,Graphic>();
+       }
+       public Graphic addGraphic(String name, Graphic g)
+       {
+               while(width()< g.position+1)
+               {
+                               graphicBuffer.add(lx.hsb(0,0,0));
+               }
+               drawAll();
+               displayList.put(name , g);
+               changed =true;
+               return g;
+       }
+
+       public Graphic getGraphicByName(String name)
+       {
+               return displayList.get(name);
+       }
+
+       public void update()
+       {
+               
+               for(Graphic g : displayList.values())
+               {
+                       if(g instanceof Granim)
+                       {
+                               ((Granim) g).update();
+                               
+                       }
+                       changed = changed || g.changed;
+                       if(changed)
+                       {
+                               while(width()< g.position + g.width())
+                               {
+                                       graphicBuffer.add(lx.hsb(0,0,0));
+                               }
+                               if(g.changed)
+                               {
+                                       drawOne(g);
+                                       g.changed =false;
+                               }
+                       }
+               }
+               changed = false;
+
+       }
+       public void drawOne(Graphic g)
+       {
+               graphicBuffer.addAll(g.position,g.graphicBuffer);
+       }
+       public void drawAll()
+       {
+       }
+};
+class GranimPattern extends SCPattern
+{
+       HashMap<String,Graphic> displayList;
+
+       GranimPattern(GLucose glucose)
+       {
+               super(glucose);
+               displayList = new HashMap<String,Graphic>();
+       }
+
+       public Graphic addGraphic(String name, Graphic g)
+       {
+               displayList.put(name,g);
+               return g;
+       }
+
+       public Graphic getGraphicByName(String name)
+       {
+               return displayList.get(name);
+       }
+
+       public void run(double deltaMs) 
+       {
+               drawToPointList();
+       }
+       private Integer[] gbuffer;
+       public void drawToPointList()
+       {
+               for(Graphic g : displayList.values())
+               {
+                       if(g instanceof Granim)
+                       {
+                               ((Granim) g).update();
+                       }
+                       List<Point> drawList = model.points.subList(Math.min(g.position,colors.length-1), Math.min(g.position + g.width(),colors.length-1));
+                       //println("drawlistsize "+drawList.size());
+                       
+                       gbuffer = g.graphicBuffer.toArray(new Integer[0]);
+                       
+                       for (int i=0; i < drawList.size(); i++)
+                       {
+                               colors[drawList.get(i).index] = gbuffer[i];
+                       }
+                       g.changed = false;
+               }
+       }
+
+};
+
+class RedsGraphic extends Graphic
+{
+       RedsGraphic()
+       {
+               super();
+               drawit(10);
+       }
+       RedsGraphic(int len)
+       {
+               super();
+               drawit(len);
+               
+       }
+       public void drawit(int len)
+       {
+               for(int i = 0; i < len ;i++)
+               {
+                       graphicBuffer.add(lx.hsb(0,255,255));
+               }
+       }
+};
+
+class RedsGranim extends Granim
+{
+       RedsGranim()
+       {
+               super();
+               addGraphic("myreds", new RedsGraphic(10));
+       }
+       RedsGranim(int len)
+       {
+               super();
+               addGraphic("myreds", new RedsGraphic(len));
+       }
+       public float count = 0.0f;
+       public void update()
+       {
+               Graphic g=getGraphicByName("myreds");
+               g.position = Math.round(sin(count)*20)+100;
+               count+= 0.1f;
+               if(count>Math.PI*2)
+               {
+                       count=0;
+               }
+               super.update();
+       }
+       
+};
+
+class RandomsGranim extends Granim
+{
+       private int _len =0 ;
+       RandomsGranim()
+       {
+               super();
+               _len =100;
+               addGraphic("myrandoms", makeGraphic(_len));
+       }
+       RandomsGranim(int len)
+       {
+               super();
+               _len=len;
+               addGraphic("myrandoms", makeGraphic(len));
+       }
+       int colorLid=0;
+       public Graphic makeGraphic(int len)
+       {
+
+               int[] colors= new int[len]; 
+               for(int i =0;i<len;i++)
+               {
+                       colors[i]=(int) Math.round(Math.random()*80)+colorLid;
+                       
+               }
+               colorLid+=4;
+               return new ColorDotsGraphic(colors);
+       }
+       private int count =1;
+       private int instanceCount =0;
+       public void update()
+       {
+               
+               if(instanceCount<90 && count % 20==0)
+               {
+                       instanceCount++;
+                       Graphic h=addGraphic("myrandoms_"+instanceCount, makeGraphic(_len));
+                       h.position = instanceCount*(_len+100);
+                       //println("one more " + instanceCount+" at "+h.position);
+                       count=0;
+                       changed = true;
+               }
+               count++;
+               super.update();
+       }
+       
+};
+
+
+class ColorDotsGraphic extends Graphic
+{
+       ColorDotsGraphic(int[] colorSequence)
+       {
+               super();
+               for (int colorVal : colorSequence)
+               {
+                       graphicBuffer.add(lx.hsb(colorVal, 255, 255));
+               }
+               changed = true;
+       }
+};
+int BLACK = 0xff000000;
+
+class Gimbal extends SCPattern {
+
+  private final boolean DEBUG_MANUAL_ABG = false;
+  private final int MAXIMUM_BEATS_PER_REVOLUTION = 100;
+  
+  private boolean first_run = true;
+  private final Projection projection;
+  private final BasicParameter beatsPerRevolutionParam = new BasicParameter("SLOW", 20.f/MAXIMUM_BEATS_PER_REVOLUTION);
+  private final BasicParameter hueDeltaParam = new BasicParameter("HUED", 60.f/360);
+  private final BasicParameter fadeFromCoreParam = new BasicParameter("FADE", 1);
+  private final BasicParameter girthParam = new BasicParameter("GRTH", .18f);
+  private final BasicParameter ringExtendParam = new BasicParameter("XTND", 1);
+  private final BasicParameter relativeSpeedParam = new BasicParameter("RLSP", .83f);
+  private final BasicParameter sizeParam = new BasicParameter("SIZE", .9f);
+
+  private final BasicParameter aP = new BasicParameter("a", 0);
+  private final BasicParameter bP = new BasicParameter("b", 0);
+  private final BasicParameter gP = new BasicParameter("g", 0);
+
+  Gimbal(GLucose glucose) {
+    super(glucose);
+    projection = new Projection(model);
+    addParameter(beatsPerRevolutionParam);
+    addParameter(hueDeltaParam);
+    addParameter(fadeFromCoreParam);
+    addParameter(girthParam);
+    addParameter(ringExtendParam);
+    addParameter(relativeSpeedParam);
+    addParameter(sizeParam);
+    
+    if (DEBUG_MANUAL_ABG) {
+      addParameter(aP);
+      addParameter(bP);
+      addParameter(gP);
+    }
+  }
+
+  float a = 0, b = 0, g = 0;
+
+  public void run(double deltaMs) {
+
+    if (DEBUG_MANUAL_ABG) {
+      a = aP.getValuef() * (2 * PI); 
+      b = bP.getValuef() * (2 * PI);
+      g = gP.getValuef() * (2 * PI);
+    } else {
+      float relativeSpeed = relativeSpeedParam.getValuef();
+      float time = millis() / 1000.f;
+      
+      int beatsPerRevolution = (int) (beatsPerRevolutionParam.getValuef() * MAXIMUM_BEATS_PER_REVOLUTION) + 1;
+      float radiansPerMs = 2 * PI             // radians / revolution
+                         / beatsPerRevolution // beats / revolution
+                         * lx.tempo.bpmf()    // BPM beats / min
+                         / 60                 // sec / min
+                         / 1000;              // ms / sec
+      
+      a += deltaMs * radiansPerMs * pow(relativeSpeed, 0);
+      b += deltaMs * radiansPerMs * pow(relativeSpeed, 1);
+      g += deltaMs * radiansPerMs * pow(relativeSpeed, 2);
+      a %= 2 * PI;
+      b %= 2 * PI;
+      g %= 2 * PI;
+    }
+
+    float hue = lx.getBaseHuef();
+    float hue_delta = hueDeltaParam.getValuef() * 360;
+    
+    float radius1 = model.xMax / 2 * sizeParam.getValuef();
+    float radius2 = ((model.xMax + model.yMax) / 2) / 2 * sizeParam.getValuef();
+    float radius3 = model.yMax / 2 * sizeParam.getValuef();
+    float girth = model.xMax * girthParam.getValuef();
+    Ring ring1 = new Ring((hue + hue_delta * 0) % 360, radius1, girth);
+    Ring ring2 = new Ring((hue + hue_delta * 1) % 360, radius2, girth);
+    Ring ring3 = new Ring((hue + hue_delta * 2) % 360, radius3, girth);
+
+    projection.reset(model)
+      // Translate so the center of the car is the origin
+      .translateCenter(model, 0, 0, 0);
+
+    for (Coord c : projection) {
+      //if (first_run) println(c.x + "," + c.y + "," + c.z);
+
+      rotate3d(c, a, 0, 0);
+      rotate3d(c, PI/4, PI/4, PI/4);
+      int color1 = ring1.colorFor(c);
+
+      rotate3d(c, 0, b, 0);
+      int color2 = ring2.colorFor(c);
+
+      rotate3d(c, 0, 0, g);
+      int color3 = ring3.colorFor(c);
+            
+      colors[c.index] = specialBlend(color1, color2, color3);      
+    }
+
+    first_run = false;
+  }
+
+  class Ring {
+
+    float hue;
+    float radius, girth;
+
+    public Ring(float hue, float radius, float girth) {
+      this.hue = hue;
+      this.radius = radius;
+      this.girth = girth;
+    }
+
+    public int colorFor(Coord c) {
+      float theta = atan2(c.y, c.x);
+      float nearest_circle_x = cos(theta) * radius;
+      float nearest_circle_y = sin(theta) * radius;
+      float nearest_circle_z = 0;
+
+      float distance_to_circle
+          = sqrt(pow(nearest_circle_x - c.x, 2)
+               + pow(nearest_circle_y - c.y, 2)
+               + pow(nearest_circle_z - c.z * ringExtendParam.getValuef(), 2));
+
+      float xy_distance = sqrt(c.x*c.x + c.y*c.y);
+      return lx.hsb(this.hue, 100, (1 - distance_to_circle / girth * fadeFromCoreParam.getValuef()) * 100);
+    }
+
+  }
+
+}
+
+
+
+
+
+
+class Zebra extends SCPattern {
+
+  private final Projection projection;
+  SinLFO angleM = new SinLFO(0, PI * 2, 30000);
+
+/*
+  SinLFO x, y, z, dx, dy, dz;
+  float cRad;
+  _P size;
+  */
+
+  Zebra(GLucose glucose) {
+    super(glucose);
+    projection = new Projection(model);
+
+    addModulator(angleM).trigger();
+  }
+
+  public int colorFor(Coord c) {
+    float hue = lx.getBaseHuef();
+
+
+
+
+/* SLIDE ALONG
+    c.x = c.x + millis() / 100.f;
+    */
+
+
+
+    int stripe_count = 12;
+    float stripe_width = model.xMax / (float)stripe_count;
+    if (Math.floor((c.x) / stripe_width) % 2 == 0) {
+      return lx.hsb(hue, 100, 100);
+    } else {
+      return lx.hsb((hue + 90) % 360, 100, 100);
+    }
+
+
+    /* OCTANTS
+
+    if ((isPositiveBit(c.x) + isPositiveBit(c.y) + isPositiveBit(c.z)) % 2 == 0) {
+      return lx.hsb(lx.getBaseHuef(), 100, 100);
+    } else {
+      return lx.hsb(0, 0, 0);
+    }
+    */
+  }
+
+  public int isPositiveBit(float f) {
+    return f > 0 ? 1 : 0;
+  }
+
+  public void run(double deltaMs) {
+    float a = (millis() / 1000.f) % (2 * PI);
+    float b = (millis() / 1200.f) % (2 * PI);
+    float g = (millis() / 1600.f) % (2 * PI);
+
+    projection.reset(model)
+      // Translate so the center of the car is the origin
+      .translateCenter(model, 0, 0, 0);
+
+    for (Coord c : projection) {
+//      rotate3d(c, a, b, g);
+      colors[c.index] = colorFor(c);
+    }
+
+    first_run = false;
+  }
+
+
+  // Utility!
+  boolean first_run = true;
+  private void log(String s) {
+    if (first_run) {
+      println(s);
+    }
+  }
+
+
+}
+
+public void rotate3d(Coord c, float a /* roll */, float b /* pitch */, float g /* yaw */) {
+  float cosa = cos(a);
+  float cosb = cos(b);
+  float cosg = cos(g);
+  float sina = sin(a);
+  float sinb = sin(b);
+  float sing = sin(g);
+
+  float a1 = cosa*cosb;
+  float a2 = cosa*sinb*sing - sina*cosg;
+  float a3 = cosa*sinb*cosg + sina*sing;
+  float b1 = sina*cosb;
+  float b2 = sina*sinb*sing + cosa*cosg;
+  float b3 = sina*sinb*cosg - cosa*sing;
+  float c1 = -sinb;
+  float c2 = cosb*sing;
+  float c3 = cosb*cosg;
+
+  float[] cArray = { c.x, c.y, c.z };
+  c.x = dotProduct(new float[] {a1, a2, a3}, cArray);
+  c.y = dotProduct(new float[] {b1, b2, b3}, cArray);
+  c.z = dotProduct(new float[] {c1, c2, c3}, cArray);
+}
+
+public float dotProduct(float[] a, float[] b) {
+  float ret = 0;
+  for (int i = 0 ; i < a.length; ++i) {
+    ret += a[i] * b[i];
+  }
+  return ret;
+}
+
+public int specialBlend(int c1, int c2, int c3) {
+  float h1 = hue(c1);
+  float h2 = hue(c2); 
+  float h3 = hue(c3);
+  
+  // force h1 < h2 < h3
+  while (h2 < h1) {
+    h2 += 360;
+  }
+  while (h3 < h2) {
+    h3 += 360;
+  }
+
+  float s1 = saturation(c1); 
+  float s2 = saturation(c2); 
+  float s3 = saturation(c3);
+  
+  float b1 = brightness(c1); 
+  float b2 = brightness(c2);
+  float b3 = brightness(c3);
+  float relative_b1 = b1 / (b1 + b2 + b3);
+  float relative_b2 = b2 / (b1 + b2 + b3);
+  float relative_b3 = b3 / (b1 + b2 + b3);
+  
+  return lx.hsb(
+    (h1 * relative_b1 + h2 * relative_b1 + h3 * relative_b3) % 360,
+     s1 * relative_b1 + s2 * relative_b2 + s3 * relative_b3,
+     max(max(b1, b2), b3)
+  );
+}
+
+/**
+ * A Projection of sin wave in 3d space. 
+ * It sort of looks like an animal swiming around in water.
+ * Angle sliders are sort of a work in progress that allow yo to change the crazy ways it moves around.
+ * Hue slider allows you to control how different the colors are along the wave. 
+ *
+ * This code copied heavily from Tim and Slee.
+ */
+class Swim extends SCPattern {
+
+  // Projection stuff
+  private final Projection projection;
+  SawLFO rotation = new SawLFO(0, TWO_PI, 19000);
+  SinLFO yPos = new SinLFO(-25, 25, 12323);
+  final BasicParameter xAngle = new BasicParameter("XANG", 0.9f);
+  final BasicParameter yAngle = new BasicParameter("YANG", 0.3f);
+  final BasicParameter zAngle = new BasicParameter("ZANG", 0.3f);
+
+  final BasicParameter hueScale = new BasicParameter("HUE", 0.3f);
+
+  public Swim(GLucose glucose) {
+    super(glucose);
+    projection = new Projection(model);
+
+    addParameter(xAngle);
+    addParameter(yAngle);
+    addParameter(zAngle);
+    addParameter(hueScale);
+
+    addModulator(rotation).trigger();
+    addModulator(yPos).trigger();
+  }
+
+
+  int beat = 0;
+  float prevRamp = 0;
+  public void run(double deltaMs) {
+
+    // Sync to the beat
+    float ramp = (float)lx.tempo.ramp();
+    if (ramp < prevRamp) {
+      beat = (beat + 1) % 4;
+    }
+    prevRamp = ramp;
+    float phase = (beat+ramp) / 2.0f * 2 * PI;
+
+    float denominator = max(xAngle.getValuef() + yAngle.getValuef() + zAngle.getValuef(), 1);
+
+    projection.reset(model)
+      // Swim around the world
+      .rotate(rotation.getValuef(), xAngle.getValuef() / denominator, yAngle.getValuef() / denominator, zAngle.getValuef() / denominator)
+        .translateCenter(model, 0, 50 + yPos.getValuef(), 0);
+
+    float model_height =  model.yMax - model.yMin;
+    float model_width =  model.xMax - model.xMin;
+    for (Coord p : projection) {
+      float x_percentage = (p.x - model.xMin)/model_width;
+
+      // Multiply by 1.4 to shrink the size of the sin wave to be less than the height of the cubes.
+      float y_in_range = 1.4f * (2*p.y - model.yMax - model.yMin) / model_height;
+      float sin_x =  sin(phase + 2 * PI * x_percentage);       
+
+      // Color fade near the top of the sin wave
+      float v1 = sin_x > y_in_range  ? (100 + 100*(y_in_range - sin_x)) : 0;     
+
+      float hue_color = (lx.getBaseHuef() + hueScale.getValuef() * (abs(p.x-model.xMax/2.f)*.3f + abs(p.y-model.yMax/2)*.9f + abs(p.z - model.zMax/2.f))) % 360;
+      colors[p.index] = lx.hsb(hue_color, 70, v1);
+    }
+  }
+}
+
+/** 
+ * The idea here is to do another sin wave pattern, but with less rotation and more of a breathing / heartbeat affect with spheres above / below the wave.
+ * This is not done.
+ */
+class Balance extends SCPattern {
+
+  final BasicParameter hueScale = new BasicParameter("Hue", 0.4f);
+
+  class Sphere {
+    float x, y, z;
+  }
+
+
+  // Projection stuff
+  private final Projection projection;
+
+  SinLFO sphere1Z = new SinLFO(0, 0, 15323);
+  SinLFO sphere2Z = new SinLFO(0, 0, 8323);
+  SinLFO rotationX = new SinLFO(-PI/32, PI/32, 9000);
+  SinLFO rotationY = new SinLFO(-PI/16, PI/16, 7000);
+  SinLFO rotationZ = new SinLFO(-PI/16, PI/16, 11000);
+  SawLFO phaseLFO = new SawLFO(0, 2 * PI, 5000 - 4500 * 0.5f);
+  final BasicParameter phaseParam = new BasicParameter("Spd", 0.5f);
+  final BasicParameter crazyParam = new BasicParameter("Crzy", 0.2f);
+
+
+  private final Sphere[] spheres;
+  private final float centerX, centerY, centerZ, modelHeight, modelWidth, modelDepth;
+  SinLFO heightMod = new SinLFO(0.8f, 1.9f, 17298);
+
+  public Balance(GLucose glucose) {
+    super(glucose);
+
+    projection = new Projection(model);
+
+    addParameter(hueScale);
+    addParameter(phaseParam);
+    addParameter(crazyParam);
+
+    spheres = new Sphere[2];
+    centerX = (model.xMax + model.xMin) / 2;
+    centerY = (model.yMax + model.yMin) / 2;
+    centerZ = (model.zMax + model.zMin) / 2;
+    modelHeight = model.yMax - model.yMin;
+    modelWidth = model.xMax - model.xMin;
+    modelDepth = model.zMax - model.zMin;
+
+    spheres[0] = new Sphere();
+    spheres[0].x = 1*modelWidth/2 + model.xMin;
+    spheres[0].y = centerY + 20;
+    spheres[0].z = centerZ;
+
+    spheres[1] = new Sphere();
+    spheres[1].x = model.xMin;
+    spheres[1].y = centerY - 20;
+    spheres[1].z = centerZ;
+
+    addModulator(rotationX).trigger();
+    addModulator(rotationY).trigger();
+    addModulator(rotationZ).trigger();
+
+
+    addModulator(sphere1Z).trigger();
+    addModulator(sphere2Z).trigger();
+    addModulator(phaseLFO).trigger();
+
+    addModulator(heightMod).trigger();
+  }
+
+  public void onParameterChanged(LXParameter parameter) {
+    if (parameter == phaseParam) {
+      phaseLFO.setDuration(5000 - 4500 * parameter.getValuef());
+    }
+  }
+
+  int beat = 0;
+  float prevRamp = 0;
+  public void run(double deltaMs) {
+
+    // Sync to the beat
+    float ramp = (float)lx.tempo.ramp();
+    if (ramp < prevRamp) {
+      beat = (beat + 1) % 4;
+    }
+    prevRamp = ramp;
+    float phase = phaseLFO.getValuef();
+
+    float crazy_factor = crazyParam.getValuef() / 0.2f;
+    projection.reset(model)
+      .rotate(rotationZ.getValuef() * crazy_factor,  0, 1, 0)
+        .rotate(rotationX.getValuef() * crazy_factor, 0, 0, 1)
+          .rotate(rotationY.getValuef() * crazy_factor, 0, 1, 0);
+
+    for (Coord p : projection) {
+      float x_percentage = (p.x - model.xMin)/modelWidth;
+
+      float y_in_range = heightMod.getValuef() * (2*p.y - model.yMax - model.yMin) / modelHeight;
+      float sin_x =  sin(PI / 2 + phase + 2 * PI * x_percentage);       
+
+      // Color fade near the top of the sin wave
+      float v1 = max(0, 100 * (1 - 4*abs(sin_x - y_in_range)));     
+
+      float hue_color = (lx.getBaseHuef() + hueScale.getValuef() * (abs(p.x-model.xMax/2.f) + abs(p.y-model.yMax/2)*.2f + abs(p.z - model.zMax/2.f)*.5f)) % 360;
+      int c = lx.hsb(hue_color, 80, v1);
+
+      // Now draw the spheres
+      for (Sphere s : spheres) {
+        float phase_x = (s.x - phase / (2 * PI) * modelWidth) % modelWidth;    
+        float x_dist = LXUtils.wrapdistf(p.x, phase_x, modelWidth);
+
+        float sphere_z = (s == spheres[0]) ? (s.z + sphere1Z.getValuef()) : (s.z - sphere2Z.getValuef()); 
+
+
+        float d = sqrt(pow(x_dist, 2) + pow(p.y - s.y, 2) + pow(p.z - sphere_z, 2));
+
+        float distance_from_beat =  (beat % 2 == 1) ? 1 - ramp : ramp;
+
+        min(ramp, 1-ramp);
+
+        float r = 40 - pow(distance_from_beat, 0.75f) * 20;
+
+        float distance_value = max(0, 1 - max(0, d - r) / 10);
+        float beat_value = 1.0f;
+
+        float value = min(beat_value, distance_value);
+
+        float sphere_color = (lx.getBaseHuef() - (1 - hueScale.getValuef()) * d/r * 45) % 360;
+
+        c = blendColor(c, lx.hsb((sphere_color + 270) % 360, 60, min(1, value) * 100), ADD);
+      }
+      colors[p.index] = c;
+    }
+  }
+}
+class Cathedrals extends SCPattern {
+  
+  private final BasicParameter xpos = new BasicParameter("XPOS", 0.5f);
+  private final BasicParameter wid = new BasicParameter("WID", 0.5f);
+  private final BasicParameter arms = new BasicParameter("ARMS", 0.5f);
+  private final BasicParameter sat = new BasicParameter("SAT", 0.5f);
+  private GraphicEQ eq;
+  
+  Cathedrals(GLucose glucose) {
+    super(glucose);
+    addParameter(xpos);
+    addParameter(wid);
+    addParameter(arms);
+    addParameter(sat);
+  }
+  protected void onActive() {
+    if (eq == null) {
+      eq = new GraphicEQ(lx, 16);
+      eq.slope.setValue(0.7f);
+      eq.range.setValue(0.4f);
+      eq.attack.setValue(0.4f);
+      eq.release.setValue(0.4f);
+      addParameter(eq.level);
+      addParameter(eq.range);
+      addParameter(eq.attack);
+      addParameter(eq.release);
+      addParameter(eq.slope);
+    }
+  }
+
+  public void run(double deltaMs) {
+    eq.run(deltaMs);
+    float bassLevel = eq.getAverageLevel(0, 4);
+    float trebleLevel = eq.getAverageLevel(8, 6);
+    
+    float falloff = 100 / (2 + 14*wid.getValuef());
+    float cx = model.xMin + (model.xMax-model.xMin) * xpos.getValuef();
+    float barm = 12 + 60*arms.getValuef()*max(0, 2*(bassLevel-0.1f));
+    float tarm = 12 + 60*arms.getValuef()*max(0, 2*(trebleLevel-0.1f));
+    
+    float arm = 0;
+    float middle = 0;
+    
+    float sf = 100.f / (70 - 69.9f*sat.getValuef());
+
+    for (Point p : model.points) {
+      float d = MAX_FLOAT;
+      if (p.y > model.cy) {
+        arm = tarm;
+        middle = model.yMax * 3/5.f;
+      } else {
+        arm = barm;
+        middle = model.yMax * 1/5.f;
+      }
+      if (abs(p.x - cx) < arm) {
+        d = min(abs(p.x - cx), abs(p.y - middle));
+      }
+      colors[p.index] = color(
+        (lx.getBaseHuef() + .2f*abs(p.y - model.cy)) % 360,
+        min(100, sf*dist(abs(p.x - cx), p.y, arm, middle)),
+        max(0, 120 - d*falloff));
+    }
+  } 
+}
+  
+class MidiMusic extends SCPattern {
+  
+  private final Stack<LXLayer> newLayers = new Stack<LXLayer>();
+  
+  private final Map<Integer, LightUp> lightMap = new HashMap<Integer, LightUp>();
+  private final List<LightUp> lights = new ArrayList<LightUp>();
+  private final BasicParameter lightSize = new BasicParameter("SIZE", 0.5f);
+
+  private final List<Sweep> sweeps = new ArrayList<Sweep>();
+
+  private final LinearEnvelope sparkle = new LinearEnvelope(0, 1, 500);
+  private boolean sparkleDirection = true;
+  private float sparkleBright = 100;
+  
+  private final BasicParameter wave = new BasicParameter("WAVE", 0);
+  
+  MidiMusic(GLucose glucose) {
+    super(glucose);
+    addParameter(lightSize);
+    addParameter(wave);
+    addModulator(sparkle).setValue(1);
+  }
+  
+  public void onReset() {
+    for (LightUp light : lights) {
+      light.noteOff(null);
+    }
+  }
+  
+  class Sweep extends LXLayer {
+    
+    final LinearEnvelope position = new LinearEnvelope(0, 1, 1000);
+    float bright = 100;
+    float falloff = 10;
+    
+    Sweep() {
+      addModulator(position);
+    }
+    
+    public void run(double deltaMs, int[] colors) {
+      if (!position.isRunning()) {
+        return;
+      }
+      float posf = position.getValuef();
+      for (Point p : model.points) {
+        colors[p.index] = blendColor(colors[p.index], color(
+          (lx.getBaseHuef() + .2f*abs(p.x - model.cx) + .2f*abs(p.y - model.cy)) % 360,
+          100,
+          max(0, bright - posf*100 - falloff*abs(p.y - posf*model.yMax))
+        ), ADD);
+      }
+    }
+  }
+  
+  class LightUp extends LXLayer {
+    
+    private final LinearEnvelope brt = new LinearEnvelope(0, 0, 0);
+    private final Accelerator yPos = new Accelerator(0, 0, 0);
+    private float xPos;
+    
+    LightUp() {
+      addModulator(brt);
+      addModulator(yPos);
+    }
+    
+    public boolean isAvailable() {
+      return brt.getValuef() <= 0;
+    }
+    
+    public void noteOn(Note note) {
+      xPos = lerp(0, model.xMax, constrain(0.5f + (note.getPitch() - 60) / 28.f, 0, 1));
+      yPos.setValue(lerp(20, model.yMax*.72f, note.getVelocity() / 127.f)).stop();
+      brt.setRangeFromHereTo(lerp(40, 100, note.getVelocity() / 127.f), 20).start();     
+    }
+
+    public void noteOff(Note note) {
+      yPos.setVelocity(0).setAcceleration(-380).start();
+      brt.setRangeFromHereTo(0, 1000).start();
+    }
+    
+    public void run(double deltaMs, int[] colors) {
+      float bVal = brt.getValuef();
+      if (bVal <= 0) {
+        return;
+      }
+      float yVal = yPos.getValuef();
+      for (Point p : model.points) {
+        float falloff = 6 - 5*lightSize.getValuef();
+        float b = max(0, bVal - falloff*dist(p.x, p.y, xPos, yVal));
+        if (b > 0) {
+          colors[p.index] = blendColor(colors[p.index], lx.hsb(
+            (lx.getBaseHuef() + .2f*abs(p.x - model.cx) + .2f*abs(p.y - model.cy)) % 360,
+            100,
+            b
+          ), ADD);
+        }
+      }
+    }
+  }
+  
+  private LightUp getLight() {
+    for (LightUp light : lights) {
+      if (light.isAvailable()) {
+        return light;
+      }
+    }
+    LightUp newLight = new LightUp();
+    lights.add(newLight);
+    synchronized(newLayers) {
+      newLayers.push(newLight);
+    }
+    return newLight;
+  }
+  
+  private Sweep getSweep() {
+    for (Sweep s : sweeps) {
+      if (!s.position.isRunning()) {
+        return s;
+      }
+    }
+    Sweep newSweep = new Sweep();
+    sweeps.add(newSweep);
+    synchronized(newLayers) {
+      newLayers.push(newSweep);
+    }
+    return newSweep;
+  }
+  
+  public synchronized boolean noteOn(Note note) {
+    if (note.getChannel() == 0) {
+      LightUp light = getLight();
+      lightMap.put(note.getPitch(), light);
+      light.noteOn(note);
+    } else if (note.getChannel() == 1) {
+    } else if (note.getChannel() == 9) {
+      if (note.getVelocity() > 0) {
+        switch (note.getPitch()) {
+          case 36:
+            Sweep s = getSweep();
+            s.bright = 50 + note.getVelocity() / 127.f * 50;
+            s.falloff = 20 - note.getVelocity() / 127.f * 17;
+            s.position.trigger();
+            break;
+          case 37:
+            sparkleBright = note.getVelocity() / 127.f * 100;
+            sparkleDirection = true;
+            sparkle.trigger();
+            break;
+          case 38:
+            sparkleBright = note.getVelocity() / 127.f * 100;
+            sparkleDirection = false;
+            sparkle.trigger();       
+            break;
+          case 39:
+            effects.boom.trigger();
+            break;
+          case 40:
+            effects.flash.trigger();
+            break;
+        }
+      }
+    }
+    return true;
+  }
+  
+  public synchronized boolean noteOff(Note note) {
+    if (note.getChannel() == 0) {
+      LightUp light = lightMap.get(note.getPitch());
+      if (light != null) {
+        light.noteOff(note);
+      }
+    }
+    return true;
+  }
+  
+  final float[] wval = new float[16];
+  float wavoff = 0;
+  
+  public synchronized void run(double deltaMs) {
+    wavoff += deltaMs * .001f;
+    for (int i = 0; i < wval.length; ++i) {
+      wval[i] = model.cy + 0.2f * model.yMax/2.f * sin(wavoff + i / 1.9f);
+    }
+    float sparklePos = (sparkleDirection ? sparkle.getValuef() : (1 - sparkle.getValuef())) * (Cube.POINTS_PER_STRIP)/2.f;
+    float maxBright = sparkleBright * (1 - sparkle.getValuef());
+    for (Strip s : model.strips) {
+      int i = 0;
+      for (Point p : s.points) {
+        int wavi = (int) constrain(p.x / model.xMax * wval.length, 0, wval.length-1);
+        float wavb = max(0, wave.getValuef()*100.f - 8.f*abs(p.y - wval[wavi]));
+        colors[p.index] = color(
+          (lx.getBaseHuef() + .2f*abs(p.x - model.cx) + .2f*abs(p.y - model.cy)) % 360,
+          100,
+          constrain(wavb + max(0, maxBright - 40.f*abs(sparklePos - abs(i - (Cube.POINTS_PER_STRIP-1)/2.f))), 0, 100)
+        );
+        ++i;
+      }
+    }
+        
+    if (!newLayers.isEmpty()) {
+      synchronized(newLayers) {
+        while (!newLayers.isEmpty()) {
+          addLayer(newLayers.pop());
+        }
+      }
+    }
+  }
+}
+
+class Pulley extends SCPattern {
+  
+  final int NUM_DIVISIONS = 16;
+  private final Accelerator[] gravity = new Accelerator[NUM_DIVISIONS];
+  private final Click[] delays = new Click[NUM_DIVISIONS];
+  
+  private final Click reset = new Click(9000);
+  private boolean isRising = false;
+  
+  private BasicParameter sz = new BasicParameter("SIZE", 0.5f);
+  private BasicParameter beatAmount = new BasicParameter("BEAT", 0);
+  
+  Pulley(GLucose glucose) {
+    super(glucose);
+    for (int i = 0; i < NUM_DIVISIONS; ++i) {
+      addModulator(gravity[i] = new Accelerator(0, 0, 0));
+      addModulator(delays[i] = new Click(0));
+    }
+    addModulator(reset).start();
+    addParameter(sz);
+    addParameter(beatAmount);
+    trigger();
+
+  }
+  
+  private void trigger() {
+    isRising = !isRising;
+    int i = 0;
+    for (Accelerator g : gravity) {
+      if (isRising) {
+        g.setSpeed(random(20, 33), 0).start();
+      } else {
+        g.setVelocity(0).setAcceleration(-420);
+        delays[i].setDuration(random(0, 500)).trigger();
+      }
+      ++i;
+    }
+  }
+  
+  public void run(double deltaMs) {
+    if (reset.click()) {
+      trigger();
+    }
+        
+    if (isRising) {
+      // Fucking A, had to comment this all out because of that bizarre
+      // Processing bug where some simple loop takes an absurd amount of
+      // time, must be some pre-processor bug
+//      for (Accelerator g : gravity) {
+//        if (g.getValuef() > model.yMax) {
+//          g.stop();
+//        } else if (g.getValuef() > model.yMax*.55) {
+//          if (g.getVelocityf() > 10) {
+//            g.setAcceleration(-16);
+//          } else {
+//            g.setAcceleration(0);
+//          }
+//        }
+//      }
+    } else {
+      int j = 0;
+      for (Click d : delays) {
+        if (d.click()) {
+          gravity[j].start();
+          d.stop();
+        }
+        ++j;
+      }
+      for (Accelerator g : gravity) {
+        if (g.getValuef() < 0) {
+          g.setValue(-g.getValuef());
+          g.setVelocity(-g.getVelocityf() * random(0.74f, 0.84f));
+        }
+      }
+    }
+
+    // A little silliness to test the grid API    
+    if (midiEngine != null && midiEngine.getFocusedPattern() == this) {
+           for (int i = 0; i < 5; ++i) {
+        for (int j = 0; j < 8; ++j) {
+          int gi = (int) constrain(j * NUM_DIVISIONS / 8, 0, NUM_DIVISIONS-1);
+          float b = 1 - 4.f*abs((6-i)/6.f - gravity[gi].getValuef() / model.yMax);
+          midiEngine.grid.setState(i, j, (b < 0) ? 0 : 3);
+        }
+      }
+    }
+    
+    float fPos = 1 - lx.tempo.rampf();
+    if (fPos < .2f) {
+      fPos = .2f + 4 * (.2f - fPos);
+    }
+    float falloff = 100.f / (3 + sz.getValuef() * 36 + fPos * beatAmount.getValuef()*48);
+    for (Point p : model.points) {
+      int gi = (int) constrain((p.x - model.xMin) * NUM_DIVISIONS / (model.xMax - model.xMin), 0, NUM_DIVISIONS-1);
+      colors[p.index] = lx.hsb(
+        (lx.getBaseHuef() + abs(p.x - model.cx)*.8f + p.y*.4f) % 360,
+        constrain(130 - p.y*.8f, 0, 100),
+        max(0, 100 - abs(p.y - gravity[gi].getValuef())*falloff)
+      );
+    }
+  }
+}
+
+class ViolinWave extends SCPattern {
+  
+  BasicParameter level = new BasicParameter("LVL", 0.45f);
+  BasicParameter range = new BasicParameter("RNG", 0.5f);
+  BasicParameter edge = new BasicParameter("EDG", 0.5f);
+  BasicParameter release = new BasicParameter("RLS", 0.5f);
+  BasicParameter speed = new BasicParameter("SPD", 0.5f);
+  BasicParameter amp = new BasicParameter("AMP", 0.25f);
+  BasicParameter period = new BasicParameter("WAVE", 0.5f);
+  BasicParameter pSize = new BasicParameter("PSIZE", 0.5f);
+  BasicParameter pSpeed = new BasicParameter("PSPD", 0.5f);
+  BasicParameter pDensity = new BasicParameter("PDENS", 0.25f);
+  
+  LinearEnvelope dbValue = new LinearEnvelope(0, 0, 10);
+
+  ViolinWave(GLucose glucose) {
+    super(glucose);
+    addParameter(level);
+    addParameter(edge);
+    addParameter(range);
+    addParameter(release);
+    addParameter(speed);
+    addParameter(amp);
+    addParameter(period);
+    addParameter(pSize);
+    addParameter(pSpeed);
+    addParameter(pDensity);
+
+    addModulator(dbValue);
+  }
+  
+  final List<Particle> particles = new ArrayList<Particle>();
+  
+  class Particle {
+    
+    LinearEnvelope x = new LinearEnvelope(0, 0, 0);
+    LinearEnvelope y = new LinearEnvelope(0, 0, 0);
+    
+    Particle() {
+      addModulator(x);
+      addModulator(y);
+    }
+    
+    public Particle trigger(boolean direction) {
+      float xInit = random(model.xMin, model.xMax);
+      float time = 3000 - 2500*pSpeed.getValuef();
+      x.setRange(xInit, xInit + random(-40, 40), time).trigger();
+      y.setRange(model.cy + 10, direction ? model.yMax + 50 : model.yMin - 50, time).trigger();
+      return this;
+    }
+    
+    public boolean isActive() {
+      return x.isRunning() || y.isRunning();
+    }
+    
+    public void run(double deltaMs) {
+      if (!isActive()) {
+        return;
+      }
+      
+      float pFalloff = (30 - 27*pSize.getValuef());
+      for (Point p : model.points) {
+        float b = 100 - pFalloff * (abs(p.x - x.getValuef()) + abs(p.y - y.getValuef()));
+        if (b > 0) {
+          colors[p.index] = blendColor(colors[p.index], lx.hsb(
+            lx.getBaseHuef(), 20, b
+          ), ADD);
+        }
+      }
+    }
+  }
+  
+  float[] centers = new float[30];
+  double accum = 0;
+  boolean rising = true;
+  
+  public void fireParticle(boolean direction) {
+    boolean gotOne = false;
+    for (Particle p : particles) {
+      if (!p.isActive()) {
+       p.trigger(direction);
+       return;
+      }
+    }
+    particles.add(new Particle().trigger(direction));
+  }
+  
+  public void run(double deltaMs) {
+    accum += deltaMs / (1000.f - 900.f*speed.getValuef());
+    for (int i = 0; i < centers.length; ++i) {
+      centers[i] = model.cy + 30*amp.getValuef()*sin((float) (accum + (i-centers.length/2.f)/(1.f + 9.f*period.getValuef())));
+    }
+    
+    float zeroDBReference = pow(10, (50 - 190*level.getValuef())/20.f);
+    float dB = 20*GraphicEQ.log10(lx.audioInput().mix.level() / zeroDBReference);
+    if (dB > dbValue.getValuef()) {
+      rising = true;
+      dbValue.setRangeFromHereTo(dB, 10).trigger();
+    } else {
+      if (rising) {
+        for (int j = 0; j < pDensity.getValuef()*3; ++j) {
+          fireParticle(true);
+          fireParticle(false);
+        }
+      }
+      rising = false;
+      dbValue.setRangeFromHereTo(max(dB, -96), 50 + 1000*release.getValuef()).trigger();
+    }
+    float edg = 1 + edge.getValuef() * 40;
+    float rng = (78 - 64 * range.getValuef()) / (model.yMax - model.cy);
+    float val = max(2, dbValue.getValuef());
+    
+    for (Point p : model.points) {
+      int ci = (int) lerp(0, centers.length-1, (p.x - model.xMin) / (model.xMax - model.xMin));
+      float rFactor = 1.0f -  0.9f * abs(p.x - model.cx) / (model.xMax - model.cx);
+      colors[p.index] = lx.hsb(
+        (lx.getBaseHuef() + abs(p.x - model.cx)) % 360,
+        min(100, 20 + 8*abs(p.y - centers[ci])),
+        constrain(edg*(val*rFactor - rng * abs(p.y-centers[ci])), 0, 100)
+      );
+    }
+    
+    for (Particle p : particles) {
+      p.run(deltaMs);
+    }
+  }
+}
+
+class BouncyBalls extends SCPattern {
+  
+  static final int NUM_BALLS = 6;
+  
+  class BouncyBall {
+       
+    Accelerator yPos;
+    TriangleLFO xPos = new TriangleLFO(0, model.xMax, random(8000, 19000));
+    float zPos;
+    
+    BouncyBall(int i) {
+      addModulator(xPos.setBasis(random(0, TWO_PI)).start());
+      addModulator(yPos = new Accelerator(0, 0, 0));
+      zPos = lerp(model.zMin, model.zMax, (i+2.f) / (NUM_BALLS + 4.f));
+    }
+    
+    public void bounce(float midiVel) {
+      float v = 100 + 8*midiVel;
+      yPos.setSpeed(v, getAccel(v, 60 / lx.tempo.bpmf())).start();
+    }
+    
+    public float getAccel(float v, float oneBeat) {
+      return -2*v / oneBeat;
+    }
+    
+    public void run(double deltaMs) {
+      float flrLevel = flr.getValuef() * model.xMax/2.f;
+      if (yPos.getValuef() < flrLevel) {
+        if (yPos.getVelocity() < -50) {
+          yPos.setValue(2*flrLevel-yPos.getValuef());
+          float v = -yPos.getVelocityf() * bounce.getValuef();
+          yPos.setSpeed(v, getAccel(v, 60 / lx.tempo.bpmf()));
+        } else {
+          yPos.setValue(flrLevel).stop();
+        }
+      }
+      float falloff = 130.f / (12 + blobSize.getValuef() * 36);
+      float xv = xPos.getValuef();
+      float yv = yPos.getValuef();
+      
+      for (Point p : model.points) {
+        float d = sqrt((p.x-xv)*(p.x-xv) + (p.y-yv)*(p.y-yv) + .1f*(p.z-zPos)*(p.z-zPos));
+        float b = constrain(130 - falloff*d, 0, 100);
+        if (b > 0) {
+          colors[p.index] = blendColor(colors[p.index], lx.hsb(
+            (lx.getBaseHuef() + p.y*.5f + abs(model.cx - p.x) * .5f) % 360,
+            max(0, 100 - .45f*(p.y - flrLevel)),
+            b
+          ), ADD);
+        }
+      }
+    }
+  }
+  
+  final BouncyBall[] balls = new BouncyBall[NUM_BALLS];
+  
+  final BasicParameter bounce = new BasicParameter("BNC", .8f);
+  final BasicParameter flr = new BasicParameter("FLR", 0);
+  final BasicParameter blobSize = new BasicParameter("SIZE", 0.5f);
+  
+  BouncyBalls(GLucose glucose) {
+    super(glucose);
+    for (int i = 0; i < balls.length; ++i) {
+      balls[i] = new BouncyBall(i);
+    }
+    addParameter(bounce);
+    addParameter(flr);
+    addParameter(blobSize);
+  }
+  
+  public void run(double deltaMs) {
+    setColors(0xff000000);
+    for (BouncyBall b : balls) {
+      b.run(deltaMs);
+    }
+  }
+  
+  public boolean noteOn(Note note) {
+    int pitch = (note.getPitch() + note.getChannel()) % NUM_BALLS;
+    balls[pitch].bounce(note.getVelocity());
+    return true;
+  }
+}
+
+class SpaceTime extends SCPattern {
+
+  SinLFO pos = new SinLFO(0, 1, 3000);
+  SinLFO rate = new SinLFO(1000, 9000, 13000);
+  SinLFO falloff = new SinLFO(10, 70, 5000);
+  float angle = 0;
+
+  BasicParameter rateParameter = new BasicParameter("RATE", 0.5f);
+  BasicParameter sizeParameter = new BasicParameter("SIZE", 0.5f);
+
+
+  public SpaceTime(GLucose glucose) {
+    super(glucose);
+    
+    addModulator(pos).trigger();
+    addModulator(rate).trigger();
+    addModulator(falloff).trigger();    
+    pos.modulateDurationBy(rate);
+    addParameter(rateParameter);
+    addParameter(sizeParameter);
+  }
+
+  public void onParameterChanged(LXParameter parameter) {
+    if (parameter == rateParameter) {
+      rate.stop().setValue(9000 - 8000*parameter.getValuef());
+    }  else if (parameter == sizeParameter) {
+      falloff.stop().setValue(70 - 60*parameter.getValuef());
+    }
+  }
+
+  public void run(double deltaMs) {    
+    angle += deltaMs * 0.0007f;
+    float sVal1 = model.strips.size() * (0.5f + 0.5f*sin(angle));
+    float sVal2 = model.strips.size() * (0.5f + 0.5f*cos(angle));
+
+    float pVal = pos.getValuef();
+    float fVal = falloff.getValuef();
+
+    int s = 0;
+    for (Strip strip : model.strips) {
+      int i = 0;
+      for (Point p : strip.points) {
+        colors[p.index] = lx.hsb(
+          (lx.getBaseHuef() + 360 - p.x*.2f + p.y * .3f) % 360, 
+          constrain(.4f * min(abs(s - sVal1), abs(s - sVal2)), 20, 100),
+          max(0, 100 - fVal*abs(i - pVal*(strip.metrics.numPoints - 1)))
+        );
+        ++i;
+      }
+      ++s;
+    }
+  }
+}
+
+class Swarm extends SCPattern {
+  
+  SawLFO offset = new SawLFO(0, 1, 1000);
+  SinLFO rate = new SinLFO(350, 1200, 63000);
+  SinLFO falloff = new SinLFO(15, 50, 17000);
+  SinLFO fX = new SinLFO(0, model.xMax, 19000);
+  SinLFO fY = new SinLFO(0, model.yMax, 11000);
+  SinLFO hOffX = new SinLFO(0, model.xMax, 13000);
+
+  public Swarm(GLucose glucose) {
+    super(glucose);
+    
+    addModulator(offset).trigger();
+    addModulator(rate).trigger();
+    addModulator(falloff).trigger();
+    addModulator(fX).trigger();
+    addModulator(fY).trigger();
+    addModulator(hOffX).trigger();
+    offset.modulateDurationBy(rate);
+  }
+
+  public float modDist(float v1, float v2, float mod) {
+    v1 = v1 % mod;
+    v2 = v2 % mod;
+    if (v2 > v1) {
+      return min(v2-v1, v1+mod-v2);
+    } 
+    else {
+      return min(v1-v2, v2+mod-v1);
+    }
+  }
+
+  public void run(double deltaMs) {
+    float s = 0;
+    for (Strip strip : model.strips  ) {
+      int i = 0;
+      for (Point p : strip.points) {
+        float fV = max(-1, 1 - dist(p.x/2.f, p.y, fX.getValuef()/2.f, fY.getValuef()) / 64.f);
+        colors[p.index] = lx.hsb(
+        (lx.getBaseHuef() + 0.3f * abs(p.x - hOffX.getValuef())) % 360, 
+        constrain(80 + 40 * fV, 0, 100), 
+        constrain(100 - (30 - fV * falloff.getValuef()) * modDist(i + (s*63)%61, offset.getValuef() * strip.metrics.numPoints, strip.metrics.numPoints), 0, 100)
+          );
+        ++i;
+      }
+      ++s;
+    }
+  }
+}
+
+class SwipeTransition extends SCTransition {
+  
+  final BasicParameter bleed = new BasicParameter("WIDTH", 0.5f);
+  
+  SwipeTransition(GLucose glucose) {
+    super(glucose);
+    setDuration(5000);
+    addParameter(bleed);
+  }
+
+  public void computeBlend(int[] c1, int[] c2, double progress) {
+    float bleedf = 10 + bleed.getValuef() * 200.f;
+    float xPos = (float) (-bleedf + progress * (model.xMax + bleedf));
+    for (Point p : model.points) {
+      float d = (p.x - xPos) / bleedf;
+      if (d < 0) {
+        colors[p.index] = c2[p.index];
+      } else if (d > 1) {
+        colors[p.index] = c1[p.index];
+      } else {
+        colors[p.index] = lerpColor(c2[p.index], c1[p.index], d, RGB);
+      }
+    }
+  }
+}
+
+abstract class BlendTransition extends SCTransition {
+  
+  final int blendType;
+  
+  BlendTransition(GLucose glucose, int blendType) {
+    super(glucose);
+    this.blendType = blendType;
+  }
+
+  public void computeBlend(int[] c1, int[] c2, double progress) {
+    if (progress < 0.5f) {
+      for (int i = 0; i < c1.length; ++i) {
+        colors[i] = lerpColor(
+          c1[i],
+          blendColor(c1[i], c2[i], blendType),
+          (float) (2.f*progress),
+          RGB);
+      }
+    } else {
+      for (int i = 0; i < c1.length; ++i) {
+        colors[i] = lerpColor(
+          c2[i],
+          blendColor(c1[i], c2[i], blendType),
+          (float) (2.f*(1.f - progress)),
+          RGB);
+      }
+    }
+  }
+}
+
+class MultiplyTransition extends BlendTransition {
+  MultiplyTransition(GLucose glucose) {
+    super(glucose, MULTIPLY);
+  }
+}
+
+class ScreenTransition extends BlendTransition {
+  ScreenTransition(GLucose glucose) {
+    super(glucose, SCREEN);
+  }
+}
+
+class BurnTransition extends BlendTransition {
+  BurnTransition(GLucose glucose) {
+    super(glucose, BURN);
+  }
+}
+
+class DodgeTransition extends BlendTransition {
+  DodgeTransition(GLucose glucose) {
+    super(glucose, DODGE);
+  }
+}
+
+class OverlayTransition extends BlendTransition {
+  OverlayTransition(GLucose glucose) {
+    super(glucose, OVERLAY);
+  }
+}
+
+class AddTransition extends BlendTransition {
+  AddTransition(GLucose glucose) {
+    super(glucose, ADD);
+  }
+}
+
+class SubtractTransition extends BlendTransition {
+  SubtractTransition(GLucose glucose) {
+    super(glucose, SUBTRACT);
+  }
+}
+
+class SoftLightTransition extends BlendTransition {
+  SoftLightTransition(GLucose glucose) {
+    super(glucose, SOFT_LIGHT);
+  }
+}
+
+class BassPod extends SCPattern {
+
+  private GraphicEQ eq = null;
+  
+  private final BasicParameter clr = new BasicParameter("CLR", 0.5f);
+  
+  public BassPod(GLucose glucose) {
+    super(glucose);
+    addParameter(clr);
+  }
+  
+  protected void onActive() {
+    if (eq == null) {
+      eq = new GraphicEQ(lx, 16);
+      eq.range.setValue(0.4f);
+      eq.level.setValue(0.4f);
+      eq.slope.setValue(0.6f);
+      addParameter(eq.level);
+      addParameter(eq.range);
+      addParameter(eq.attack);
+      addParameter(eq.release);
+      addParameter(eq.slope);
+    }
+  }
+
+  public void run(double deltaMs) {
+    eq.run(deltaMs);
+    
+    float bassLevel = eq.getAverageLevel(0, 5);
+    
+    float satBase = bassLevel*480*clr.getValuef();
+    
+    for (Point p : model.points) {
+      int avgIndex = (int) constrain(1 + abs(p.x-model.cx)/(model.cx)*(eq.numBands-5), 0, eq.numBands-5);
+      float value = 0;
+      for (int i = avgIndex; i < avgIndex + 5; ++i) {
+        value += eq.getLevel(i);
+      }
+      value /= 5.f;
+
+      float b = constrain(8 * (value*model.yMax - abs(p.y-model.yMax/2.f)), 0, 100);
+      colors[p.index] = lx.hsb(
+        (lx.getBaseHuef() + abs(p.y - model.cy) + abs(p.x - model.cx)) % 360,
+        constrain(satBase - .6f*dist(p.x, p.y, model.cx, model.cy), 0, 100),
+        b
+      );
+    }
+  }
+}
+
+
+class CubeEQ extends SCPattern {
+
+  private GraphicEQ eq = null;
+
+  private final BasicParameter edge = new BasicParameter("EDGE", 0.5f);
+  private final BasicParameter clr = new BasicParameter("CLR", 0.5f);
+  private final BasicParameter blockiness = new BasicParameter("BLK", 0.5f);
+
+  public CubeEQ(GLucose glucose) {
+    super(glucose);
+  }
+
+  protected void onActive() {
+    if (eq == null) {
+      eq = new GraphicEQ(lx, 16);
+      addParameter(eq.level);
+      addParameter(eq.range);
+      addParameter(eq.attack);
+      addParameter(eq.release);
+      addParameter(eq.slope);
+      addParameter(edge);
+      addParameter(clr);
+      addParameter(blockiness);
+    }
+  }
+
+  public void run(double deltaMs) {
+    eq.run(deltaMs);
+
+    float edgeConst = 2 + 30*edge.getValuef();
+    float clrConst = 1.1f + clr.getValuef();
+
+    for (Point p : model.points) {
+      float avgIndex = constrain(2 + p.x / model.xMax * (eq.numBands-4), 0, eq.numBands-4);
+      int avgFloor = (int) avgIndex;
+
+      float leftVal = eq.getLevel(avgFloor);
+      float rightVal = eq.getLevel(avgFloor+1);
+      float smoothValue = lerp(leftVal, rightVal, avgIndex-avgFloor);
+      
+      float chunkyValue = (
+        eq.getLevel(avgFloor/4*4) +
+        eq.getLevel(avgFloor/4*4 + 1) +
+        eq.getLevel(avgFloor/4*4 + 2) +
+        eq.getLevel(avgFloor/4*4 + 3)
+      ) / 4.f; 
+      
+      float value = lerp(smoothValue, chunkyValue, blockiness.getValuef());
+
+      float b = constrain(edgeConst * (value*model.yMax - p.y), 0, 100);
+      colors[p.index] = lx.hsb(
+        (480 + lx.getBaseHuef() - min(clrConst*p.y, 120)) % 360, 
+        100, 
+        b
+      );
+    }
+  }
+}
+
+class BoomEffect extends SCEffect {
+
+  final BasicParameter falloff = new BasicParameter("WIDTH", 0.5f);
+  final BasicParameter speed = new BasicParameter("SPD", 0.5f);
+  final BasicParameter bright = new BasicParameter("BRT", 1.0f);
+  final BasicParameter sat = new BasicParameter("SAT", 0.2f);
+  List<Layer> layers = new ArrayList<Layer>();
+  final float maxr = sqrt(model.xMax*model.xMax + model.yMax*model.yMax + model.zMax*model.zMax) + 10;
+
+  class Layer {
+    LinearEnvelope boom = new LinearEnvelope(-40, 500, 1300);
+
+    Layer() {
+      addModulator(boom);
+      trigger();
+    }
+
+    public void trigger() {
+      float falloffv = falloffv();
+      boom.setRange(-100 / falloffv, maxr + 100/falloffv, 4000 - speed.getValuef() * 3300);
+      boom.trigger();
+    }
+
+    public void apply(int[] colors) {
+      float brightv = 100 * bright.getValuef();
+      float falloffv = falloffv();
+      float satv = sat.getValuef() * 100;
+      float huev = lx.getBaseHuef();
+      for (Point p : model.points) {
+        colors[p.index] = blendColor(
+        colors[p.index], 
+        lx.hsb(huev, satv, constrain(brightv - falloffv*abs(boom.getValuef() - dist(p.x, 2*p.y, 3*p.z, model.xMax/2, model.yMax, model.zMax*1.5f)), 0, 100)), 
+        ADD);
+      }
+    }
+  }
+
+  BoomEffect(GLucose glucose) {
+    super(glucose, true);
+    addParameter(falloff);
+    addParameter(speed);
+    addParameter(bright);
+    addParameter(sat);
+  }
+
+  public void onEnable() {
+    for (Layer l : layers) {
+      if (!l.boom.isRunning()) {
+        l.trigger();
+        return;
+      }
+    }
+    layers.add(new Layer());
+  }
+
+  private float falloffv() {
+    return 20 - 19 * falloff.getValuef();
+  }
+
+  public void onTrigger() {
+    onEnable();
+  }
+
+  public void apply(int[] colors) {
+    for (Layer l : layers) {
+      if (l.boom.isRunning()) {
+        l.apply(colors);
+      }
+    }
+  }
+}
+
+public class PianoKeyPattern extends SCPattern {
+  
+  final LinearEnvelope[] cubeBrt;
+  final SinLFO base[];  
+  final BasicParameter attack = new BasicParameter("ATK", 0.1f);
+  final BasicParameter release = new BasicParameter("REL", 0.5f);
+  final BasicParameter level = new BasicParameter("AMB", 0.6f);
+  
+  PianoKeyPattern(GLucose glucose) {
+    super(glucose);
+        
+    addParameter(attack);
+    addParameter(release);
+    addParameter(level);
+    cubeBrt = new LinearEnvelope[model.cubes.size() / 4];
+    for (int i = 0; i < cubeBrt.length; ++i) {
+      addModulator(cubeBrt[i] = new LinearEnvelope(0, 0, 100));
+    }
+    base = new SinLFO[model.cubes.size() / 12];
+    for (int i = 0; i < base.length; ++i) {
+      addModulator(base[i] = new SinLFO(0, 1, 7000 + 1000*i)).trigger();
+    }
+  }
+  
+  private float getAttackTime() {
+    return 15 + attack.getValuef()*attack.getValuef() * 2000;
+  }
+  
+  private float getReleaseTime() {
+    return 15 + release.getValuef() * 3000;
+  }
+  
+  private LinearEnvelope getEnvelope(int index) {
+    return cubeBrt[index % cubeBrt.length];
+  }
+  
+  private SinLFO getBase(int index) {
+    return base[index % base.length];
+  }
+    
+  public boolean noteOn(Note note) {
+    LinearEnvelope env = getEnvelope(note.getPitch());
+    env.setEndVal(min(1, env.getValuef() + (note.getVelocity() / 127.f)), getAttackTime()).start();
+    return true;
+  }
+  
+  public boolean noteOff(Note note) {
+    getEnvelope(note.getPitch()).setEndVal(0, getReleaseTime()).start();
+    return true;
+  }
+  
+  public void run(double deltaMs) {
+    int i = 0;
+    float huef = lx.getBaseHuef();
+    float levelf = level.getValuef();
+    for (Cube c : model.cubes) {
+      float v = max(getBase(i).getValuef() * levelf/4.f, getEnvelope(i++).getValuef());
+      setColor(c, lx.hsb(
+        (huef + 20*v + abs(c.cx-model.xMax/2.f)*.3f + c.cy) % 360,
+        min(100, 120*v),
+        100*v
+      ));
+    }
+  }
+}
+
+class CrossSections extends SCPattern {
+  
+  final SinLFO x = new SinLFO(0, model.xMax, 5000);
+  final SinLFO y = new SinLFO(0, model.yMax, 6000);
+  final SinLFO z = new SinLFO(0, model.zMax, 7000);
+  
+  final BasicParameter xw = new BasicParameter("XWID", 0.3f);
+  final BasicParameter yw = new BasicParameter("YWID", 0.3f);
+  final BasicParameter zw = new BasicParameter("ZWID", 0.3f);  
+  final BasicParameter xr = new BasicParameter("XRAT", 0.7f);
+  final BasicParameter yr = new BasicParameter("YRAT", 0.6f);
+  final BasicParameter zr = new BasicParameter("ZRAT", 0.5f);
+  final BasicParameter xl = new BasicParameter("XLEV", 1);
+  final BasicParameter yl = new BasicParameter("YLEV", 1);
+  final BasicParameter zl = new BasicParameter("ZLEV", 0.5f);
+
+  
+  CrossSections(GLucose glucose) {
+    super(glucose);
+    addModulator(x).trigger();
+    addModulator(y).trigger();
+    addModulator(z).trigger();
+    addParams();
+  }
+  
+  protected void addParams() {
+    addParameter(xr);
+    addParameter(yr);
+    addParameter(zr);    
+    addParameter(xw);
+    addParameter(xl);
+    addParameter(yl);
+    addParameter(zl);
+    addParameter(yw);    
+    addParameter(zw);
+  }
+  
+  public void onParameterChanged(LXParameter p) {
+    if (p == xr) {
+      x.setDuration(10000 - 8800*p.getValuef());
+    } else if (p == yr) {
+      y.setDuration(10000 - 9000*p.getValuef());
+    } else if (p == zr) {
+      z.setDuration(10000 - 9000*p.getValuef());
+    }
+  }
+  
+  float xv, yv, zv;
+  
+  protected void updateXYZVals() {
+    xv = x.getValuef();
+    yv = y.getValuef();
+    zv = z.getValuef();    
+  }
+
+  public void run(double deltaMs) {
+    updateXYZVals();
+    
+    float xlv = 100*xl.getValuef();
+    float ylv = 100*yl.getValuef();
+    float zlv = 100*zl.getValuef();
+    
+    float xwv = 100.f / (10 + 40*xw.getValuef());
+    float ywv = 100.f / (10 + 40*yw.getValuef());
+    float zwv = 100.f / (10 + 40*zw.getValuef());
+    
+    for (Point p : model.points) {
+      int c = 0;
+      c = blendColor(c, lx.hsb(
+      (lx.getBaseHuef() + p.x/10 + p.y/3) % 360, 
+      constrain(140 - 1.1f*abs(p.x - model.xMax/2.f), 0, 100), 
+      max(0, xlv - xwv*abs(p.x - xv))
+        ), ADD);
+      c = blendColor(c, lx.hsb(
+      (lx.getBaseHuef() + 80 + p.y/10) % 360, 
+      constrain(140 - 2.2f*abs(p.y - model.yMax/2.f), 0, 100), 
+      max(0, ylv - ywv*abs(p.y - yv))
+        ), ADD); 
+      c = blendColor(c, lx.hsb(
+      (lx.getBaseHuef() + 160 + p.z / 10 + p.y/2) % 360, 
+      constrain(140 - 2.2f*abs(p.z - model.zMax/2.f), 0, 100), 
+      max(0, zlv - zwv*abs(p.z - zv))
+        ), ADD); 
+      colors[p.index] = c;
+    }
+  }
+}
+
+class Blinders extends SCPattern {
+    
+  final SinLFO[] m;
+  final TriangleLFO r;
+  final SinLFO s;
+  final TriangleLFO hs;
+
+  public Blinders(GLucose glucose) {
+    super(glucose);
+    m = new SinLFO[12];
+    for (int i = 0; i < m.length; ++i) {  
+      addModulator(m[i] = new SinLFO(0.5f, 120, (120000.f / (3+i)))).trigger();
+    }
+    addModulator(r = new TriangleLFO(9000, 15000, 29000)).trigger();
+    addModulator(s = new SinLFO(-20, 275, 11000)).trigger();
+    addModulator(hs = new TriangleLFO(0.1f, 0.5f, 15000)).trigger();
+    s.modulateDurationBy(r);
+  }
+
+  public void run(double deltaMs) {
+    float hv = lx.getBaseHuef();
+    int si = 0;
+    for (Strip strip : model.strips) {
+      int i = 0;
+      float mv = m[si % m.length].getValuef();
+      for (Point p : strip.points) {
+        colors[p.index] = lx.hsb(
+          (hv + p.z + p.y*hs.getValuef()) % 360, 
+          min(100, abs(p.x - s.getValuef())/2.f), 
+          max(0, 100 - mv/2.f - mv * abs(i - (strip.metrics.length-1)/2.f))
+        );
+        ++i;
+      }
+      ++si;
+    }
+  }
+}
+
+class Psychedelia extends SCPattern {
+  
+  final int NUM = 3;
+  SinLFO m = new SinLFO(-0.5f, NUM-0.5f, 9000);
+  SinLFO s = new SinLFO(-20, 147, 11000);
+  TriangleLFO h = new TriangleLFO(0, 240, 19000);
+  SinLFO c = new SinLFO(-.2f, .8f, 31000);
+
+  Psychedelia(GLucose glucose) {
+    super(glucose);
+    addModulator(m).trigger();
+    addModulator(s).trigger();
+    addModulator(h).trigger();
+    addModulator(c).trigger();
+  }
+
+  public void run(double deltaMs) {
+    float huev = h.getValuef();
+    float cv = c.getValuef();
+    float sv = s.getValuef();
+    float mv = m.getValuef();
+    int i = 0;
+    for (Strip strip : model.strips) {
+      for (Point p : strip.points) {
+        colors[p.index] = lx.hsb(
+          (huev + i*constrain(cv, 0, 2) + p.z/2.f + p.x/4.f) % 360, 
+          min(100, abs(p.y-sv)), 
+          max(0, 100 - 50*abs((i%NUM) - mv))
+        );
+      }
+      ++i;
+    }
+  }
+}
+
+class AskewPlanes extends SCPattern {
+  
+  class Plane {
+    private final SinLFO a;
+    private final SinLFO b;
+    private final SinLFO c;
+    float av = 1;
+    float bv = 1;
+    float cv = 1;
+    float denom = 0.1f;
+    
+    Plane(int i) {
+      addModulator(a = new SinLFO(-1, 1, 4000 + 1029*i)).trigger();
+      addModulator(b = new SinLFO(-1, 1, 11000 - 1104*i)).trigger();
+      addModulator(c = new SinLFO(-50, 50, 4000 + 1000*i * ((i % 2 == 0) ? 1 : -1))).trigger();      
+    }
+    
+    public void run(double deltaMs) {
+      av = a.getValuef();
+      bv = b.getValuef();
+      cv = c.getValuef();
+      denom = sqrt(av*av + bv*bv);
+    }
+  }
+    
+  final Plane[] planes;
+  final int NUM_PLANES = 3;
+  
+  AskewPlanes(GLucose glucose) {
+    super(glucose);
+    planes = new Plane[NUM_PLANES];
+    for (int i = 0; i < planes.length; ++i) {
+      planes[i] = new Plane(i);
+    }
+  }
+  
+  public void run(double deltaMs) {
+    float huev = lx.getBaseHuef();
+    
+    // This is super fucking bizarre. But if this is a for loop, the framerate
+    // tanks to like 30FPS, instead of 60. Call them manually and it works fine.
+    // Doesn't make ANY sense... there must be some weird side effect going on
+    // with the Processing internals perhaps?
+//    for (Plane plane : planes) {
+//      plane.run(deltaMs);
+//    }
+    planes[0].run(deltaMs);
+    planes[1].run(deltaMs);
+    planes[2].run(deltaMs);    
+    
+    for (Point p : model.points) {
+      float d = MAX_FLOAT;
+      for (Plane plane : planes) {
+        if (plane.denom != 0) {
+          d = min(d, abs(plane.av*(p.x-model.cx) + plane.bv*(p.y-model.cy) + plane.cv) / plane.denom);
+        }
+      }
+      colors[p.index] = lx.hsb(
+        (huev + abs(p.x-model.cx)*.3f + p.y*.8f) % 360,
+        max(0, 100 - .8f*abs(p.x - model.cx)),
+        constrain(140 - 10.f*d, 0, 100)
+      );
+    }
+  }
+}
+
+class ShiftingPlane extends SCPattern {
+
+  final SinLFO a = new SinLFO(-.2f, .2f, 5300);
+  final SinLFO b = new SinLFO(1, -1, 13300);
+  final SinLFO c = new SinLFO(-1.4f, 1.4f, 5700);
+  final SinLFO d = new SinLFO(-10, 10, 9500);
+
+  ShiftingPlane(GLucose glucose) {
+    super(glucose);
+    addModulator(a).trigger();
+    addModulator(b).trigger();
+    addModulator(c).trigger();
+    addModulator(d).trigger();    
+  }
+  
+  public void run(double deltaMs) {
+    float hv = lx.getBaseHuef();
+    float av = a.getValuef();
+    float bv = b.getValuef();
+    float cv = c.getValuef();
+    float dv = d.getValuef();    
+    float denom = sqrt(av*av + bv*bv + cv*cv);
+    for (Point p : model.points) {
+      float d = abs(av*(p.x-model.cx) + bv*(p.y-model.cy) + cv*(p.z-model.cz) + dv) / denom;
+      colors[p.index] = lx.hsb(
+        (hv + abs(p.x-model.cx)*.6f + abs(p.y-model.cy)*.9f + abs(p.z - model.cz)) % 360,
+        constrain(110 - d*6, 0, 100),
+        constrain(130 - 7*d, 0, 100)
+      );
+    }
+  }
+}
+
+class Traktor extends SCPattern {
+
+  final int FRAME_WIDTH = 60;
+  
+  final BasicParameter speed = new BasicParameter("SPD", 0.5f);
+  
+  private float[] bass = new float[FRAME_WIDTH];
+  private float[] treble = new float[FRAME_WIDTH];
+    
+  private int index = 0;
+  private GraphicEQ eq = null;
+
+  public Traktor(GLucose glucose) {
+    super(glucose);
+    for (int i = 0; i < FRAME_WIDTH; ++i) {
+      bass[i] = 0;
+      treble[i] = 0;
+    }
+    addParameter(speed);
+  }
+
+  public void onActive() {
+    if (eq == null) {
+      eq = new GraphicEQ(lx, 16);
+      eq.slope.setValue(0.6f);
+      eq.level.setValue(0.65f);
+      eq.range.setValue(0.35f);
+      eq.release.setValue(0.4f);
+      addParameter(eq.level);
+      addParameter(eq.range);
+      addParameter(eq.attack);
+      addParameter(eq.release);
+      addParameter(eq.slope);
+    }
+  }
+
+  int counter = 0;
+  
+  public void run(double deltaMs) {
+    eq.run(deltaMs);
+    
+    int stepThresh = (int) (40 - 39*speed.getValuef());
+    counter += deltaMs;
+    if (counter < stepThresh) {
+      return;
+    }
+    counter = counter % stepThresh;
+
+    index = (index + 1) % FRAME_WIDTH;
+    
+    float rawBass = eq.getAverageLevel(0, 4);
+    float rawTreble = eq.getAverageLevel(eq.numBands-7, 7);
+    
+    bass[index] = rawBass * rawBass * rawBass * rawBass;
+    treble[index] = rawTreble * rawTreble;
+
+    for (Point p : model.points) {
+      int i = (int) constrain((model.xMax - p.x) / model.xMax * FRAME_WIDTH, 0, FRAME_WIDTH-1);
+      int pos = (index + FRAME_WIDTH - i) % FRAME_WIDTH;
+      
+      colors[p.index] = lx.hsb(
+        (360 + lx.getBaseHuef() + .8f*abs(p.x-model.cx)) % 360,
+        100,
+        constrain(9 * (bass[pos]*model.cy - abs(p.y - model.cy + 5)), 0, 100)
+      );
+      colors[p.index] = blendColor(colors[p.index], lx.hsb(
+        (400 + lx.getBaseHuef() + .5f*abs(p.x-model.cx)) % 360,
+        60,
+        constrain(5 * (treble[pos]*.6f*model.cy - abs(p.y - model.cy)), 0, 100)
+
+      ), ADD);
+    }
+  }
+}
+
+class ColorFuckerEffect extends SCEffect {
+  
+  final BasicParameter level = new BasicParameter("BRT", 1);
+  final BasicParameter desat = new BasicParameter("DSAT", 0);
+  final BasicParameter hueShift = new BasicParameter("HSHFT", 0);
+  final BasicParameter sharp = new BasicParameter("SHARP", 0);
+  final BasicParameter soft = new BasicParameter("SOFT", 0);
+  final BasicParameter mono = new BasicParameter("MONO", 0);
+  final BasicParameter invert = new BasicParameter("INVERT", 0);
+
+  
+  float[] hsb = new float[3];
+  
+  ColorFuckerEffect(GLucose glucose) {
+    super(glucose);
+    addParameter(level);
+    addParameter(desat);
+    addParameter(sharp);
+    addParameter(hueShift);
+    addParameter(soft);
+    addParameter(mono);
+    addParameter(invert);
+  }
+  
+  public void apply(int[] colors) {
+    if (!enabled) {
+      return;
+    }
+    float bMod = level.getValuef();
+    float sMod = 1 - desat.getValuef();
+    float hMod = hueShift.getValuef();
+    float fSharp = 1/(1.0001f-sharp.getValuef());
+    float fSoft = soft.getValuef();
+    boolean mon = mono.getValuef() > 0.5f;
+    boolean ivt = invert.getValuef() > 0.5f;
+    if (bMod < 1 || sMod < 1 || hMod > 0 || fSharp > 0 || ivt || mon || fSoft > 0) {
+      for (int i = 0; i < colors.length; ++i) {
+        lx.RGBtoHSB(colors[i], hsb);
+        if (mon) {
+          hsb[0] = lx.getBaseHuef() / 360.f;
+        }
+        if (ivt) {
+          hsb[2] = 1 - hsb[2];
+        }
+        if (fSharp > 0) {
+          hsb[2] = hsb[2] < .5f ? pow(hsb[2],fSharp) : 1-pow(1-hsb[2],fSharp);
+        }
+        if (fSoft > 0) {
+          if (hsb[2] > 0.5f) {
+            hsb[2] = lerp(hsb[2], 0.5f + 2 * (hsb[2]-0.5f)*(hsb[2]-0.5f), fSoft);
+          } else {
+            hsb[2] = lerp(hsb[2], 0.5f * sqrt(2*hsb[2]), fSoft);
+          }
+        }
+        colors[i] = lx.hsb(
+          (360.f * hsb[0] + hMod*360.f) % 360,
+          100.f * hsb[1] * sMod,
+          100.f * hsb[2] * bMod
+        );
+      }
+    }
+  }
+}
+
+class QuantizeEffect extends SCEffect {
+  
+  int[] quantizedFrame;
+  float lastQuant;
+  final BasicParameter amount = new BasicParameter("AMT", 0);
+  
+  QuantizeEffect(GLucose glucose) {
+    super(glucose);
+    quantizedFrame = new int[glucose.lx.total];
+    lastQuant = 0;
+  } 
+  
+  public void apply(int[] colors) {
+    float fQuant = amount.getValuef();
+    if (fQuant > 0) {
+      float tRamp = (lx.tempo.rampf() % (1.f/pow(2,floor((1-fQuant) * 4))));
+      float f = lastQuant;
+      lastQuant = tRamp;
+      if (tRamp > f) {
+        for (int i = 0; i < colors.length; ++i) {
+          colors[i] = quantizedFrame[i];
+        }
+        return;
+      }
+    }
+    for (int i = 0; i < colors.length; ++i) {
+      quantizedFrame[i] = colors[i];
+    }
+  }
+}
+
+class BlurEffect extends SCEffect {
+  
+  final LXParameter amount = new BasicParameter("AMT", 0);
+  final int[] frame;
+  final LinearEnvelope env = new LinearEnvelope(0, 1, 100);
+  
+  BlurEffect(GLucose glucose) {
+    super(glucose);
+    addParameter(amount);
+    addModulator(env);
+    frame = new int[lx.total];
+    for (int i = 0; i < frame.length; ++i) {
+      frame[i] = 0xff000000;
+    }
+  }
+  
+  public void onEnable() {
+    env.setRangeFromHereTo(1, 400).start();
+    for (int i = 0; i < frame.length; ++i) {
+      frame[i] = 0xff000000;
+    }
+  }
+  
+  public void onDisable() {
+    env.setRangeFromHereTo(0, 1000).start();
+  }
+  
+  public void apply(int[] colors) {
+    float amt = env.getValuef() * amount.getValuef();
+    if (amt > 0) {    
+      amt = (1 - amt);
+      amt = 1 - (amt*amt*amt);
+      for (int i = 0; i < colors.length; ++i) {
+        // frame[i] = colors[i] = blendColor(colors[i], lerpColor(#000000, frame[i], amt, RGB), SCREEN);
+        frame[i] = colors[i] = lerpColor(colors[i], blendColor(colors[i], frame[i], SCREEN), amt, RGB);
+      }
+    }
+      
+  }  
+}
+abstract class SamPattern extends SCPattern {
+  public SamPattern(GLucose glucose) {
+    super(glucose);
+    setEligible(false);
+  }
+}
+
+class JazzRainbow extends SamPattern {
+  public JazzRainbow(GLucose glucose) {
+    super(glucose);
+  }
+
+  
+  public void run(double deltaMs) {
+    // Access the core master hue via this method call
+    float hv = lx.getBaseHuef();
+    for (int i = 0; i < colors.length*5; i=i+27) {
+      float a = hv%250;
+      if (i%2 == 0) {
+        for (int b = 0; b < 70; b++) {
+         colors[(i+b)%colors.length] = lx.hsb(a+i%250, 100, b*a%100);
+        }
+      }
+    }
+  } 
+}
+
+
+
+class HelixPattern extends SCPattern {
+
+  // Stores a line in point + vector form
+  private class Line {
+    private final PVector origin;
+    private final PVector vector;
+
+    Line(PVector pt, PVector v) {
+      origin = pt;
+      vector = v.get();
+      vector.normalize();
+    }
+
+    public PVector getPoint() {
+      return origin;
+    }
+
+    public PVector getVector() {
+      return vector;
+    }
+
+    public PVector getPointAt(final float t) {
+      return PVector.add(origin, PVector.mult(vector, t));
+    }
+
+    public boolean isColinear(final PVector pt) {
+      PVector projected = projectPoint(pt);
+      return projected.x==pt.x && projected.y==pt.y && projected.z==pt.z;
+    }
+
+    public float getTValue(final PVector pt) {
+      PVector subtraction = PVector.sub(pt, origin);
+      return subtraction.dot(vector);
+    }
+
+    public PVector projectPoint(final PVector pt) {
+      return getPointAt(getTValue(pt));
+    }
+
+    public PVector rotatePoint(final PVector p, final float t) {
+      final PVector o = origin;
+      final PVector v = vector;
+      
+      final float cost = cos(t);
+      final float sint = sin(t);
+
+      float x = (o.x*(v.y*v.y + v.z*v.z) - v.x*(o.y*v.y + o.z*v.z - v.x*p.x - v.y*p.y - v.z*p.z))*(1 - cost) + p.x*cost + (-o.z*v.y + o.y*v.z - v.z*p.y + v.y*p.z)*sint;
+      float y = (o.y*(v.x*v.x + v.z*v.z) - v.y*(o.x*v.x + o.z*v.z - v.x*p.x - v.y*p.y - v.z*p.z))*(1 - cost) + p.y*cost + (o.z*v.x - o.x*v.z + v.z*p.x - v.x*p.z)*sint;
+      float z = (o.z*(v.x*v.x + v.y*v.y) - v.z*(o.x*v.x + o.y*v.y - v.x*p.x - v.y*p.y - v.z*p.z))*(1 - cost) + p.z*cost + (-o.y*v.x + o.x*v.y - v.y*p.x + v.x*p.y)*sint;
+      return new PVector(x, y, z);
+    }
+  }
+
+  private class Helix {
+    private final Line axis;
+    private final float period; // period of coil
+    private final float rotationPeriod; // animation period
+    private final float radius; // radius of coil
+    private final float girth; // girth of coil
+    private final PVector referencePoint;
+    private float phase;
+    private PVector phaseNormal;
+
+    Helix(Line axis, float period, float radius, float girth, float phase, float rotationPeriod) {
+      this.axis = axis;
+      this.period = period;
+      this.radius = radius;
+      this.girth = girth;
+      this.phase = phase;
+      this.rotationPeriod = rotationPeriod;
+
+      // Generate a normal that will rotate to
+      // produce the helical shape.
+      PVector pt = new PVector(0, 1, 0);
+      if (this.axis.isColinear(pt)) {
+        pt = new PVector(0, 0, 1);
+        if (this.axis.isColinear(pt)) {
+          pt = new PVector(0, 1, 1);
+        }
+      }
+
+      this.referencePoint = pt;
+
+      // The normal is calculated by the cross product of the axis
+      // and a random point that is not colinear with it.
+      phaseNormal = axis.getVector().cross(referencePoint);
+      phaseNormal.normalize();
+      phaseNormal.mult(radius);
+    }
+
+    public Line getAxis() {
+      return axis;
+    }
+    
+    public PVector getPhaseNormal() {
+      return phaseNormal;
+    }
+    
+    public float getPhase() {
+      return phase;
+    }
+
+    public void step(double deltaMs) {
+      // Rotate
+      if (rotationPeriod != 0) {
+        this.phase = (phase + ((float)deltaMs / (float)rotationPeriod) * TWO_PI);
+      }
+    }
+
+    public PVector pointOnToroidalAxis(float t) {
+      PVector p = axis.getPointAt(t);
+      PVector middle = PVector.add(p, phaseNormal);
+      return axis.rotatePoint(middle, (t / period) * TWO_PI + phase);
+    }
+    
+    private float myDist(PVector p1, PVector p2) {
+      final float x = p2.x-p1.x;
+      final float y = p2.y-p1.y;
+      final float z = p2.z-p1.z;
+      return sqrt(x*x + y*y + z*z);
+    }
+
+    public int colorOfPoint(final PVector p) {
+      final float t = axis.getTValue(p);
+      final PVector axisPoint = axis.getPointAt(t);
+
+      // For performance reasons, cut out points that are outside of
+      // the tube where the toroidal coil lives.
+      if (abs(myDist(p, axisPoint) - radius) > girth*.5f) {
+        return lx.hsb(0,0,0);
+      }
+
+      // Find the appropriate point for the current rotation
+      // of the helix.
+      PVector toroidPoint = axisPoint;
+      toroidPoint.add(phaseNormal);
+      toroidPoint = axis.rotatePoint(toroidPoint, (t / period) * TWO_PI + phase);
+
+      // The rotated point represents the middle of the girth of
+      // the helix.  Figure out if the current point is inside that
+      // region.
+      float d = myDist(p, toroidPoint);
+
+      // Soften edges by fading brightness.
+      float b = constrain(100*(1 - ((d-.5f*girth)/(girth*.5f))), 0, 100);
+      return lx.hsb((lx.getBaseHuef() + (360*(phase / TWO_PI)))%360, 80, b);
+    }
+  }
+  
+  private class BasePairInfo {
+    Line line;
+    float colorPhase1;
+    float colorPhase2;
+    
+    BasePairInfo(Line line, float colorPhase1, float colorPhase2) {
+      this.line = line;
+      this.colorPhase1 = colorPhase1;
+      this.colorPhase2 = colorPhase2;
+    }
+  }
+
+  private final Helix h1;
+  private final Helix h2;
+  private final BasePairInfo[] basePairs;
+
+  private final BasicParameter helix1On = new BasicParameter("H1ON", 1);
+  private final BasicParameter helix2On = new BasicParameter("H2ON", 1);
+  private final BasicParameter basePairsOn = new BasicParameter("BPON", 1);
+
+  private static final float helixCoilPeriod = 100;
+  private static final float helixCoilRadius = 50;
+  private static final float helixCoilGirth = 30;
+  private static final float helixCoilRotationPeriod = 5000;
+
+  private static final float spokePeriod = 40;
+  private static final float spokeGirth = 20;
+  private static final float spokePhase = 10;
+  private static final float spokeRadius = helixCoilRadius - helixCoilGirth*.5f;
+  
+  private static final float tMin = -200;
+  private static final float tMax = 200;
+
+  public HelixPattern(GLucose glucose) {
+    super(glucose);
+
+    addParameter(helix1On);
+    addParameter(helix2On);
+    addParameter(basePairsOn);
+
+    PVector origin = new PVector(100, 50, 55);
+    PVector axis = new PVector(1,0,0);
+
+    h1 = new Helix(
+      new Line(origin, axis),
+      helixCoilPeriod,
+      helixCoilRadius,
+      helixCoilGirth,
+      0,
+      helixCoilRotationPeriod);
+    h2 = new Helix(
+      new Line(origin, axis),
+      helixCoilPeriod,
+      helixCoilRadius,
+      helixCoilGirth,
+      PI,
+      helixCoilRotationPeriod);
+      
+    basePairs = new BasePairInfo[(int)floor((tMax - tMin)/spokePeriod)];
+  }
+
+  private void calculateSpokes() {
+    float colorPhase = PI/6;
+    for (float t = tMin + spokePhase; t < tMax; t += spokePeriod) {
+      int spokeIndex = (int)floor((t - tMin)/spokePeriod);
+      PVector h1point = h1.pointOnToroidalAxis(t);
+      PVector spokeCenter = h1.getAxis().getPointAt(t);
+      PVector spokeVector = PVector.sub(h1point, spokeCenter);
+      Line spokeLine = new Line(spokeCenter, spokeVector);
+      basePairs[spokeIndex] = new BasePairInfo(spokeLine, colorPhase * spokeIndex, colorPhase * (spokeIndex + 1));
+    }
+  }
+  
+  private int calculateSpokeColor(final PVector pt) {
+    // Find the closest spoke's t-value and calculate its
+    // axis.  Until everything animates in the model reference
+    // frame, this has to be calculated at every step because
+    // the helices rotate.
+    Line axis = h1.getAxis();
+    float t = axis.getTValue(pt) + spokePhase;
+    int spokeIndex = (int)floor((t - tMin + spokePeriod/2) / spokePeriod);
+    if (spokeIndex < 0 || spokeIndex >= basePairs.length) {
+      return lx.hsb(0,0,0);
+    }
+    BasePairInfo basePair = basePairs[spokeIndex];
+    Line spokeLine = basePair.line;
+    PVector pointOnSpoke = spokeLine.projectPoint(pt);
+    float d = PVector.dist(pt, pointOnSpoke);
+    float b = (PVector.dist(pointOnSpoke, spokeLine.getPoint()) < spokeRadius) ? constrain(100*(1 - ((d-.5f*spokeGirth)/(spokeGirth*.5f))), 0, 100) : 0.f;
+    float phase = spokeLine.getTValue(pointOnSpoke) < 0 ? basePair.colorPhase1 : basePair.colorPhase2;
+    return lx.hsb((lx.getBaseHuef() + (360*(phase / TWO_PI)))%360, 80.f, b);
+  }
+
+  public void run(double deltaMs) {
+    boolean h1on = helix1On.getValue() > 0.5f;
+    boolean h2on = helix2On.getValue() > 0.5f;
+    boolean spokesOn = (float)basePairsOn.getValue() > 0.5f;
+
+    h1.step(deltaMs);
+    h2.step(deltaMs);
+    calculateSpokes();
+
+    for (Point p : model.points) {
+      PVector pt = new PVector(p.x,p.y,p.z);
+      int h1c = h1.colorOfPoint(pt);
+      int h2c = h2.colorOfPoint(pt);
+      int spokeColor = calculateSpokeColor(pt);
+
+      if (!h1on) {
+        h1c = lx.hsb(0,0,0);
+      }
+
+      if (!h2on) {
+        h2c = lx.hsb(0,0,0);
+      }
+
+      if (!spokesOn) {
+        spokeColor = lx.hsb(0,0,0);
+      }
+
+      // The helices are positioned to not overlap.  If that changes,
+      // a better blending formula is probably needed.
+      colors[p.index] = blendColor(blendColor(h1c, h2c, ADD), spokeColor, ADD);
+    }
+  }
+}
+
+class BlankPattern extends SCPattern {
+  BlankPattern(GLucose glucose) {
+    super(glucose);
+  }
+  
+  public void run(double deltaMs) {
+    setColors(0xff000000);
+  }
+}
+
+abstract class TestPattern extends SCPattern {
+  public TestPattern(GLucose glucose) {
+    super(glucose);
+    setEligible(false);
+  }
+}
+
+class TestSpeakerMapping extends TestPattern {
+  TestSpeakerMapping(GLucose glucose) {
+    super(glucose);
+  }
+  
+  public void run(double deltaMs) {
+    int h = 0;
+    for (Speaker speaker : model.speakers) {
+      for (Strip strip : speaker.strips) {
+        float b = 100;
+        for (Point p : strip.points) {
+          colors[p.index] = lx.hsb(h % 360, 100, b);
+          b = max(0, b - 10);
+        }
+        h += 70;
+      }
+    }
+  }
+
+}
+
+class TestBassMapping extends TestPattern {
+  TestBassMapping(GLucose glucose) {
+    super(glucose);
+  }
+  
+  public void run(double deltaMs) {
+    int[] strips = { 2, 1, 0, 3, 13, 12, 15, 14, 9, 8, 11, 10, 5, 4, 7, 6 };
+    int h = 0;
+    for (int si : strips) {
+      float b = 100;
+      for (Point p : model.bassBox.strips.get(si).points) {
+        colors[p.index] = lx.hsb(h % 360, 100, b);
+        b = max(0, b - 10);
+      }
+      h += 70;
+    }
+  }
+}
+
+class TestFloorMapping extends TestPattern {
+  TestFloorMapping(GLucose glucose) {
+    super(glucose);
+  }
+
+  public void run(double deltaMs) {
+    int[] strutIndices = {6, 5, 4, 3, 2, 1, 0, 7};
+    int h = 0;
+    for (int si : strutIndices) {
+      float b = 100;
+      for (Point p : model.bassBox.struts.get(si).points) {
+        colors[p.index] = lx.hsb(h % 360, 100, b);
+        b = max(0, b - 10);
+      }
+      h += 50;
+    }
+    int[] floorIndices = {0, 1, 2, 3};
+    h = 0;
+    for (int fi : floorIndices) {
+      float b = 100;
+      for (Point p : model.boothFloor.strips.get(fi).points) {
+        colors[p.index] = lx.hsb(h, 100, b);
+        b = max(0, b - 3);
+      }
+      h += 90;
+    }
+  }
+}
+
+class TestPerformancePattern extends TestPattern {
+  
+  final BasicParameter ops = new BasicParameter("OPS", 0);
+  final BasicParameter iter = new BasicParameter("ITER", 0);
+  
+  TestPerformancePattern(GLucose glucose) {
+    super(glucose);
+    addParameter(ops);
+    addParameter(iter);
+  }
+  
+  public void run(double deltaMs) {
+    float x = 1;
+    for (int j = 0; j < ops.getValuef() * 400000; ++j) {
+      x *= random(0, 1);
+    }
+
+    if (iter.getValuef() < 0.25f) {
+      for (Point p : model.points) {
+        colors[p.index] = lx.hsb(
+          (p.x*.1f + p.y*.1f) % 360,
+          100,
+          100
+        );
+      }
+    } else if (iter.getValuef() < 0.5f) {
+      for (int i = 0; i < colors.length; ++i) {
+        colors[i] = lx.hsb(
+          (90 + model.px[i]*.1f + model.py[i]*.1f) % 360,
+          100,
+          100
+        );
+      }
+    } else if (iter.getValuef() < 0.75f) {
+      for (int i = 0; i < colors.length; ++i) {
+        colors[i] = lx.hsb(
+          (180 + model.p[3*i]*.1f + model.p[3*i+1]*.1f) % 360,
+          100,
+          100
+        );
+      }
+    } else {
+      for (int i = 0; i < colors.length; ++i) {
+        colors[i] = lx.hsb(
+          (270 + model.x(i)*.1f + model.y(i)*.1f) % 360,
+          100,
+          100
+        );
+      }
+    }
+  }
+}
+
+class TestStripPattern extends TestPattern {
+  
+  SinLFO d = new SinLFO(4, 40, 4000);
+  
+  public TestStripPattern(GLucose glucose) {
+    super(glucose);
+    addModulator(d).trigger();
+  }
+  
+  public void run(double deltaMs) {
+    for (Strip s : model.strips) {
+      for (Point p : s.points) {
+        colors[p.index] = lx.hsb(
+          lx.getBaseHuef(),
+          100,
+          max(0, 100 - d.getValuef()*dist(p.x, p.y, s.cx, s.cy))
+        );
+      }
+    }
+  }
+}
+
+/**
+ * Simplest demonstration of using the rotating master hue.
+ * All pixels are full-on the same color.
+ */
+class TestHuePattern extends TestPattern {
+  public TestHuePattern(GLucose glucose) {
+    super(glucose);
+  }
+  
+  public void run(double deltaMs) {
+    // Access the core master hue via this method call
+    float hv = lx.getBaseHuef();
+    for (int i = 0; i < colors.length; ++i) {
+      colors[i] = lx.hsb(hv, 100, 100);
+    }
+  } 
+}
+
+/**
+ * Test of a wave moving across the X axis.
+ */
+class TestXPattern extends TestPattern {
+  private final SinLFO xPos = new SinLFO(0, model.xMax, 4000);
+  public TestXPattern(GLucose glucose) {
+    super(glucose);
+    addModulator(xPos).trigger();
+  }
+  public void run(double deltaMs) {
+    float hv = lx.getBaseHuef();
+    for (Point p : model.points) {
+      // This is a common technique for modulating brightness.
+      // You can use abs() to determine the distance between two
+      // values. The further away this point is from an exact
+      // point, the more we decrease its brightness
+      float bv = max(0, 100 - abs(p.x - xPos.getValuef()));
+      colors[p.index] = lx.hsb(hv, 100, bv);
+    }
+  }
+}
+
+/**
+ * Test of a wave on the Y axis.
+ */
+class TestYPattern extends TestPattern {
+  private final SinLFO yPos = new SinLFO(0, model.yMax, 4000);
+  public TestYPattern(GLucose glucose) {
+    super(glucose);
+    addModulator(yPos).trigger();
+  }
+  public void run(double deltaMs) {
+    float hv = lx.getBaseHuef();
+    for (Point p : model.points) {
+      float bv = max(0, 100 - abs(p.y - yPos.getValuef()));
+      colors[p.index] = lx.hsb(hv, 100, bv);
+    }
+  }
+}
+
+/**
+ * Test of a wave on the Z axis.
+ */
+class TestZPattern extends TestPattern {
+  private final SinLFO zPos = new SinLFO(0, model.zMax, 4000);
+  public TestZPattern(GLucose glucose) {
+    super(glucose);
+    addModulator(zPos).trigger();
+  }
+  public void run(double deltaMs) {
+    float hv = lx.getBaseHuef();
+    for (Point p : model.points) {
+      float bv = max(0, 100 - abs(p.z - zPos.getValuef()));
+      colors[p.index] = lx.hsb(hv, 100, bv);
+    }
+  }
+}
+
+/**
+ * This shows how to iterate over towers, enumerated in the model.
+ */
+class TestTowerPattern extends TestPattern {
+  private final SawLFO towerIndex = new SawLFO(0, model.towers.size(), 1000*model.towers.size());
+  
+  public TestTowerPattern(GLucose glucose) {
+    super(glucose);
+    addModulator(towerIndex).trigger();
+  }
+
+  public void run(double deltaMs) {
+    int ti = 0;
+    for (Tower t : model.towers) {
+      for (Point p : t.points) {
+        colors[p.index] = lx.hsb(
+          lx.getBaseHuef(),
+          100,
+          max(0, 100 - 80*LXUtils.wrapdistf(ti, towerIndex.getValuef(), model.towers.size()))
+        );
+      }
+      ++ti;
+    }
+  }
+  
+}
+
+/**
+ * This is a demonstration of how to use the projection library. A projection
+ * creates a mutation of the coordinates of all the points in the model, creating
+ * virtual x,y,z coordinates. In effect, this is like virtually rotating the entire
+ * art car. However, since in reality the car does not move, the result is that
+ * it appears that the object we are drawing on the car is actually moving.
+ *
+ * Keep in mind that what we are creating a projection of is the view coordinates.
+ * Depending on your intuition, some operations may feel backwards. For instance,
+ * if you translate the view to the right, it will make it seem that the object
+ * you are drawing has moved to the left. If you scale the view up 2x, objects
+ * drawn with the same absolute values will seem to be half the size.
+ *
+ * If this feels counterintuitive at first, don't worry. Just remember that you
+ * are moving the pixels, not the structure. We're dealing with a finite set
+ * of sparse, non-uniformly spaced pixels. Mutating the structure would move
+ * things to a space where there are no pixels in 99% of the cases.
+ */
+class TestProjectionPattern extends TestPattern {
+  
+  private final Projection projection;
+  private final SawLFO angle = new SawLFO(0, TWO_PI, 9000);
+  private final SinLFO yPos = new SinLFO(-20, 40, 5000);
+  
+  public TestProjectionPattern(GLucose glucose) {
+    super(glucose);
+    projection = new Projection(model);
+    addModulator(angle).trigger();
+    addModulator(yPos).trigger();
+  }
+  
+  public void run(double deltaMs) {
+    // For the same reasons described above, it may logically feel to you that
+    // some of these operations are in reverse order. Again, just keep in mind that
+    // the car itself is what's moving, not the object
+    projection.reset(model)
+    
+      // Translate so the center of the car is the origin, offset by yPos
+      .translateCenter(model, 0, yPos.getValuef(), 0)
+
+      // Rotate around the origin (now the center of the car) about an X-vector
+      .rotate(angle.getValuef(), 1, 0, 0)
+
+      // Scale up the Y axis (objects will look smaller in that access)
+      .scale(1, 1.5f, 1);
+
+    float hv = lx.getBaseHuef();
+    for (Coord c : projection) {
+      float d = sqrt(c.x*c.x + c.y*c.y + c.z*c.z); // distance from origin
+      // d = abs(d-60) + max(0, abs(c.z) - 20); // life saver / ring thing
+      d = max(0, abs(c.y) - 10 + .1f*abs(c.z) + .02f*abs(c.x)); // plane / spear thing
+      colors[c.index] = lx.hsb(
+        (hv + .6f*abs(c.x) + abs(c.z)) % 360,
+        100,
+        constrain(140 - 40*d, 0, 100)
+      );
+    }
+  } 
+}
+
+class TestCubePattern extends TestPattern {
+  
+  private SawLFO index = new SawLFO(0, Cube.POINTS_PER_CUBE, Cube.POINTS_PER_CUBE*60);
+  
+  TestCubePattern(GLucose glucose) {
+    super(glucose);
+    addModulator(index).start();
+  }
+  
+  public void run(double deltaMs) {
+    for (Cube c : model.cubes) {
+      int i = 0;
+      for (Point p : c.points) {
+        colors[p.index] = lx.hsb(
+          lx.getBaseHuef(),
+          100,
+          max(0, 100 - 80.f*abs(i - index.getValuef()))
+        );
+        ++i;
+      }
+    }
+  }
+}
+
+class MappingTool extends TestPattern {
+    
+  private int cubeIndex = 0;
+  private int stripIndex = 0;
+  private int channelIndex = 0;
+
+  public final int MAPPING_MODE_ALL = 0;
+  public final int MAPPING_MODE_CHANNEL = 1;
+  public final int MAPPING_MODE_SINGLE_CUBE = 2;
+  public int mappingMode = MAPPING_MODE_ALL;
+
+  public final int CUBE_MODE_ALL = 0;
+  public final int CUBE_MODE_SINGLE_STRIP = 1;
+  public final int CUBE_MODE_STRIP_PATTERN = 2;
+  public int cubeMode = CUBE_MODE_ALL;
+
+  public boolean channelModeRed = true;
+  public boolean channelModeGreen = false;
+  public boolean channelModeBlue = false;
+  
+  private final int numChannels;
+  
+  private final PandaMapping[] pandaMappings;
+  private PandaMapping activePanda;
+  private ChannelMapping activeChannel;
+  
+  MappingTool(GLucose glucose, PandaMapping[] pandaMappings) {
+    super(glucose);
+    this.pandaMappings = pandaMappings;
+    numChannels = pandaMappings.length * PandaMapping.CHANNELS_PER_BOARD;
+    setChannel();
+  }
+
+  public int numChannels() {
+    return numChannels;
+  }
+  
+  private void setChannel() {
+    activePanda = pandaMappings[channelIndex / PandaMapping.CHANNELS_PER_BOARD];
+    activeChannel = activePanda.channelList[channelIndex % PandaMapping.CHANNELS_PER_BOARD];
+  }
+  
+  private int indexOfCubeInChannel(Cube c) {
+    if (activeChannel.mode == ChannelMapping.MODE_CUBES) {
+      int i = 1;
+      for (int index : activeChannel.objectIndices) {
+        if ((index >= 0) && (c == model.getCubeByRawIndex(index))) {
+          return i;
+        }
+        ++i;
+      }
+    }
+    return 0;
+  }
+  
+  private void printInfo() {
+    println("Cube:" + cubeIndex + " Strip:" + (stripIndex+1));
+  }
+  
+  public void cube(int delta) {
+    int len = model.cubes.size();
+    cubeIndex = (len + cubeIndex + delta) % len;
+    printInfo();
+  }
+  
+  public void strip(int delta) {
+    int len = Cube.STRIPS_PER_CUBE;
+    stripIndex = (len + stripIndex + delta) % len;
+    printInfo();
+  }
+  
+  public void run(double deltaMs) {
+    int off = 0xff000000;
+    int c = off;
+    int r = 0xffFF0000;
+    int g = 0xff00FF00;
+    int b = 0xff0000FF;
+    if (channelModeRed) c |= r;
+    if (channelModeGreen) c |= g;
+    if (channelModeBlue) c |= b;
+    
+    int ci = 0;
+    for (Cube cube : model.cubes) {
+      boolean cubeOn = false;
+      int indexOfCubeInChannel = indexOfCubeInChannel(cube);
+      switch (mappingMode) {
+        case MAPPING_MODE_ALL: cubeOn = true; break;
+        case MAPPING_MODE_SINGLE_CUBE: cubeOn = (cubeIndex == ci); break;
+        case MAPPING_MODE_CHANNEL: cubeOn = (indexOfCubeInChannel > 0); break;
+      }
+      if (cubeOn) {
+        if (mappingMode == MAPPING_MODE_CHANNEL) {
+          int cc = off;
+          switch (indexOfCubeInChannel) {
+            case 1: cc = r; break;
+            case 2: cc = r|g; break;
+            case 3: cc = g; break;
+            case 4: cc = b; break;
+            case 5: cc = r|b; break;
+          }
+          setColor(cube, cc);
+        } else if (cubeMode == CUBE_MODE_STRIP_PATTERN) {
+          int si = 0;
+          int sc = off;
+          for (Strip strip : cube.strips) {
+            int faceI = si / Face.STRIPS_PER_FACE;
+            switch (faceI) {
+              case 0: sc = r; break;
+              case 1: sc = g; break;
+              case 2: sc = b; break;
+              case 3: sc = r|g|b; break;
+            }
+            if (si % Face.STRIPS_PER_FACE == 2) {
+              sc = r|g;
+            }
+            setColor(strip, sc);
+            ++si;
+          }
+        } else if (cubeMode == CUBE_MODE_SINGLE_STRIP) {
+          setColor(cube, off);
+          setColor(cube.strips.get(stripIndex), c);
+        } else {
+          setColor(cube, c);
+        }
+      } else {
+        setColor(cube, off);
+      }
+      ++ci;
+    }
+  }
+  
+  public void setCube(int index) {
+    cubeIndex = index % model.cubes.size();
+  }
+  
+  public void incCube() {
+    cubeIndex = (cubeIndex + 1) % model.cubes.size();
+  }
+  
+  public void decCube() {
+    --cubeIndex;
+    if (cubeIndex < 0) {
+      cubeIndex += model.cubes.size();
+    }
+  }
+  
+  public void setChannel(int index) {
+    channelIndex = index % numChannels;
+    setChannel();
+  }
+
+  public void incChannel() {
+    channelIndex = (channelIndex + 1) % numChannels;
+    setChannel();
+  }
+  
+  public void decChannel() {
+    channelIndex = (channelIndex + numChannels - 1) % numChannels;
+    setChannel();    
+  }
+  
+  public void setStrip(int index) {
+    stripIndex = index % Cube.STRIPS_PER_CUBE;
+  }
+  
+  public void incStrip() {
+    stripIndex = (stripIndex + 1) % Cube.STRIPS_PER_CUBE;
+  }
+  
+  public void decStrip() {
+    stripIndex = (stripIndex + Cube.STRIPS_PER_CUBE - 1) % Cube.STRIPS_PER_CUBE;
+  }
+  
+  public void keyPressed(UIMapping uiMapping) {
+    switch (keyCode) {
+      case UP: if (mappingMode == MAPPING_MODE_CHANNEL) incChannel(); else incCube(); break;
+      case DOWN: if (mappingMode == MAPPING_MODE_CHANNEL) decChannel(); else decCube(); break;
+      case LEFT: decStrip(); break;
+      case RIGHT: incStrip(); break;
+    }
+    switch (key) {
+      case 'r': channelModeRed = !channelModeRed; break;
+      case 'g': channelModeGreen = !channelModeGreen; break;
+      case 'b': channelModeBlue = !channelModeBlue; break;
+    }
+    uiMapping.setChannelID(channelIndex+1);
+    uiMapping.setCubeID(cubeIndex+1);
+    uiMapping.setStripID(stripIndex+1);
+    uiMapping.redraw();
+  }
+
+}
+/**
+ * Not very flushed out, but kind of fun nonetheless.
+ */
+class TimSpheres extends SCPattern {
+  private BasicParameter hueParameter = new BasicParameter("RAD", 1.0f);
+  private final SawLFO lfo = new SawLFO(0, 1, 10000);
+  private final SinLFO sinLfo = new SinLFO(0, 1, 4000);
+  private final float centerX, centerY, centerZ;
+  
+  class Sphere {
+    float x, y, z;
+    float radius;
+    float hue;
+  }
+  
+  private final Sphere[] spheres;
+  
+  public TimSpheres(GLucose glucose) {
+    super(glucose);
+    addParameter(hueParameter);
+    addModulator(lfo).trigger();
+    addModulator(sinLfo).trigger();
+    centerX = (model.xMax + model.xMin) / 2;
+    centerY = (model.yMax + model.yMin) / 2;
+    centerZ = (model.zMax + model.zMin) / 2;
+    
+    spheres = new Sphere[2];
+    
+    spheres[0] = new Sphere();
+    spheres[0].x = model.xMin;
+    spheres[0].y = centerY;
+    spheres[0].z = centerZ;
+    spheres[0].hue = 0;
+    spheres[0].radius = 50;
+    
+    spheres[1] = new Sphere();
+    spheres[1].x = model.xMax;
+    spheres[1].y = centerY;
+    spheres[1].z = centerZ;
+    spheres[1].hue = 0.33f;
+    spheres[1].radius = 50;
+  }
+  
+  public void run(double deltaMs) {
+    // Access the core master hue via this method call
+    float hv = hueParameter.getValuef();
+    float lfoValue = lfo.getValuef();
+    float sinLfoValue = sinLfo.getValuef();
+    
+    spheres[0].x = model.xMin + sinLfoValue * model.xMax;
+    spheres[1].x = model.xMax - sinLfoValue * model.xMax;
+    
+    spheres[0].radius = 100 * hueParameter.getValuef();
+    spheres[1].radius = 100 * hueParameter.getValuef();
+    
+    for (Point p : model.points) {
+      float value = 0;
+
+      int c = lx.hsb(0, 0, 0);      
+      for (Sphere s : spheres) {
+        float d = sqrt(pow(p.x - s.x, 2) + pow(p.y - s.y, 2) + pow(p.z - s.z, 2));
+        float r = (s.radius); // * (sinLfoValue + 0.5));
+        value = max(0, 1 - max(0, d - r) / 10);
+        
+        c = blendColor(c, lx.hsb(((s.hue + lfoValue) % 1) * 360, 100, min(1, value) * 100), ADD);
+      }
+      
+      colors[p.index] = c;
+    }
+  } 
+}
+
+class Vector2 {
+  float x, y;
+  
+  Vector2() {
+    this(0, 0);
+  }
+  
+  Vector2(float x, float y) {
+    this.x = x;
+    this.y = y;
+  }
+  
+  public float distanceTo(float x, float y) {
+    return sqrt(pow(x - this.x, 2) + pow(y - this.y, 2));
+  }
+  
+  public float distanceTo(Vector2 v) {
+    return distanceTo(v.x, v.y);
+  }
+  
+  public Vector2 plus(float x, float y) {
+    return new Vector2(this.x + x, this.y + y);
+  }
+  
+  public Vector2 plus(Vector2 v) {
+    return plus(v.x, v.y);
+  }
+    
+  public Vector2 minus(Vector2 v) {
+    return plus(-1 * v.x, -1 * v.y);
+  }
+}
+
+class Vector3 {
+  float x, y, z;
+  
+  Vector3() {
+    this(0, 0, 0);
+  }
+  
+  Vector3(float x, float y, float z) {
+    this.x = x;
+    this.y = y;
+    this.z = z;
+  }
+  
+  public float distanceTo(float x, float y, float z) {
+    return sqrt(pow(x - this.x, 2) + pow(y - this.y, 2) + pow(z - this.z, 2));
+  }
+  
+  public float distanceTo(Vector3 v) {
+    return distanceTo(v.x, v.y, v.z);
+  }
+  
+  public float distanceTo(Point p) {
+    return distanceTo(p.x, p.y, p.z);
+  }
+  
+  public void add(Vector3 other, float multiplier) {
+    this.add(other.x * multiplier, other.y * multiplier, other.z * multiplier);
+  }  
+    
+  public void add(float x, float y, float z) {
+    this.x += x;
+    this.y += y;
+    this.z += z;
+  }
+  
+  public void divide(float factor) {
+    this.x /= factor;
+    this.y /= factor;
+    this.z /= factor;
+  }
+}
+
+class Rotation {
+  private float a, b, c, d, e, f, g, h, i;
+  
+  Rotation(float yaw, float pitch, float roll) {
+    float cosYaw = cos(yaw);
+    float sinYaw = sin(yaw);
+    float cosPitch = cos(pitch);
+    float sinPitch = sin(pitch);
+    float cosRoll = cos(roll);
+    float sinRoll = sin(roll);
+    
+    a = cosYaw * cosPitch;
+    b = cosYaw * sinPitch * sinRoll - sinYaw * cosRoll;
+    c = cosYaw * sinPitch * cosRoll + sinYaw * sinRoll;
+    d = sinYaw * cosPitch;
+    e = sinYaw * sinPitch * sinRoll + cosYaw * cosRoll;
+    f = sinYaw * sinPitch * cosRoll - cosYaw * sinRoll;
+    g = -1 * sinPitch;
+    h = cosPitch * sinRoll;
+    i = cosPitch * cosRoll;
+  }
+  
+  public Vector3 rotated(Vector3 v) {
+    return new Vector3(
+      rotatedX(v),
+      rotatedY(v),
+      rotatedZ(v));
+
+  }
+  
+  public float rotatedX(Vector3 v) {
+    return a * v.x + b * v.y + c * v.z;
+  }
+  
+  public float rotatedY(Vector3 v) {
+    return d * v.x + e * v.y + f * v.z;
+  }
+  
+  public float rotatedZ(Vector3 v) {
+    return g * v.x + h * v.y + i * v.z;
+  }
+}
+
+/**
+ * Very literal rain effect.  Not that great as-is but some tweaking could make it nice.
+ * A couple ideas:
+ *   - changing hue and direction of "rain" could make a nice fire effect
+ *   - knobs to change frequency and size of rain drops
+ *   - sync somehow to tempo but maybe less frequently than every beat?
+ */
+class TimRaindrops extends SCPattern {
+  public Vector3 randomVector3() {
+    return new Vector3(
+        random(model.xMax - model.xMin) + model.xMin,
+        random(model.yMax - model.yMin) + model.yMin,
+        random(model.zMax - model.zMin) + model.zMin);
+  }
+
+  class Raindrop {
+    Vector3 p;
+    Vector3 v;
+    float radius;
+    float hue;
+    
+    Raindrop() {
+      this.radius = 30;
+      this.p = new Vector3(
+              random(model.xMax - model.xMin) + model.xMin,
+              model.yMax + this.radius,
+              random(model.zMax - model.zMin) + model.zMin);
+      float velMagnitude = 120;
+      this.v = new Vector3(
+          0,
+          -3 * model.yMax,
+          0);
+      this.hue = random(40) + 200;
+    }
+    
+    // returns TRUE when this should die
+    public boolean age(double ms) {
+      p.add(v, (float) (ms / 1000.0f));
+      return this.p.y < (0 - this.radius);
+    }
+  }
+  
+  private float leftoverMs = 0;
+  private float msPerRaindrop = 40;
+  private List<Raindrop> raindrops;
+  
+  public TimRaindrops(GLucose glucose) {
+    super(glucose);
+    raindrops = new LinkedList<Raindrop>();
+  }
+  
+  public void run(double deltaMs) {
+    leftoverMs += deltaMs;
+    while (leftoverMs > msPerRaindrop) {
+      leftoverMs -= msPerRaindrop;
+      raindrops.add(new Raindrop());
+    }
+    
+    for (Point p : model.points) {
+      int c = 
+        blendColor(
+          lx.hsb(210, 20, (float)Math.max(0, 1 - Math.pow((model.yMax - p.y) / 10, 2)) * 50),
+          lx.hsb(220, 60, (float)Math.max(0, 1 - Math.pow((p.y - model.yMin) / 10, 2)) * 100),
+          ADD);
+      for (Raindrop raindrop : raindrops) {
+        if (p.x >= (raindrop.p.x - raindrop.radius) && p.x <= (raindrop.p.x + raindrop.radius) &&
+            p.y >= (raindrop.p.y - raindrop.radius) && p.y <= (raindrop.p.y + raindrop.radius)) {
+          float d = raindrop.p.distanceTo(p) / raindrop.radius;
+  //      float value = (float)Math.max(0, 1 - Math.pow(Math.min(0, d - raindrop.radius) / 5, 2)); 
+          if (d < 1) {
+            c = blendColor(c, lx.hsb(raindrop.hue, 80, (float)Math.pow(1 - d, 0.01f) * 100), ADD);
+          }
+        }
+      }
+      colors[p.index] = c;
+    }
+    
+    Iterator<Raindrop> i = raindrops.iterator();
+    while (i.hasNext()) {
+      Raindrop raindrop = i.next();
+      boolean dead = raindrop.age(deltaMs);
+      if (dead) {
+        i.remove();
+      }
+    }
+  } 
+}
+
+
+class TimCubes extends SCPattern {
+  private BasicParameter rateParameter = new BasicParameter("RATE", 0.125f);
+  private BasicParameter attackParameter = new BasicParameter("ATTK", 0.5f);
+  private BasicParameter decayParameter = new BasicParameter("DECAY", 0.5f);
+  private BasicParameter hueParameter = new BasicParameter("HUE", 0.5f);
+  private BasicParameter hueVarianceParameter = new BasicParameter("H.V.", 0.25f);
+  private BasicParameter saturationParameter = new BasicParameter("SAT", 0.5f);
+  
+  class CubeFlash {
+    Cube c;
+    float value;
+    float hue;
+    boolean hasPeaked;
+    
+    CubeFlash() {
+      c = model.cubes.get(floor(random(model.cubes.size())));
+      hue = random(1);
+      boolean infiniteAttack = (attackParameter.getValuef() > 0.999f);
+      hasPeaked = infiniteAttack;
+      value = (infiniteAttack ? 1 : 0);
+    }
+    
+    // returns TRUE if this should die
+    public boolean age(double ms) {
+      if (!hasPeaked) {
+        value = value + (float) (ms / 1000.0f * ((attackParameter.getValuef() + 0.01f) * 5));
+        if (value >= 1.0f) {
+          value = 1.0f;
+          hasPeaked = true;
+        }
+        return false;
+      } else {
+        value = value - (float) (ms / 1000.0f * ((decayParameter.getValuef() + 0.01f) * 10));
+        return value <= 0;
+      }
+    }
+  }
+  
+  private float leftoverMs = 0;
+  private List<CubeFlash> flashes;
+  
+  public TimCubes(GLucose glucose) {
+    super(glucose);
+    addParameter(rateParameter);
+    addParameter(attackParameter);
+    addParameter(decayParameter);
+    addParameter(hueParameter);
+    addParameter(hueVarianceParameter);
+    addParameter(saturationParameter);
+    flashes = new LinkedList<CubeFlash>();
+  }
+  
+  public void run(double deltaMs) {
+    leftoverMs += deltaMs;
+    float msPerFlash = 1000 / ((rateParameter.getValuef() + .01f) * 100);
+    while (leftoverMs > msPerFlash) {
+      leftoverMs -= msPerFlash;
+      flashes.add(new CubeFlash());
+    }
+    
+    for (Point p : model.points) {
+      colors[p.index] = 0;
+    }
+    
+    for (CubeFlash flash : flashes) {
+      float hue = (hueParameter.getValuef() + (hueVarianceParameter.getValuef() * flash.hue)) % 1.0f;
+      int c = lx.hsb(hue * 360, saturationParameter.getValuef() * 100, (flash.value) * 100);
+      for (Point p : flash.c.points) {
+        colors[p.index] = c;
+      }
+    }
+    
+    Iterator<CubeFlash> i = flashes.iterator();
+    while (i.hasNext()) {
+      CubeFlash flash = i.next();
+      boolean dead = flash.age(deltaMs);
+      if (dead) {
+        i.remove();
+      }
+    }
+  } 
+}
+
+/**
+ * This one is the best but you need to play with all the knobs.  It's synced to
+ * the tempo, with the WSpd knob letting you pick 4 discrete multipliers for
+ * the tempo.
+ *
+ * Basically it's just 3 planes all rotating to the beat, but also rotated relative
+ * to one another.  The intersection of the planes and the cubes over time makes
+ * for a nice abstract effect.
+ */
+class TimPlanes extends SCPattern {
+  private BasicParameter wobbleParameter = new BasicParameter("Wob", 0.166f);
+  private BasicParameter wobbleSpreadParameter = new BasicParameter("WSpr", 0.25f);
+  private BasicParameter wobbleSpeedParameter = new BasicParameter("WSpd", 0.375f);
+  private BasicParameter wobbleOffsetParameter = new BasicParameter("WOff", 0);
+  private BasicParameter derezParameter = new BasicParameter("Drez", 0.5f);
+  private BasicParameter thicknessParameter = new BasicParameter("Thick", 0.4f);
+  private BasicParameter ySpreadParameter = new BasicParameter("ySpr", 0.2f);
+  private BasicParameter hueParameter = new BasicParameter("Hue", 0.75f);
+  private BasicParameter hueSpreadParameter = new BasicParameter("HSpr", 0.68f);
+
+  final float centerX, centerY, centerZ;
+  float phase;
+  
+  class Plane {
+    Vector3 center;
+    Rotation rotation;
+    float hue;
+    
+    Plane(Vector3 center, Rotation rotation, float hue) {
+      this.center = center;
+      this.rotation = rotation;
+      this.hue = hue;
+    }
+  }
+      
+  TimPlanes(GLucose glucose) {
+    super(glucose);
+    centerX = (model.xMin + model.xMax) / 2;
+    centerY = (model.yMin + model.yMax) / 2;
+    centerZ = (model.zMin + model.zMax) / 2;
+    phase = 0;
+    addParameter(wobbleParameter);
+    addParameter(wobbleSpreadParameter);
+    addParameter(wobbleSpeedParameter);
+//    addParameter(wobbleOffsetParameter);
+    addParameter(derezParameter);
+    addParameter(thicknessParameter);
+    addParameter(ySpreadParameter);
+    addParameter(hueParameter);
+    addParameter(hueSpreadParameter);
+  }
+  
+  int beat = 0;
+  float prevRamp = 0;
+  float[] wobbleSpeeds = { 1.0f/8, 1.0f/4, 1.0f/2, 1.0f };
+  
+  public void run(double deltaMs) {
+    float ramp = (float)lx.tempo.ramp();
+    if (ramp < prevRamp) {
+      beat = (beat + 1) % 32;
+    }
+    prevRamp = ramp;
+    
+    float wobbleSpeed = wobbleSpeeds[floor(wobbleSpeedParameter.getValuef() * wobbleSpeeds.length * 0.9999f)];
+
+    phase = (((beat + ramp) * wobbleSpeed + wobbleOffsetParameter.getValuef()) % 1) * 2 * PI;
+    
+    float ySpread = ySpreadParameter.getValuef() * 50;
+    float wobble = wobbleParameter.getValuef() * PI;
+    float wobbleSpread = wobbleSpreadParameter.getValuef() * PI;
+    float hue = hueParameter.getValuef() * 360;
+    float hueSpread = (hueSpreadParameter.getValuef() - 0.5f) * 360;
+
+    float saturation = 10 + 60.0f * pow(ramp, 0.25f);
+    
+    float derez = derezParameter.getValuef();
+    
+    Plane[] planes = {
+      new Plane(
+        new Vector3(centerX, centerY + ySpread, centerZ),
+        new Rotation(wobble - wobbleSpread, phase, 0),
+        (hue + 360 - hueSpread) % 360),
+      new Plane(
+        new Vector3(centerX, centerY, centerZ),
+        new Rotation(wobble, phase, 0),
+        hue),
+      new Plane(
+        new Vector3(centerX, centerY - ySpread, centerZ),
+        new Rotation(wobble + wobbleSpread, phase, 0),
+        (hue + 360 + hueSpread) % 360)
+    };
+
+    float thickness = (thicknessParameter.getValuef() * 25 + 1);
+    
+    Vector3 normalizedPoint = new Vector3();
+
+    for (Point p : model.points) {
+      if (random(1.0f) < derez) {
+        continue;
+      }
+      
+      int c = 0;
+      
+      for (Plane plane : planes) {
+        normalizedPoint.x = p.x - plane.center.x;
+        normalizedPoint.y = p.y - plane.center.y;
+        normalizedPoint.z = p.z - plane.center.z;
+        
+        float v = plane.rotation.rotatedY(normalizedPoint);
+        float d = abs(v);
+        
+        final int planeColor;
+        if (d <= thickness) {
+          planeColor = lx.hsb(plane.hue, saturation, 100);
+        } else if (d <= thickness * 2) {    
+          float value = 1 - ((d - thickness) / thickness);
+          planeColor = lx.hsb(plane.hue, saturation, value * 100);
+        } else {
+          planeColor = 0;
+        }
+
+        if (planeColor != 0) {
+          if (c == 0) {
+            c = planeColor; 
+          } else {
+            c = blendColor(c, planeColor, ADD);
+          }
+        }
+      }
+
+      colors[p.index] = c;
+    }
+  }
+}
+
+/**
+ * Two spinning wheels, basically XORed together, with a color palette that should
+ * be pretty easy to switch around.  Timed to the beat; also introduces "clickiness"
+ * which makes the movement non-linear throughout a given beat, giving it a nice
+ * dance feel.  I'm not 100% sure that it's actually going to look like it's _on_
+ * the beat, but that should be easy enough to adjust.
+ *
+ * It's particularly nice to turn down the clickiness and turn up derez during
+ * slow/beatless parts of the music and then revert them at the drop :)  But maybe
+ * I shouldn't be listening to so much shitty dubstep while making these...
+ */
+class TimPinwheels extends SCPattern { 
+  private BasicParameter horizSpreadParameter = new BasicParameter("HSpr", 0.75f);
+  private BasicParameter vertSpreadParameter = new BasicParameter("VSpr", 0.5f);
+  private BasicParameter vertOffsetParameter = new BasicParameter("VOff", 1.0f);
+  private BasicParameter zSlopeParameter = new BasicParameter("ZSlp", 0.6f);
+  private BasicParameter sharpnessParameter = new BasicParameter("Shrp", 0.25f);
+  private BasicParameter derezParameter = new BasicParameter("Drez", 0.25f);
+  private BasicParameter clickinessParameter = new BasicParameter("Clic", 0.5f);
+  private BasicParameter hueParameter = new BasicParameter("Hue", 0.667f);
+  private BasicParameter hueSpreadParameter = new BasicParameter("HSpd", 0.667f);
+
+  float phase = 0;
+  private final int NUM_BLADES = 12;
+  
+  class Pinwheel {
+    Vector2 center;
+    int numBlades;
+    float realPhase;
+    float phase;
+    float speed;
+    
+    Pinwheel(float xCenter, float yCenter, int numBlades, float speed) {
+      this.center = new Vector2(xCenter, yCenter);
+      this.numBlades = numBlades;
+      this.speed = speed;
+    }
+    
+    public void age(float numBeats) {
+      int numSteps = numBlades;
+      
+      realPhase = (realPhase + numBeats / numSteps) % 2.0f;
+      
+      float phaseStep = floor(realPhase * numSteps);
+      float phaseRamp = (realPhase * numSteps) % 1.0f;
+      phase = (phaseStep + pow(phaseRamp, (clickinessParameter.getValuef() * 10) + 1)) / (numSteps * 2);
+//      phase = (phase + deltaMs / 1000.0 * speed) % 1.0;      
+    }
+    
+    public boolean isOnBlade(float x, float y) {
+      x = x - center.x;
+      y = y - center.y;
+      
+      float normalizedAngle = (atan2(x, y) / (2 * PI) + 1 + phase) % 1;
+      float v = (normalizedAngle * 4 * numBlades);
+      int blade_num = floor((v + 2) / 4);
+      return (blade_num % 2) == 0;
+    }
+  }
+  
+  private final List<Pinwheel> pinwheels;
+  private final float[] values;
+  
+  TimPinwheels(GLucose glucose) {
+    super(glucose);
+    
+    addParameter(horizSpreadParameter);
+//    addParameter(vertSpreadParameter);
+    addParameter(vertOffsetParameter);
+    addParameter(zSlopeParameter);
+    addParameter(sharpnessParameter);
+    addParameter(derezParameter);
+    addParameter(clickinessParameter);
+    addParameter(hueParameter);
+    addParameter(hueSpreadParameter);
+    
+    pinwheels = new ArrayList();
+    pinwheels.add(new Pinwheel(0, 0, NUM_BLADES, 0.1f));
+    pinwheels.add(new Pinwheel(0, 0, NUM_BLADES, -0.1f));
+    
+    this.updateHorizSpread();
+    this.updateVertPositions();
+    
+    values = new float[model.points.size()];
+  }
+  
+  public void onParameterChanged(LXParameter parameter) {
+    if (parameter == horizSpreadParameter) {
+      updateHorizSpread();
+    } else if (parameter == vertSpreadParameter || parameter == vertOffsetParameter) {
+      updateVertPositions();
+    }
+  }
+  
+  private void updateHorizSpread() {
+    float xDist = model.xMax - model.xMin;
+    float xCenter = (model.xMin + model.xMax) / 2;
+    
+    float spread = horizSpreadParameter.getValuef() - 0.5f;
+    pinwheels.get(0).center.x = xCenter - xDist * spread;
+    pinwheels.get(1).center.x = xCenter + xDist * spread; 
+  }
+  
+  private void updateVertPositions() {
+    float yDist = model.yMax - model.yMin;
+    float yCenter = model.yMin + yDist * vertOffsetParameter.getValuef();
+
+    float spread = vertSpreadParameter.getValuef() - 0.5f;
+    pinwheels.get(0).center.y = yCenter - yDist * spread;
+    pinwheels.get(1).center.y = yCenter + yDist * spread;     
+  }
+  
+  private float prevRamp = 0;
+  
+  public void run(double deltaMs) {
+    float ramp = lx.tempo.rampf();
+    float numBeats = (1 + ramp - prevRamp) % 1;
+    prevRamp = ramp;
+    
+    float hue = hueParameter.getValuef() * 360;
+    // 0 -> -180
+    // 0.5 -> 0
+    // 1 -> 180
+    float hueSpread = (hueSpreadParameter.getValuef() - 0.5f) * 360;
+    
+    float fadeAmount = (float) (deltaMs / 1000.0f) * pow(sharpnessParameter.getValuef() * 10, 1);
+    
+    for (Pinwheel pw : pinwheels) {
+      pw.age(numBeats);
+    }
+    
+    float derez = derezParameter.getValuef();
+    
+    float zSlope = (zSlopeParameter.getValuef() - 0.5f) * 2;
+    
+    int i = -1;
+    for (Point p : model.points) {
+      ++i;
+      
+      int value = 0;
+      for (Pinwheel pw : pinwheels) {
+        value += (pw.isOnBlade(p.x, p.y - p.z * zSlope) ? 1 : 0);
+      }
+      if (value == 1) {
+        values[i] = 1;
+//        colors[p.index] = lx.hsb(120, 0, 100);
+      } else {
+        values[i] = max(0, values[i] - fadeAmount);
+        //color c = colors[p.index];
+        //colors[p.index] = lx.hsb(max(0, lx.h(c) - 10), min(100, lx.s(c) + 10), lx.b(c) - 5 );
+      }
+      
+      if (random(1.0f) >= derez) {
+        float v = values[i];
+        colors[p.index] = lx.hsb((360 + hue + pow(v, 2) * hueSpread) % 360, 30 + pow(1 - v, 0.25f) * 60, v * 100);
+      }      
+    }
+  }
+}
+
+/**
+ * This tries to figure out neighboring pixels from one cube to another to
+ * let you have a bunch of moving points tracing all over the structure.
+ * Adds a couple seconds of startup time to do the calculation, and in the
+ * end just comes out looking a lot like a screensaver.  Probably not worth
+ * it but there may be useful code here.
+ */
+class TimTrace extends SCPattern {
+  private Map<Point, List<Point>> pointToNeighbors;
+  private Map<Point, Strip> pointToStrip;
+  //  private final Map<Strip, List<Strip>> stripToNearbyStrips;
+  
+  int extraMs;
+  
+  class MovingPoint {
+    Point currentPoint;
+    float hue;
+    private Strip currentStrip;
+    private int currentStripIndex;
+    private int direction; // +1 or -1
+    
+    MovingPoint(Point p) {
+      this.setPointOnNewStrip(p);
+      hue = random(360);
+    }
+    
+    private void setPointOnNewStrip(Point p) {
+      this.currentPoint = p;
+      this.currentStrip = pointToStrip.get(p);
+      for (int i = 0; i < this.currentStrip.points.size(); ++i) {
+        if (this.currentStrip.points.get(i) == p) {
+          this.currentStripIndex = i;
+          break;
+        }
+      }
+      if (this.currentStripIndex == 0) {
+        // we are at the beginning of the strip; go forwards
+        this.direction = 1;
+      } else if (this.currentStripIndex == this.currentStrip.points.size()) {
+        // we are at the end of the strip; go backwards
+        this.direction = -1;
+      } else {
+        // we are in the middle of a strip; randomly go one way or another
+        this.direction = ((random(1.0f) < 0.5f) ? -1 : 1);
+      }
+    }
+    
+    public void step() {
+      List<Point> neighborsOnOtherStrips = pointToNeighbors.get(this.currentPoint);
+
+      Point nextPointOnCurrentStrip = null;      
+      this.currentStripIndex += this.direction;
+      if (this.currentStripIndex >= 0 && this.currentStripIndex < this.currentStrip.points.size()) {
+        nextPointOnCurrentStrip = this.currentStrip.points.get(this.currentStripIndex);
+      }
+      
+      // pick which option to take; if we can keep going on the current strip then
+      // add that as another option
+      int option = floor(random(neighborsOnOtherStrips.size() + (nextPointOnCurrentStrip == null ? 0 : 100)));
+      
+      if (option < neighborsOnOtherStrips.size()) {
+        this.setPointOnNewStrip(neighborsOnOtherStrips.get(option));
+      } else {
+        this.currentPoint = nextPointOnCurrentStrip;
+      }
+    }
+  }
+  
+  List<MovingPoint> movingPoints;
+  
+  TimTrace(GLucose glucose) {
+    super(glucose);
+    
+    extraMs = 0;
+    
+    pointToNeighbors = this.buildPointToNeighborsMap();
+    pointToStrip = this.buildPointToStripMap();
+    
+    int numMovingPoints = 1000;
+    movingPoints = new ArrayList();
+    for (int i = 0; i < numMovingPoints; ++i) {
+      movingPoints.add(new MovingPoint(model.points.get(floor(random(model.points.size())))));
+    }
+    
+  }
+  
+  private Map<Strip, List<Strip>> buildStripToNearbyStripsMap() {
+    Map<Strip, Vector3> stripToCenter = new HashMap();
+    for (Strip s : model.strips) {
+      Vector3 v = new Vector3();
+      for (Point p : s.points) {
+        v.add(p.x, p.y, p.z);
+      }
+      v.divide(s.points.size());
+      stripToCenter.put(s, v);
+    }
+    
+    Map<Strip, List<Strip>> stripToNeighbors = new HashMap();
+    for (Strip s : model.strips) {
+      List<Strip> neighbors = new ArrayList();
+      Vector3 sCenter = stripToCenter.get(s);
+      for (Strip potentialNeighbor : model.strips) {
+        if (s != potentialNeighbor) {
+          float distance = sCenter.distanceTo(stripToCenter.get(potentialNeighbor));
+          if (distance < 25) {
+            neighbors.add(potentialNeighbor);
+          }
+        }
+      }
+      stripToNeighbors.put(s, neighbors);
+    }
+    
+    return stripToNeighbors;
+  }
+  
+  private Map<Point, List<Point>> buildPointToNeighborsMap() {
+    Map<Point, List<Point>> m = new HashMap();
+    Map<Strip, List<Strip>> stripToNearbyStrips = this.buildStripToNearbyStripsMap();
+    
+    for (Strip s : model.strips) {
+      List<Strip> nearbyStrips = stripToNearbyStrips.get(s);
+      
+      for (Point p : s.points) {
+        Vector3 v = new Vector3(p.x, p.y, p.z);
+        
+        List<Point> neighbors = new ArrayList();
+        
+        for (Strip nearbyStrip : nearbyStrips) {
+          Point closestPoint = null;
+          float closestPointDistance = 100000;
+          
+          for (Point nsp : nearbyStrip.points) {
+            float distance = v.distanceTo(nsp.x, nsp.y, nsp.z);
+            if (closestPoint == null || distance < closestPointDistance) {
+              closestPoint = nsp;
+              closestPointDistance = distance;
+            }
+          }
+          
+          if (closestPointDistance < 15) {
+            neighbors.add(closestPoint);
+          }
+        }
+        
+        m.put(p, neighbors);
+      }
+    }
+    
+    return m;
+  }
+  
+  private Map<Point, Strip> buildPointToStripMap() {
+    Map<Point, Strip> m = new HashMap();
+    for (Strip s : model.strips) {
+      for (Point p : s.points) {
+        m.put(p, s);
+      }
+    }
+    return m;
+  }
+  
+  public void run(double deltaMs) {
+    for (Point p : model.points) {
+      int c = colors[p.index];
+      colors[p.index] = lx.hsb(lx.h(c), lx.s(c), lx.b(c) - 3);
+    }
+    
+    for (MovingPoint mp : movingPoints) {
+      mp.step();
+      colors[mp.currentPoint.index] = blendColor(colors[mp.currentPoint.index], lx.hsb(mp.hue, 10, 100), ADD);
+    }
+  }
+}
+class GlitchPlasma extends SCPattern {
+  private int pos = 0;
+  private float satu = 100;
+  private float speed = 1;
+  private float glitch = 0;
+  BasicParameter saturationParameter = new BasicParameter("SATU", 1.0f);
+  BasicParameter speedParameter = new BasicParameter("SPEED", 0.1f);
+  BasicParameter glitchParameter = new BasicParameter("GLITCH", 0.0f);
+  
+  public GlitchPlasma(GLucose glucose) {
+    super(glucose);
+    addParameter(saturationParameter);
+    addParameter(speedParameter);
+    addParameter(glitchParameter);
+  }
+  public void onParameterChanged(LXParameter parameter) {
+    if (parameter == saturationParameter) {
+      satu = 100*parameter.getValuef();
+    } else if (parameter == speedParameter) {
+      speed = 8*parameter.getValuef();
+    } else if (parameter == glitchParameter) {
+      glitch = parameter.getValuef();
+    }
+  }
+
+  public void run(double deltaMs) {
+    for (Point p : model.points) {
+      float hv = sin(dist(p.x + pos, p.y, 128.0f, 128.0f) / 8.0f)
+         + sin(dist(p.x, p.y, 64.0f, 64.0f) / 8.0f)
+         + sin(dist(p.x, p.y + pos / 7, 192.0f, 64.0f) / 7.0f)
+         + sin(dist(p.x, p.z + pos, 192.0f, 100.0f) / 8.0f);
+      float bv = 100;
+      colors[p.index] = lx.hsb((hv+2)*50, satu, bv);
+    }
+    if (random(1.0f)<glitch/20) {
+      pos=pos-PApplet.parseInt(random(10,30));
+    }
+    pos+=speed;
+    if (pos >= MAX_INT-1) pos=0;    
+  }
+}
+
+// This is very much a work in progress. Trying to get a flame effect.
+class FireEffect extends SCPattern {
+  private float[][] intensity;
+  private float hotspot;
+  private float decay = 0.3f;
+  private int xm;
+  private int ym;
+  BasicParameter decayParameter = new BasicParameter("DECAY", 0.3f);
+  
+  public FireEffect(GLucose glucose) {
+    super(glucose);
+    xm = PApplet.parseInt(model.xMax);
+    ym = PApplet.parseInt(model.yMax);
+    
+    intensity = new float[xm][ym];
+    addParameter(decayParameter);
+  }
+  public void onParameterChanged(LXParameter parameter) {
+    if (parameter == decayParameter) {
+      decay = parameter.getValuef();
+    }
+  } 
+  private int flameColor(float level) {
+    if (level<=0) return lx.hsb(0,0,0);
+    float br=min(100,sqrt(level)*15);
+    return lx.hsb(level/1.7f,100,br);
+  }
+  public void run(double deltaMs) {
+    for (int x=10;x<xm-10;x++) {
+        if (x%50>45 || x%50<5) {
+          intensity[x][ym-1] = random(30,100);
+        } else {
+          intensity[x][ym-1] = random(0,50);
+        }
+    }
+    for (int x=1;x<xm-1;x++) {
+      for (int y=0;y<ym-1;y++) {        
+        intensity[x][y] = (intensity[x-1][y+1]+intensity[x][y+1]+intensity[x+1][y+1])/3-decay;
+      }
+    }
+    
+    for (Point p : model.points) {
+      int x = max(0,(PApplet.parseInt(p.x)+PApplet.parseInt(p.z))%xm);
+      int y = constrain(ym-PApplet.parseInt(p.y),0,ym-1);
+      colors[p.index] = flameColor(intensity[x][y]);
+    }
+  }
+}
+
+class StripBounce extends SCPattern {
+  private final int numOsc = 30;
+  SinLFO[] fX = new SinLFO[numOsc]; //new SinLFO(0, model.xMax, 5000);
+  SinLFO[] fY = new SinLFO[numOsc]; //new SinLFO(0, model.yMax, 4000);
+  SinLFO[] fZ = new SinLFO[numOsc]; //new SinLFO(0, model.yMax, 3000);
+  SinLFO[] sat = new SinLFO[numOsc];
+  float[] colorOffset = new float[numOsc];
+  
+  public StripBounce(GLucose glucose) {
+    super(glucose);
+    for (int i=0;i<numOsc;i++) {
+      fX[i] = new SinLFO(0, model.xMax, random(2000,20000)); 
+      fY[i] = new SinLFO(0, model.yMax, random(2000,20000)); 
+      fZ[i] = new SinLFO(0, model.zMax, random(2000,20000)); 
+      sat[i] = new SinLFO(60, 100, random(2000,50000)); 
+      addModulator(fX[i]).trigger();      
+      addModulator(fY[i]).trigger();
+      addModulator(fZ[i]).trigger();
+      colorOffset[i]=random(0,256);
+    }
+  }
+  
+  public void run(double deltaMs) {
+    float[] bright = new float[model.points.size()];
+    for (Strip strip : model.strips) {
+      for (int i=0;i<numOsc;i++) {
+        float avgdist=0.0f;
+        avgdist = dist(strip.points.get(8).x,strip.points.get(8).y,strip.points.get(8).z,fX[i].getValuef(),fY[i].getValuef(),fZ[i].getValuef());
+        boolean on = avgdist<30;
+        float hv = (lx.getBaseHuef()+colorOffset[i])%360;
+        float br = max(0,100-avgdist*4);
+        for (Point p : strip.points) {
+          if (on && br>bright[p.index]) {
+            colors[p.index] = lx.hsb(hv,sat[i].getValuef(),br);
+            bright[p.index] = br;
+          }
+        }
+      }
+    }
+  }
+}
+
+class SoundRain extends SCPattern {
+
+  private FFT fft = null; 
+  private LinearEnvelope[] bandVals = null;
+  private float[] lightVals = null;
+  private int avgSize;
+  private float gain = 25;
+  SawLFO pos = new SawLFO(0, 9, 8000);
+  SinLFO col1 = new SinLFO(0, model.xMax, 5000);
+  BasicParameter gainParameter = new BasicParameter("GAIN", 0.5f);
+  
+  public SoundRain(GLucose glucose) {
+    super(glucose);
+    addModulator(pos).trigger();
+    addModulator(col1).trigger();
+    addParameter(gainParameter);
+  }
+
+  public void onParameterChanged(LXParameter parameter) {
+    if (parameter == gainParameter) {
+      gain = 50*parameter.getValuef();
+    }
+  }
+  protected void onActive() {
+    if (this.fft == null) {
+      this.fft = new FFT(lx.audioInput().bufferSize(), lx.audioInput().sampleRate());
+      this.fft.window(FFT.HAMMING);
+      this.fft.logAverages(40, 1);
+      this.avgSize = this.fft.avgSize();
+      this.bandVals = new LinearEnvelope[this.avgSize];
+      for (int i = 0; i < this.bandVals.length; ++i) {
+        this.addModulator(this.bandVals[i] = (new LinearEnvelope(0, 0, 700+i*4))).trigger();
+      }
+      lightVals = new float[avgSize];
+    }
+  }
+  
+  public void run(double deltaMs) {
+    this.fft.forward(this.lx.audioInput().mix);
+    for (int i = 0; i < avgSize; ++i) {
+      float value = this.fft.getAvg(i);
+      this.bandVals[i].setEndVal(value,40).trigger();
+      float lv = min(value*gain,100);
+      if (lv>lightVals[i]) {
+        lightVals[i]=min(lightVals[i]+15,lv,100);
+      } else {
+        lightVals[i]=max(lv,lightVals[i]-5,0);
+      }
+    }
+    for (Cube c : model.cubes) {
+      for (int j=0; j<c.strips.size(); j++) {
+        Strip s = c.strips.get(j);
+        if (j%4!=0 && j%4!=2) {
+          for (Point p : s.points) {
+            int seq = PApplet.parseInt(p.y*avgSize/model.yMax+pos.getValuef()+sin(p.x+p.z)*2)%avgSize;
+            seq=min(abs(seq-(avgSize/2)),avgSize-1);
+            colors[p.index] = lx.hsb(200,max(0,100-abs(p.x-col1.getValuef())/2),lightVals[seq]);
+          }
+        }
+      }
+    }
+  }  
+}
+
+class FaceSync extends SCPattern {
+  SinLFO xosc = new SinLFO(-10, 10, 3000);
+  SinLFO zosc = new SinLFO(-10, 10, 3000);
+  SinLFO col1 = new SinLFO(0, model.xMax, 5000);
+  SinLFO col2 = new SinLFO(0, model.xMax, 4000);
+
+  public FaceSync(GLucose glucose) {
+    super(glucose);
+    addModulator(xosc).trigger();
+    addModulator(zosc).trigger();
+    zosc.setValue(0);
+    addModulator(col1).trigger();
+    addModulator(col2).trigger();    
+    col2.setValue(model.xMax);
+  }
+
+  public void run(double deltaMs) {
+    int i=0;
+    for (Strip s : model.strips) {
+      i++;
+      for (Point p : s.points) {
+        float dx, dz;
+        if (i%32 < 16) {
+          dx = p.x - (s.cx+xosc.getValuef());
+          dz = p.z - (s.cz+zosc.getValuef());
+        } else {
+          dx = p.x - (s.cx+zosc.getValuef());
+          dz = p.z - (s.cz+xosc.getValuef());
+        }                
+        //println(dx);
+        float a1=max(0,100-abs(p.x-col1.getValuef()));
+        float a2=max(0,100-abs(p.x-col2.getValuef()));        
+        float sat = max(a1,a2);
+        float h = (359*a1+200*a2) / (a1+a2);
+        colors[p.index] = lx.hsb(h,sat,100-abs(dx*5)-abs(dz*5));
+      }
+    }
+  }
+}
+
+class SoundSpikes extends SCPattern {
+  private FFT fft = null; 
+  private LinearEnvelope[] bandVals = null;
+  private float[] lightVals = null;
+  private int avgSize;
+  private float gain = 25;
+  BasicParameter gainParameter = new BasicParameter("GAIN", 0.5f);
+  SawLFO pos = new SawLFO(0, model.xMax, 8000);
+
+  public SoundSpikes(GLucose glucose) {
+    super(glucose);
+    addParameter(gainParameter);
+    addModulator(pos).trigger();
+  }
+
+  public void onParameterChanged(LXParameter parameter) {
+    if (parameter == gainParameter) {
+      gain = 50*parameter.getValuef();
+    }
+  }
+  protected void onActive() {
+    if (this.fft == null) {
+      this.fft = new FFT(lx.audioInput().bufferSize(), lx.audioInput().sampleRate());
+      this.fft.window(FFT.HAMMING);
+      this.fft.logAverages(40, 1);
+      this.avgSize = this.fft.avgSize();
+      this.bandVals = new LinearEnvelope[this.avgSize];
+      for (int i = 0; i < this.bandVals.length; ++i) {
+        this.addModulator(this.bandVals[i] = (new LinearEnvelope(0, 0, 700+i*4))).trigger();
+      }
+      lightVals = new float[avgSize];
+    }
+  }
+  
+  public void run(double deltaMs) {
+    this.fft.forward(this.lx.audioInput().mix);
+    for (int i = 0; i < avgSize; ++i) {
+      float value = this.fft.getAvg(i);
+      this.bandVals[i].setEndVal(value,40).trigger();
+      float lv = min(value*gain,model.yMax+10);
+      if (lv>lightVals[i]) {
+        lightVals[i]=min(lightVals[i]+30,lv,model.yMax+10);
+      } else {
+        lightVals[i]=max(lv,lightVals[i]-10,0);
+      }
+    }
+    int i = 0;
+    for (Cube c : model.cubes) {
+      for (int j=0; j<c.strips.size(); j++) {
+        Strip s = c.strips.get(j);
+        if (j%4!=0 && j%4!=2) {
+          for (Point p : s.points) {
+            float dis = (abs(p.x-model.xMax/2)+pos.getValuef())%model.xMax/2;
+            int seq = PApplet.parseInt((dis*avgSize*2)/model.xMax);
+            if (seq>avgSize) seq=avgSize-seq;
+            seq=constrain(seq,0,avgSize-1);
+            float br=max(0, lightVals[seq]-p.y);
+            colors[p.index] = lx.hsb((dis*avgSize*65)/model.xMax,90,br);
+          }
+        }
+      }
+    }
+  }  
+}
+
+  static public void main(String[] passedArgs) {
+    String[] appletArgs = new String[] { "SugarCubes" };
+    if (passedArgs != null) {
+      PApplet.main(concat(appletArgs, passedArgs));
+    } else {
+      PApplet.main(appletArgs);
+    }
+  }
+}