/* 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);
}
}