//////////////////////////////
// //
// //
// Codex Processianus //
// //
// //
//////////////////////////////
// (c) Martin Schneider 2009
// This sketch creates flow map based drawings.
// A flow map is an image where hue indicates the direction of flow
// and brightness indicates the amount of flow at any given pixel
// parameters
int n = 1000;
int rdodge = 20;
int opacity = 9;
int mapids = 5;
boolean showmap, lifelong, brush, fine, dodge = true;
int maxage, crayons, whirl, isolines, mapid, xhatch = 1;
// array of palettes
color[][] palette = {
{#ffdd99, #882211}, // da vinci
{#000000, #ffffff}, // blackboard
{#ffffff, #000000, #000099, #990099, #009999 }, // pencil crayons
{#000000, #6666ff, #aaaa66, #ff6666, #66ffff }, // fibreglass
{#000000} // rainbow colors
};
int palettes = palette.length;
int rainbow = 4;
// global variables
float[][] a = new float[n][2];
int[] age = new int[n];
float w, h;
int c;
void setup() {
size(500, 500);
w = width/2;
h = height/2;
colorMode(HSB, TWO_PI, 2, 1);
loadFlowmap(mapid);
smooth();
reset();
}
void draw() {
if(showmap) {
// show flow map
background(0);
image(ff, 0, 0);
}
else {
// number of colors in the selected palette
int colors = palette[crayons].length - 1;
// select pen or brush
strokeWeight(brush ? 3 : 1);
// create new particles
int np = n / maxage;
for(int i=0; i<np & c<n; i++, c++) newp(c);
// set detail
int grain = fine ? 1 : 3;
// draw particle traces
for(int i=0; i<c; i++) {
// get particle from the array
float[] p = a[i];
// aging and rebirth
if (age[i]++ > maxage) newp(i);
else {
// save the starting point
float p0 = p[0], p1 = p[1];
// move through flow map for a small distance ( the grain )
final int maxiter = 10;
float d = 0, j = 0;
while(d < grain & j++ < maxiter ) {
// get the flow at the current point
float[] f = f(p[0], p[1], whirl + 2 * (i % xhatch) );
// update the point
p[0] += f[0] * cos(f[1]);
p[1] += f[0] * sin(f[1]);
// calculate distance to the starting point
d = dist(p0, p1, p[0], p[1]);
}
//prevent dot marks and allow for iso-stripe pattern
if(d < grain + isolines * .15) continue;
if(d < grain + isolines * .15) continue;
// rainbow hue based on direction
if (crayons == rainbow) stroke(atan2(p[0]-p0,p[1]-p1) + PI, 1, 1, opacity);
// crayon hue based on cross-hatch direction
else stroke(palette[crayons][1 + (i % xhatch) % colors], opacity);
line(p0, p1, p[0], p[1]);
}
}
}
}
void newp(int p) {
if(dodge) {
// particle inside a circle around the mouse position
float r = random(rdodge), ang = random(TWO_PI);
a[p] = new float[] { mouseX + r * cos(ang), mouseY + r *sin(ang) };
} else {
// particle anywhere on screen
a[p] = new float[] { random(width), random(height) };
}
age[p] = 0;
}
void reset() {
// clear screen
if(!showmap) background(palette[crayons][0]);
// reset maxage and particle counter
maxage = lifelong ? 20 : 10;
c = 0;
}
//// ////
// //
// flow map //
// //
//// ////
PImage ff0;
PGraphics ff;
float tx, ty;
float zoom = 1;
// load the selected flow map
void loadFlowmap(int id) {
ff = createGraphics(width, height, P2D);
ff0 = loadImage("flowmap-" + (1+id) + ".jpg");
updateFlowmap();
}
// rather than sampling the flow map directly we rescale it
// relying on the bilinear interpolation algorithm built into Processing ...
// ( so we get smooth curves and borders, even for extreme zoom values )
void updateFlowmap() {
float fw = ff0.width / 2f;
float fh = ff0.height / 2f;
float fzoom = zoom * max(w/fw, h/fh);
ff.beginDraw();
ff.background(0);
ff.image(ff0, int(tx + w - fw*fzoom), int(ty + h - fh*fzoom), int(ff0.width * fzoom), int(ff0.height * fzoom));
ff.endDraw();
}
// the flow map function
float[] f(float x, float y, int w) {
if ( x<0 | x>width | y<0 | y>width ) return new float[] {0, 0};
else {
color c = ff.get((int)x, (int)y);
float ang = hue(c) + HALF_PI / xhatch * w;
if(ang>TWO_PI) ang-=TWO_PI;
float d = brightness(c);
return new float[] { d, ang };
}
}
// transformations
void zoom(float z) {
zoom *= z;
tx *= z;
ty *= z;
updateFlowmap();
}
void move(int dx, int dy) {
tx += dx;
ty += dy;
updateFlowmap();
}
//// ////
// //
// interaction //
// //
//// ////
void mousePressed() {
showmap = true;
}
void mouseReleased() {
showmap = false;
reset();
}
void mouseDragged() {
if(mouseButton == LEFT)
move(mouseX - pmouseX, mouseY - pmouseY);
else
zoom(1 - (mouseY - pmouseY) / h);
}
void keyPressed() {
switch(key) {
case ' ' : mapid++; mapid %= mapids; loadFlowmap(mapid); break;
case 'd' : dodge = !dodge; break;
case 'b' : brush = !brush; break;
case 'g' : fine =! fine; break;
case 'l' : lifelong =! lifelong; break;
case 'c' : crayons = ++crayons % palettes; break;
case 'i' : isolines = ++isolines % 4; break;
case 'w' : whirl = ++whirl % 4; break;
case 'z' :; case 'y' : xhatch = max(--xhatch, 1) ; break;
case 'x' : ++xhatch; break;
case '0' : tx = 0; ty = 0; zoom(1/zoom); break;
case 's' : saveFrame("codex-processianus-#####.png"); break;
default: return;
}
reset();
}
This sketch creates flow map based drawings.
A flow map is an image where hue indicates the direction of flow
and brightness indicates the amount of flow at any given pixel.
You can find some hires samples on flickr:
http://www.flickr.com/photos/bitcraft/tags/codexprocessianus/
Key Map
[space] switch flow map
[0] reset flow map position
[s] save image to disk
[d] dodge on/off
[b] brush on/off
[g] grain fine/rough
[l] lines short/long
[c] colors : daVinci, blackboard, crayons, fibreglass, rainbow
[w] whirl direction
[i] isolines
[x] more x-hatch
[z] less x-hatch