Committing my visualizers. TimPlanes is by far the
authortim <tim@asana.com>
Mon, 19 Aug 2013 00:11:38 +0000 (17:11 -0700)
committertim <tim@asana.com>
Mon, 19 Aug 2013 02:57:10 +0000 (19:57 -0700)
best one but the others may have some usefulness as
well.

SugarCubes.pde
Tim.pde [new file with mode: 0644]

index 0f102e334f5acaa688743c95eb1adec344aeae7a..615b3bd86c7da4b14620ee77ee1e601ea27138c1 100644 (file)
@@ -43,7 +43,14 @@ LXPattern[] patterns(GLucose glucose) {
     new SoundRain(glucose),
     new SoundSpikes(glucose),
     new FaceSync(glucose),
-    
+
+    new TimPlanes(glucose),
+    new TimPinwheels(glucose),
+    new TimRaindrops(glucose),
+    new TimCubes(glucose),
+//    new TimTrace(glucose),
+    new TimSpheres(glucose),
+
     // Basic test patterns for reference, not art    
     new TestCubePattern(glucose),
     new TestTowerPattern(glucose),
diff --git a/Tim.pde b/Tim.pde
new file mode 100644 (file)
index 0000000..acd4125
--- /dev/null
+++ b/Tim.pde
@@ -0,0 +1,735 @@
+/**
+ * Not very flushed out, but kind of fun nonetheless.
+ */
+class TimSpheres extends SCPattern {
+  private BasicParameter hueParameter = new BasicParameter("RAD", 1.0);
+  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.33;
+    spheres[1].radius = 50;
+  }
+  
+  public void run(int 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;
+
+      color c = color(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, color(((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;
+  }
+  
+  float distanceTo(float x, float y) {
+    return sqrt(pow(x - this.x, 2) + pow(y - this.y, 2));
+  }
+  
+  float distanceTo(Vector2 v) {
+    return distanceTo(v.x, v.y);
+  }
+  
+  Vector2 plus(float x, float y) {
+    return new Vector2(this.x + x, this.y + y);
+  }
+  
+  Vector2 plus(Vector2 v) {
+    return plus(v.x, v.y);
+  }
+    
+  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;
+  }
+  
+  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));
+  }
+  
+  float distanceTo(Vector3 v) {
+    return distanceTo(v.x, v.y, v.z);
+  }
+  
+  float distanceTo(Point p) {
+    return distanceTo(p.fx, p.fy, p.fz);
+  }
+  
+  void add(Vector3 other, float multiplier) {
+    this.add(other.x * multiplier, other.y * multiplier, other.z * multiplier);
+  }  
+    
+  void add(float x, float y, float z) {
+    this.x += x;
+    this.y += y;
+    this.z += z;
+  }
+  
+  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;
+  }
+  
+  Vector3 rotated(Vector3 v) {
+    return new Vector3(
+      rotatedX(v),
+      rotatedY(v),
+      rotatedZ(v));
+
+  }
+  
+  float rotatedX(Vector3 v) {
+    return a * v.x + b * v.y + c * v.z;
+  }
+  
+  float rotatedY(Vector3 v) {
+    return d * v.x + e * v.y + f * v.z;
+  }
+  
+  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 {
+  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
+    boolean age(int ms) {
+      p.add(v, ms / 1000.0);
+      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(int deltaMs) {
+    leftoverMs += deltaMs;
+    while (leftoverMs > msPerRaindrop) {
+      leftoverMs -= msPerRaindrop;
+      raindrops.add(new Raindrop());
+    }
+    
+    for (Point p : model.points) {
+      color c = 
+        blendColor(
+          color(210, 20, (float)Math.max(0, 1 - Math.pow((model.yMax - p.fy) / 10, 2)) * 50),
+          color(220, 60, (float)Math.max(0, 1 - Math.pow((p.fy - model.yMin) / 10, 2)) * 100),
+          ADD);
+      for (Raindrop raindrop : raindrops) {
+        if (p.fx >= (raindrop.p.x - raindrop.radius) && p.fx <= (raindrop.p.x + raindrop.radius) &&
+            p.fy >= (raindrop.p.y - raindrop.radius) && p.fy <= (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, color(raindrop.hue, 80, (float)Math.pow(1 - d, 0.01) * 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.125);
+  private BasicParameter attackParameter = new BasicParameter("ATTK", 0.5);
+  private BasicParameter decayParameter = new BasicParameter("DECAY", 0.5);
+  private BasicParameter hueParameter = new BasicParameter("HUE", 0.5);
+  private BasicParameter hueVarianceParameter = new BasicParameter("H.V.", 0.25);
+  private BasicParameter saturationParameter = new BasicParameter("SAT", 0.5);
+  
+  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.999);
+      hasPeaked = infiniteAttack;
+      value = (infiniteAttack ? 1 : 0);
+    }
+    
+    // returns TRUE if this should die
+    boolean age(int ms) {
+      if (!hasPeaked) {
+        value = value + (ms / 1000.0f * ((attackParameter.getValuef() + 0.01) * 5));
+        if (value >= 1.0) {
+          value = 1.0;
+          hasPeaked = true;
+        }
+        return false;
+      } else {
+        value = value - (ms / 1000.0f * ((decayParameter.getValuef() + 0.01) * 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(int deltaMs) {
+    leftoverMs += deltaMs;
+    float msPerFlash = 1000 / ((rateParameter.getValuef() + .01) * 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.0;
+      color c = color(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.2);
+  private BasicParameter wobbleSpreadParameter = new BasicParameter("WSpr", 0.25);
+  private BasicParameter wobbleSpeedParameter = new BasicParameter("WSpd", 0.375);
+  private BasicParameter wobbleOffsetParameter = new BasicParameter("WOff", 0);
+  private BasicParameter derezParameter = new BasicParameter("Drez", 0.5);
+  private BasicParameter thicknessParameter = new BasicParameter("Thick", 0.4);
+  private BasicParameter ySpreadParameter = new BasicParameter("ySpr", 0.2);
+  private BasicParameter hueParameter = new BasicParameter("Hue", 0.75);
+  private BasicParameter hueSpreadParameter = new BasicParameter("HSpr", 0.68);
+
+  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);
+  }
+  
+  color getColor(Vector3 normalizedPoint, float hue, Rotation rotation, float saturation) {
+    float t = (thicknessParameter.getValuef() * 25 + 1);
+    
+    float v = rotation.rotatedY(normalizedPoint);
+    float d = abs(v);
+    
+    if (d <= t) {
+      return color(hue, saturation, 100);
+    } else if (d <= t * 2) {    
+      float value = 1 - ((d - t) / t);
+      return color(hue, saturation, value * 100);
+    } else {
+      return 0;
+    }
+  }
+  
+  int beat = 0;
+  float prevRamp = 0;
+  float[] wobbleSpeeds = { 1.0/8, 1.0/4, 1.0/2, 1.0 };
+  
+  public void run(int 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.9999)];
+
+    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.5) * 360;
+
+    float saturation = 10 + 60.0 * pow(ramp, 0.25);
+    
+    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)
+    };
+    
+    for (Point p : model.points) {
+      if (random(1.0) < derez) {
+        continue;
+      }
+      
+      color c = 0;
+      
+      for (Plane plane : planes) {
+        Vector3 normalizedPoint = new Vector3(p.fx - plane.center.x, p.fy - plane.center.y, p.fz - plane.center.z);
+        color planeColor = getColor(normalizedPoint, plane.hue, plane.rotation, saturation);
+        if (planeColor != 0) {
+          if (c == 0) {
+            c = planeColor; 
+          } else {
+            c = blendColor(c, planeColor, ADD);
+          }
+        }
+      }
+
+      colors[p.index] = c;
+    }
+  }
+}
+
+/**
+ * Not very flushed out but pretty.
+ */
+class TimPinwheels extends SCPattern { 
+  float phase = 0;
+  private final int NUM_BLADES = 16;
+  
+  class Pinwheel {
+    Vector2 center;
+    int numBlades;
+    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;
+    }
+    
+    void age(int deltaMs) {
+      phase = (phase + deltaMs / 1000.0 * speed) % 1.0;      
+    }
+    
+    boolean isOnBlade(float x, float y) {
+      x = x - center.x;
+      y = y - center.y;
+      
+      float normalizedAngle = (atan2(x, y) / (2 * PI) + 1.5 + phase) % 1;
+      float v = (normalizedAngle * 4 * numBlades);
+      int blade_num = floor((v + 2) / 4);
+      return (blade_num % 2) == 0;
+    }
+  }
+  
+  private final List<Pinwheel> pinwheels;
+  
+  TimPinwheels(GLucose glucose) {
+    super(glucose);
+    
+    float xDist = model.xMax - model.xMin;
+    float xCenter = (model.xMin + model.xMax) / 2;
+    float yCenter = (model.yMin + model.yMax) / 2;
+
+    pinwheels = new ArrayList();
+    pinwheels.add(new Pinwheel(xCenter - xDist * 0.4, yCenter, NUM_BLADES, 0.1));
+    pinwheels.add(new Pinwheel(xCenter + xDist * 0.4, yCenter, NUM_BLADES, -0.1));
+  }
+  
+  public void run(int deltaMs) {
+    for (Pinwheel pw : pinwheels) {
+      pw.age(deltaMs);
+    }
+    
+    for (Point p : model.points) {
+      int value = 0;
+      for (Pinwheel pw : pinwheels) {
+        value += (pw.isOnBlade(p.fx, p.fy) ? 1 : 0);
+      }
+      if (value == 1) {
+        colors[p.index] = color(120, 0, 100);
+      } else {
+        color c = colors[p.index];
+        colors[p.index] = color(max(0, hue(c) - 10), min(100, saturation(c) + 10), brightness(c) - 5 );
+      }
+    }
+  }
+}
+
+/**
+ * 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.0) < 0.5) ? -1 : 1);
+      }
+    }
+    
+    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.fx, p.fy, p.fz);
+      }
+      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.fx, p.fy, p.fz);
+        
+        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.fx, nsp.fy, nsp.fz);
+            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(int deltaMs) {
+    for (Point p : model.points) {
+      color c = colors[p.index];
+      colors[p.index] = color(hue(c), saturation(c), brightness(c) - 3);
+    }
+    
+    for (MovingPoint mp : movingPoints) {
+      mp.step();
+      colors[mp.currentPoint.index] = blendColor(colors[mp.currentPoint.index], color(mp.hue, 10, 100), ADD);
+    }
+  }
+}