Winter Particles

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;
 }
 }
}