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.

[java]

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

[/java]