> show canvas only <


/* built with Studio Sketchpad: 
 *   https://sketchpad.cc
 * 
 * observe the evolution of this sketch: 
 *   https://studio.sketchpad.cc/sp/pad/view/ro.y98XGTCN6ux/rev.3169
 * 
 * authors: 
 *   Charles Dietrich

 * license (unless otherwise specified): 
 *   creative commons attribution-share alike 3.0 license.
 *   https://creativecommons.org/licenses/by-sa/3.0/ 
 */ 



/*
This is intended to recreate the Synchronous Objects [1] Counterpoint Tool [2] by Benjamin Schroeder [3]. Schroeder's tool is based on the concept of musical counterpoint [4] but adapts it to a visual system. The tool is available at a Processing applet but the source code is not available to my knowledge (see discussion at [5], note that the blog post is dated April 2009).
 
 My intention is to:
 1) recreate aspects of the Schroeder's tool here in Processing.js, in order to understand the scope of the coding and algorithms required
 2) implement the tool as a distributed robotic system in simulation
 3) implement the tool as a distributed robotic system
 
 The robotic system will be Honeycomb, developed by the Correll Lab at UC-Boulder [6] and used in Michael Theodore's Field Theory Exhibition [7]. I intend to investigate if and how MIT Proto [8] can be used to implement the tool.
 
 ## Code (Current Implementation)
 
 Note: there is limited functionality.
 
 There is a global stream of hand positions (CStream). Each "clock" keeps an internal pointer to CStream. When a clock needs a new value from CStream, it pulls the value after its current pointer. The clock decides how long it will take to get to that new value (from [5]: quarter, half, dotted quarter, whole). The code then interpolates positions between the old value and the new value to create smooth motion. Note that a clock's pointer may be arbitrarily far behind the maximum pointer of all clocks.
 
 [1] http://synchronousobjects.osu.edu/
 [2] http://synchronousobjects.osu.edu/tools/counterpointTool.html
 [3] http://www.cse.ohio-state.edu/~schroebe/
 [4] http://en.wikipedia.org/wiki/Counterpoint
 [5] http://synchronousobjects.osu.edu/blog/2009/04/benjamin-schroeder-on-the-counterpoint-tool/
 [6] http://correll.cs.colorado.edu/
 [7] http://correll.cs.colorado.edu/?p=2288
 [8] http://proto.bbn.com/
 */

// http://www.jfhudson.com/poem.html
String[] QUOTE = {"the having", 
"patterns and routes, breaking", 
"from them to explore other patterns or",
"better ways to routes, and then the",
"return"};
String AUTHOR = "A. R. Ammons";
String POEM ="Easter Morning";

int W = 720;
int H = 360;

int TEXT_X = W * 1/20;
int TEXT_Y = H;
int LINE_H = 15;

// grid drawing
int GRID_DOT_FILL = 196;
int H_SCALE = W/7;
int V_SCALE = H/4;
int GRID_DOT_R = 5;

// various drawing constants (could be in Clock)
int R0 = 3;
int R1 = 6;
int R2 = 35;
int R3 = 37;
int SR = 2;
float ARC_W = 10 * 2 * PI / 360;
int ARM_FILL = 0;
int SR_FILL = 128;

int[] EIGHTHS = {
  2, 3, 4, 8
};

// how long in frames is an eighth
// bpm (@ fps 30, 4/4 time): ~ 60 (would be 60 @ EIGHTHS_LENGTH = 15)
int EIGHTHS_LENGTH = 16;

// a set of clocks
ArrayList<Clock> clocks = new ArrayList<Clock>();

// a stream of clock hand positions
CStream c_stream = new CStream();

// graph
int GRAPH_X = 0;
int GRAPH_Y = H;
int GRAPH_W = W;
int GRAPH_H = H/2;
int FRAME_COUNT_INCR = 20;
int STREAM_POS_INCR = 20;

void setup() {
  size(720, 520); 
  smooth();
  clocks.add(new Clock(W/2 - 50, H/2, 0, 3, 6, 255, 0, 0));
  clocks.add(new Clock(W/2 + 50, H/2, 0, 3, 6, 0, 255, 0));
}

int fracMap(boolean behind, float r) {
   if(behind) {
    r = max(r-0.20, 0);
   } 
   return int(r*4);
}

void draw() {
  background(255);
  fill(GRID_DOT_FILL);
  noStroke();
  for (int i = H_SCALE/2; i < W; i+=H_SCALE) {
    for (int j = V_SCALE/2; j < H; j+=V_SCALE) {
      ellipse(i, j, GRID_DOT_R, GRID_DOT_R);
    }
  }
  
  // find max frameCount, streamPos
  int max_frame_count = 0;
  int max_stream_pos = 0;
  for (Clock c: clocks) {
    FrameCountToStreamPos maxFrameCountStreamPos = c.getMaxFrameCountStreamPos();
    max_frame_count = max(max_frame_count, maxFrameCountStreamPos.frame_count);
    max_stream_pos = max(max_stream_pos, maxFrameCountStreamPos.stream_pos);
  }

  // draw clocks
  for (Clock c : clocks) {
    if (c.getAt() == frameCount) {
      Hands hands = c_stream.getCs(c.getStreamPos());
      
      int eighths = EIGHTHS[fracMap(c.getStreamPos() < (max_stream_pos + 1), random(1))];
      c.updateAt(frameCount + EIGHTHS_LENGTH * eighths, 
      c.getX(), c.getY(), 
      c.getC(0) + hands.cs[0], 
      c.getC(1) + hands.cs[1], 
      c.getC(2) + hands.cs[2]);
    }
    c.draw();
  }

  // draw frameCount to streamPos
  
  int graph_max_frame_count = (max_frame_count+FRAME_COUNT_INCR) / FRAME_COUNT_INCR * FRAME_COUNT_INCR;
  int graph_max_stream_pos = (max_stream_pos+STREAM_POS_INCR)/ STREAM_POS_INCR * STREAM_POS_INCR;

  // draw frameCount (x axis) to streamPos (y axis)
  for (Clock c: clocks) {
    fill(c.getColor()[0], c.getColor()[1], c.getColor()[2], 128);
    int prev_x = GRAPH_X;
    int prev_y = GRAPH_Y + GRAPH_H;
    for (FrameCountToStreamPos frameCountToStreamPos: c.frameCountToStreamPosArr) {
      int x = GRAPH_X + GRAPH_W * frameCountToStreamPos.frame_count / graph_max_frame_count;
      int y = GRAPH_Y + GRAPH_H - GRAPH_H * frameCountToStreamPos.stream_pos / graph_max_stream_pos;
      triangle(prev_x, prev_y, x, prev_y, x, y);
      prev_x = x;
      prev_y = y;
    }
  }
  
  // text
  fill(255);
  rect(TEXT_X, TEXT_Y, W-TEXT_X, H-TEXT_Y);
  fill(0, 128);
  int i = 0;
  for(String s: QUOTE) {
    text(s, TEXT_X, TEXT_Y + LINE_H * i);
    i++;
  }
  text("   - " + AUTHOR + ", " + POEM, TEXT_X, TEXT_Y + LINE_H * i);
}

