Tom Says: Code something crazy every day you feel like it!
We've begun work on our digital etch-a-sketch using Processing, what looks to be a sweet Java environment preconfigured for 2-D and 3-D graphics, sound, and video (in and out). I like it a lot so far.
Don is going crazy creating some slick demos, but he's moving so fast that his code stinks. I'm trying to keep up by refactoring the stuff he sends me. Here are some of the classes that have dropped out of that effort.
We'll be generating a cursor position using data from other hardware later, but for testing outside the lab, the mouse is the most convenient source. These classes abstract the differences so we can change between the inputs (or write classes to mix them!) with a single point of entry in our code.
/**
* input.pde
* Version 2007-11-20:
* - MouseInput tested
* - InvertedInput tested
* - SerialInput should work, but is untested
*/
import processing.serial.*;
/* Example usage:
CursorInput input;
input = new MouseInput(this);
input = new InvertedInput(new MouseInput(this), width, height);
input = new SerialInput(this, "/dev/tty.KeySerial1");
Use input.x() and input.y() where you might otherwise have used mouseX
or mouseY.
input.init() must be called before any other functions are called.
input.update() should be called before asking for position information.
*/
abstract class CursorInput
{
protected int x, y;
public CursorInput()
{
x = 0;
y = 0;
}
public void init() {}
abstract void update();
int x() { return x; }
int y() { return y; }
}
/* Cursor position from the mouse */
class MouseInput extends CursorInput
{
private PApplet applet;
public MouseInput(PApplet applet)
{
super();
this.applet = applet;
}
public void update()
{
x = applet.mouseX;
y = applet.mouseY;
}
}
/* Invert X and Y axes! */
class InvertedInput extends CursorInput
{
private CursorInput base;
private int width, height;
public InvertedInput(CursorInput base, int width, int height)
{
super();
this.base = base;
this.width = width;
this.height = height;
}
public void update()
{
base.update();
x = width - base.x();
y = height - base.y();
}
}
/* Talks to a BASIC Stamp over serial.
* It is expected that the stamp will transmit data according to a
* protocol like "A100 B200 A101 B99 A105 "... That is, the letter "A"
* or "B" and the value from that terminal, then a space. For example,
* the PBASIC code below does this:
' {$STAMP BS2}
' {$PBASIC 2.5}
CS CON 2 ' Chip Select Pin
CLK CON 3 ' Clock Pin
DIO_n CON 4 ' Data In/Out Pin
config VAR NIB ' Configuration bits
AD0 VAR WORD ' Analog Data Channel 0
AD1 VAR WORD ' Analog Data Channel 1
HIGH CS
HIGH DIO_n
myloop:
GOSUB convert0:
GOSUB convert1:
DEBUG "A", DEC AD0, " B", DEC AD1, " "
GOTO myloop:
convert0: ' Convert channel 0 only
config = %1011
LOW CS
SHIFTOUT DIO_n,CLK,LSBFIRST,[config]
SHIFTIN DIO_n,CLK,MSBPOST,[AD02]
HIGH CS
RETURN
convert1: ' Convert channel 1 only
config = %1111
LOW CS
SHIFTOUT DIO_n,CLK,LSBFIRST,[config]
SHIFTIN DIO_n,CLK,MSBPOST,[AD12]
HIGH CS
RETURN
*/
class SerialInput extends CursorInput
{
private PApplet applet;
private String port;
private Serial s;
private StringBuffer buf = new StringBuffer();
public SerialInput(PApplet applet, String port)
{
super();
this.applet = applet;
this.port = port;
}
public void init()
{
s = new Serial(applet, port);
while(s.readChar() != ' '); // sync to the next message
}
public void update()
{
while(s.available() > 1)
{
char c = s.readChar();
if(c == ' ')
{
parseMessage(buf.toString());
buf.delete(0, buf.length());
}
else
buf.append(c);
}
}
private void parseMessage(String msg) throws IllegalArgumentException
{
switch(msg.charAt(0))
{
case 'A':
x = Integer.parseInt(msg.substring(1));
case 'B':
x = Integer.parseInt(msg.substring(1));
default:
throw new IllegalArgumentException("Invalid message: " + msg);
}
}
}
A lot of nifty effects center around having many temporary objects that represent some graphic on-screen. For example, for sparkly mouse trails, you have to track positions of the sparks so that they can be animated coherently. These sparks are very easy to think about in an object-oriented manner, so we'd like to code them that way. Tracking their lifetime is tedious, and a great candidate for a class that allows you to fire them off and let them manage themselves.
/* Managing long lists of objects when making trails is boring. This
* class manages those for you. Example usage:
*
* FireAndForget faf = new FireAndForget(20);
* faf.add(new TrailingObject(input.x(), input.y()));
* faf.decay(); // hook to allow trailers to decay if they like
* faf.draw(); // calls draw() on trailers that are alive
*/
class FireAndForget
{
private int max;
private LinkedList forgotten;
public FireAndForget(int max)
{
this.max = max;
forgotten = new LinkedList();
}
public void add(Object toForget)
{
forgotten.addLast(toForget);
if(forgotten.size() > max)
forgotten.removeFirst();
}
public void decay()
{
Forgotten f;
Iterator itr = forgotten.iterator();
while(itr.hasNext())
{
f = (Forgotten) itr.next();
f.decay();
if(f.isDead())
itr.remove();
}
}
public void draw()
{
Forgotten f;
Iterator itr = forgotten.listIterator(0);
while(itr.hasNext())
{
f = (Forgotten) itr.next();
f.draw();
}
}
}
interface Forgotten
{
void decay();
void draw();
boolean isDead();
}
Here is the graphical demo I used to drive the design choices above.
final int maxLife = 30;
final float scatter = 1.0;
Ball currentBall;
FireAndForget faf;
CursorInput input;
class Ball implements Forgotten
{
private int x, y, sizeb;
private float vx, vy, a;
private int life;
private color c;
public Ball(int x, int y)
{
this.x = x;
this.y = y;
a = 255;
sizeb = 0;
}
public Ball(int x, int y, int life, color c, int sizeb)
{
this.x = x;
this.y = y;
this.life = life;
vx = random(-scatter,scatter);
vy = random(-scatter,scatter);
a = 255;
this.c = c;
this.sizeb = sizeb;
}
public void draw()
{
stroke(200,1,1, (255*life)/maxLife);
strokeWeight(int((10.0*life)/maxLife)+1);
fill(c, (a*life)/maxLife);
noStroke();
ellipse(x, y, sizeb, sizeb);
}
public Ball spawn(int x, int y)
{
color c = color(70,250,120);
return new Ball(x, y, int(random(10,maxLife)), c, 10);
}
public void decay()
{
x += vx;
y += vy;
vx += random(-.2,.2);
vy += random(-.2,.2);
sizeb = int(10.0*life/maxLife);
life--;
}
public boolean isDead()
{
return life <= 0;
}
}
void setup()
{
size(600, 600);
input = new MouseInput(this);
//input = new InvertedInput(new MouseInput(this), width, height);
//input = new SerialInput(this, "/dev/tty.KeySerial1");
input.init();
input.update();
stroke(100,1,1);
background(0);
faf = new FireAndForget(120);
for(int i = 0; i < 120 - 1; i++)
faf.add(new Ball(input.x(),input.y()));
faf.add(currentBall = new Ball(input.x(), input.y()));
}
void mouseMoved()
{
faf.add(currentBall.spawn(input.x(), input.y()));
faf.add(currentBall.spawn(input.x(), input.y()));
faf.add(currentBall.spawn(input.x(), input.y()));
faf.add(currentBall = currentBall.spawn(input.x(), input.y()));
}
void mousePressed()
{
if(mouseButton == RIGHT)
background(0);
}
void draw()
{
input.update();
faf.decay();
faf.add(currentBall = currentBall.spawn(input.x(), input.y()));
faf.draw();
}
Posted Nov 20, 2007, in the afternoon.