Happy Holidays! It’s been ages since I posted anything, mostly because I’ve been so busy teaching at UCA Farnham and Openlab Workshops.
Here’s a little something for the holidays. The code is below, and the source image was courtesy of Jan Tik on Flickr.
/**
* Use some pretty particles to trace an image
* Thanks to Flight404 and Cinder for much inspiration:
* http://libcinder.org/docs/v0.8.2/hello_cinder.html
* Also, I used Jan Tik's CC-licensed image from flickr:
* http://www.flickr.com/photos/jantik/308709862/
*
* Copyright (C) 2010 Evan Raskob <evan@flkr.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import processing.opengl.*;
import javax.media.opengl.*;
// uses toxi's toxiclibs for vector functions.. because it's easier -
// http://hg.postspectacular.com/toxiclibs/wiki/Home
import toxi.geom.Vec2D;
final String imgName = "Winter Meal - jantik.jpg"; // make sure this is in your data folder!
PImage bgImage;
ArrayList<Particle> particles = new ArrayList<Particle>();
float D; // base diameter of all particles
float mass = 0.2; // universal mass of all particles
float DIST = 80*80; // min distance for forces to act on
float MIN_DIST = 10; // min dist between mouse positions for adding new particles
boolean drawLines = false; // draw lines btw particles?
boolean saveFrames = false; // save frames sequentially to disk?
int frameIndex = 0; // index of saved frame - better than the built-in version
void setup()
{
// size should be the size of your image. you could make it dynamic if you want...
size(640,480,OPENGL);
hint(ENABLE_OPENGL_4X_SMOOTH) ;
strokeWeight(2.0);
D = min(width,height) / 30; // base diameter of particles on screen size. change this for bigger/smaller particles
bgImage = loadImage(); // background image
DIST = D*D*1.5*1.5; // distance between particles. Smaller = more detail in final image
MIN_DIST = D/4; // see above
}
void draw()
{
PGraphicsOpenGL pgl;
GL gl;
// *** blending setup *** //
pgl = (PGraphicsOpenGL) g;
gl = pgl.beginGL();
gl.glDisable(GL.GL_DEPTH_TEST);
gl.glEnable(GL.GL_BLEND);
// gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE );
gl.glBlendFunc( GL.GL_SRC_ALPHA , GL.GL_ONE_MINUS_DST_COLOR );
pgl.endGL();
// *** end blending setup *** //
// load pixels, for getting source colors
bgImage.loadPixels();
smooth();
background(0);
image(bgImage,0,0); // draw image first.
// add a new particle if the mouse is pressed
if (mousePressed && abs(pmouseX-mouseX) > MIN_DIST)
{
Particle p = new Particle((float)mouseX, (float)mouseY, D);
// p.v.x = 0.01*(pmouseX-mouseX);
// p.v.y = 0.01*(pmouseY-mouseY);
// set max velocity based on screen size
p.MAXV.x = width/200.0;
p.MAXV.y = height/200.0;
particles.add(p);
}
// optional - keep track of "dead" particles, to remove later
ArrayList<Particle> deadParticles = new ArrayList<Particle>();
noStroke();
// go through the particles and update their position data
for (Particle p : particles)
{
p.update();
if (p.alive)
{
color c = bgImage.pixels[((int)p.pos.y)*bgImage.width + (int)p.pos.x];
p.draw(c);
}
else
deadParticles.add(p);
}
// not using this yet... but could...
for (Particle p : deadParticles)
{
particles.remove(p);
p = null;
}
// handle inter-particle forces
repulseParticles();
// save frames in a sequence
if (saveFrames)
{
saveFrame("frame-" + nf(frameIndex,6)+".png");
++frameIndex;
}
}
void keyReleased()
{
if (key == ' ')
{
saveFrame("winter" + random(0,999999) + ".png");
}
else if (key == 'l' || key == 'L') drawLines = !drawLines;
else if (key == 'f') saveFrames = !saveFrames;
}
void repulseParticles()
{
for(int i=0; i<particles.size(); ++i)
{
Particle p0 = particles.get(i);
for(int ii=i+1; ii<particles.size(); ++ii ) {
Particle p1 = particles.get(ii);
Vec2D dir = p0.pos.sub(p1.pos);
float distSquared = dir.magSquared();
if( distSquared > 0.0f && distSquared <= DIST)
{
dir.normalize();
float F = min(0.2, 1.0f/distSquared) / mass;
dir.scaleSelf( F );
if (drawLines)
{
//stroke(255,80);
stroke(0,200,0);
stroke(p0.c);
line(p0.pos.x, p0.pos.y, p1.pos.x, p1.pos.y);
}
p0.a.addSelf(dir);
p1.a.subSelf(dir);
}
}
}
}
// a simple Particle class with acceleration and velocity
class Particle
{
Vec2D MAXV = new Vec2D(2,2); // max velocity this particle can have (absolute)
Vec2D pos; // position
Vec2D v; // instantaneous velocity
Vec2D a; // instantaneous acceleration
float d; // diameter
color c; // color
boolean alive = false;
Particle(float _x, float _y, float _d)
{
pos = new Vec2D(_x,_y);
v = new Vec2D();
a = new Vec2D();
d = _d;
alive = true;
}
void draw(color _c)
{
c = _c;
fill(c);
float bd = d*(0.9*brightness(c)/255.0 + 0.1);
ellipse(pos.x, pos.y, bd,bd);
}
void update()
{
if (v.x > MAXV.x)
v.x = MAXV.x;
if (v.x < -MAXV.x)
v.x = -MAXV.x;
if (v.y > MAXV.y)
v.y = MAXV.y;
if (v.y < -MAXV.y)
v.y = -MAXV.y;
pos.addSelf(v);
v.scaleSelf(0.95);
v.x += a.x;
v.y += a.y;
a.scaleSelf(0.5);
// if (pos.x >= width || pos.x <= 0 ||
// pos.y >= height || pos.y <= 0)
if (pos.x >= width || pos.x <= 0)
{
//alive = false;
pos.x = constrain(pos.x, 0, width-1);
v.x = -v.x;
a.x = 0;
}
if (pos.y >= height || pos.y <= 0)
{
//alive = false;
pos.y = constrain(pos.y, 0, height-1);
v.y = -v.y;
a.y = 0;
}
}
}