// represents the clock hands or a delta
class Hands {
  int[] cs = new int[3];

  Hands(int c0, int c1, int c2) {
    cs[0] = c0;
    cs[1] = c1;
    cs[2] = c2;
  }
}

/**
 * A stream of deltas
 */
class CStream {
  ArrayList<Hands> c_stream = new ArrayList<Hands>();

  Hands getCs(int idx) {
    if (idx >= c_stream.size()) {
      c_stream.add(
      new Hands(int(random(12))-6, int(random(12))-6, int(random(12))-6));
    }
    return c_stream.get(idx);
  }
}

// represents the position of a clock, used in buffers
class ClockPos {
  float x, y;
  int[] c = new int[3];

  void update(float x, float y, int c0, int c1, int c2) {
    this.x = x;
    this.y = y;
    c[0] = c0;
    c[1] = c1;
    c[2] = c2;
  }
}

// a tuple of (frameCount, streamPos)
class FrameCountToStreamPos {
  int frame_count;
  int stream_pos;

  FrameCountToStreamPos(int frame_count, int stream_pos) {
    this.frame_count = frame_count;
    this.stream_pos = stream_pos;
  }
}

// a clock. There is a front and back buffer.
class Clock {
  // color
  int[] col = new int[3];
  // stream pos
  int stream_pos = 0;
  // frameCount to streamPos list
  ArrayList<FrameCountToStreamPos> frameCountToStreamPosArr
    = new ArrayList<FrameCountToStreamPos>();

  // double buffer pointer
  int buf = 1;
  // clock position double buffer
  ClockPos[] cps = new ClockPos[2];
  // update time double buffer
  int[] ts = new int[2];

  Clock(float x, float y, int c0, int c1, int c2, int r, int g, int b) {
    for (int i = 0; i < 2; i++) {
      cps[i] = new ClockPos();
      cps[i].update(x, y, c0, c1, c2);
    }
    // last update was at 0, next update at 1
    ts[0] = 0;
    ts[1] = 1;
    col[0] = r;
    col[1] = g;
    col[2] = b;
    frameCountToStreamPosArr.add(new FrameCountToStreamPos(0,0));
  }

  int[] getColor() {
    return col;
  }
  int getStreamPos() {
    return stream_pos;
  }

  int getAt() {
    return ts[buf];
  }

  float getX() {
    return cps[buf].x;
  }

  float getY() {
    return cps[buf].y;
  }

  int getC(int idx) {
    return cps[buf].c[idx];
  }

  FrameCountToStreamPos getMaxFrameCountStreamPos() {
    return frameCountToStreamPosArr.get(frameCountToStreamPosArr.size()-1);
  }

  void updateAt(int frame, float x, float y, int c0, int c1, int c2) {
    frameCountToStreamPosArr.add(new FrameCountToStreamPos(frame, stream_pos));
    stream_pos++;
    buf = 1 - buf;
    ts[buf] = frame;
    cps[buf].update(x, y, c0, c1, c2);
  }

  float interp(float old_c, float new_c) {
    return old_c + (new_c - old_c) * 
      (frameCount - ts[1 - buf]) / (ts[buf] - ts[1 - buf]);
  }

  void draw() {
    noStroke();
    ClockPos old_cp = cps[1 - buf];
    ClockPos new_cp = cps[buf];
    float x = interp(old_cp.x, new_cp.x);
    float y = interp(old_cp.y, new_cp.y);
    float c0 = interp(old_cp.c[0], new_cp.c[0]);
    float c1 = interp(old_cp.c[1], new_cp.c[1]);
    float c2 = interp(old_cp.c[2], new_cp.c[2]);

    arm(x, y, c0);
    arm(x, y, c1);
    arm(x, y, c2);
    fill(255);
    ellipse(x, y, R1*2, R1*2);
    fill(0);
    ellipse(x, y, R0*2, R0*2);
  }

  void arm(float x, float y, float c) {
    float a = (c-3) * 2 * PI / 12;
    fill(ARM_FILL);
    arc(x + R2 * cos(a), y + R2 * sin(a), 
    2 * (R2-R1), 2 * (R2-R1), 
    (PI +a) - ARC_W/2, (PI + a) + ARC_W/2);
    fill(SR_FILL);
    ellipse(x + R3 * cos(a), y + R3 * sin(a), SR*2, SR*2);
  }
}