Skip to content

Instantly share code, notes, and snippets.

@hysysk
Created August 26, 2013 16:20
Show Gist options
  • Save hysysk/6343370 to your computer and use it in GitHub Desktop.
Save hysysk/6343370 to your computer and use it in GitHub Desktop.
/*
Modified from SVG Stipple Generator, v. 2.02
Copyright (C) 2012 by Windell H. Oskay, www.evilmadscientist.com
Full Documentation: http://wiki.evilmadscience.com/StippleGen
Blog post about the release: http://www.evilmadscientist.com/go/stipple2
An implementation of Weighted Voronoi Stippling:
http://mrl.nyu.edu/~ajsecord/stipples.html
*******************************************************************************
/*
*
* This is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* http://creativecommons.org/licenses/LGPL/2.1/
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
import toxi.geom.*;
import toxi.geom.mesh2d.*;
import toxi.util.datatypes.*;
import toxi.processing.*;
// helper class for rendering
ToxiclibsSupport gfx;
int FRAME_RATE = 30;
String SOURCE_FILE_NAME = "img.jpg";
int MAX_PARTICLES_NUMBER = 3000;
float BRIGHTNESS_CUTOFF = 0.12; // White BRIGHTNESS_CUTOFF value
int CELL_BUFFER = 100; //Scale each cell to fit in a CELL_BUFFER-sized square window for computing the centeroid.
float minDotSize = 1.2; //2;
float dotSizeFactor = 4; //5;
int MAIN_WIDTH = 800;
int MAIN_HEIGHT = 600;
int BORDER_WIDTH = 6;
int lowBorderX;
int highBorderX;
int lowBorderY;
int highBorderY;
float maxDotSize;
int generation;
boolean isCellShown = false;
boolean isTempCellShown;
int voronoiPointsNumber;
boolean isVoronoiCalculated;
// Toxic libs library setup:
Voronoi voronoi;
Polygon2D regionList[];
PolygonClipper2D clip; // polygon clipper
int totalCellsNumber;
int calculatedCellsNumber;
int lastCalculatedCellsNumber;
PImage img;
PImage imgLoad;
PImage imgBlur;
Vec2D[] particles;
void loadImageAndScale(String fileName, int targetWidth, int targetHeight) {
int tempX = 0;
int tempY = 0;
img = createImage(targetWidth, targetHeight, RGB);
imgBlur = createImage(targetWidth, targetHeight, RGB);
img.loadPixels();
for (int i = 0; i < img.pixels.length; i++) {
img.pixels[i] = color(255);
}
img.updatePixels();
imgLoad = loadImage(fileName);
if ((imgLoad.width > targetWidth) || (imgLoad.height > targetWidth)) {
if (((float)imgLoad.width / (float)imgLoad.height) > ((float)targetWidth / (float)targetHeight)) {
imgLoad.resize(targetWidth, 0);
}
else {
imgLoad.resize(0, targetHeight);
}
}
if (imgLoad.width < (targetWidth - 2)) {
tempX = (int)((targetWidth - imgLoad.width ) / 2) ;
}
if (imgLoad.height < (targetHeight - 2)) {
tempY = (int)((targetHeight - imgLoad.height ) / 2) ;
}
img.copy(imgLoad, 0, 0, imgLoad.width, imgLoad.height, tempX, tempY, imgLoad.width, imgLoad.height);
imgBlur.copy(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height);
imgBlur.filter(BLUR, 1);
imgBlur.loadPixels();
}
void setupMainArray(int startX, int startY, int lengthX, int lengthY) {
particles = new Vec2D[MAX_PARTICLES_NUMBER];
int i = 0;
float x;
float y;
float b;
while (i < MAX_PARTICLES_NUMBER) {
x = startX + random(lengthX);
y = startY + random(lengthY);
b = brightness(imgBlur.pixels[floor(y)*imgBlur.width + floor(x)])/255;
if (random(1) >= b) {
Vec2D p = new Vec2D(x, y);
particles[i] = p;
i++;
}
}
generation = 0;
isVoronoiCalculated = false;
calculatedCellsNumber = 0;
voronoiPointsNumber = 0;
voronoi = new Voronoi();
isTempCellShown = true;
}
void setup() {
size(MAIN_WIDTH, MAIN_HEIGHT);
gfx = new ToxiclibsSupport(this);
lowBorderX = BORDER_WIDTH; // MAIN_WIDTH*0.01;
highBorderX = MAIN_WIDTH - BORDER_WIDTH; //MAIN_WIDTH*0.98;
lowBorderY = BORDER_WIDTH; // MAIN_HEIGHT*0.01;
highBorderY = MAIN_HEIGHT - BORDER_WIDTH; //MAIN_HEIGHT*0.98;
int innerWidth = MAIN_WIDTH - 2 * BORDER_WIDTH;
int innerHeight = MAIN_HEIGHT - 2 * BORDER_WIDTH;
clip = new SutherlandHodgemanClipper(new Rect(lowBorderX, lowBorderY, innerWidth, innerHeight));
loadImageAndScale(SOURCE_FILE_NAME, MAIN_WIDTH, MAIN_HEIGHT);
setupMainArray(lowBorderX, lowBorderY, innerWidth, innerHeight); // Main particle array setup
frameRate(FRAME_RATE);
smooth();
maxDotSize = minDotSize * (1 + dotSizeFactor);
}
void draw() {
updateParticles();
drawParticles();
}
void updateParticles() {
// Iterative relaxation via weighted Lloyd's algorithm.
if (isVoronoiCalculated == false) {
calculateVoronoi();
}
else {
calculateCells();
}
}
void calculateVoronoi() {
int tempPointsNumber;
// Part I: Calculate voronoi cell diagram of the points.
// float millisBaseline = millis(); // Baseline for timing studies
// println("Baseline. Time = " + (millis() - millisBaseline) );
if (voronoiPointsNumber == 0) {
voronoi = new Voronoi(); // Erase mesh
}
tempPointsNumber = voronoiPointsNumber + 200; // This line: VoronoiPointsPerPass (Feel free to edit this number.)
if (tempPointsNumber > MAX_PARTICLES_NUMBER) {
tempPointsNumber = MAX_PARTICLES_NUMBER;
}
int i;
for (i = voronoiPointsNumber; i < tempPointsNumber; i++) {
voronoi.addPoint(new Vec2D(particles[i].x, particles[i].y));
voronoiPointsNumber++;
}
if (voronoiPointsNumber >= MAX_PARTICLES_NUMBER) {
totalCellsNumber = (voronoi.getRegions().size());
voronoiPointsNumber = 0;
calculatedCellsNumber = 0;
lastCalculatedCellsNumber = 0;
regionList = new Polygon2D[totalCellsNumber];
i = 0;
for (Polygon2D poly : voronoi.getRegions()) {
regionList[i++] = poly; // Build array of polygons
}
isVoronoiCalculated = true;
}
}
void calculateCells() {
int tempCellsNumber;
// Part II: Calculate weighted centeroids of cells.
// float millisBaseline = millis();
// println("fps = " + frameRate );
tempCellsNumber = calculatedCellsNumber + 100; // This line: centeroidsPerPass (Feel free to edit this number.)
if (tempCellsNumber > totalCellsNumber) {
tempCellsNumber = totalCellsNumber;
}
float maxX = 0;
float minX = MAIN_WIDTH;
float maxY = 0;
float minY = MAIN_HEIGHT;
float tx, ty;
for (int i=calculatedCellsNumber; i < tempCellsNumber; i++) {
Polygon2D region = clip.clipPolygon(regionList[i]);
for (Vec2D v : region.vertices) {
tx = v.x;
ty = v.y;
if (tx < minX)
minX = tx;
if (tx > maxX)
maxX = tx;
if (ty < minY)
minY = ty;
if (ty > maxY)
maxY = ty;
}
float diffX = maxX - minX;
float diffY = maxY - minY;
float maxSize = max(diffX, diffY);
float minSize = min(diffX, diffY);
float scaleFactor = 1.0;
// Maximum voronoi cell etxent should be between
// CELL_BUFFER/2 and CELL_BUFFER in size.
while (maxSize > CELL_BUFFER) {
scaleFactor *= 0.5;
maxSize *= 0.5;
}
while (maxSize < (CELL_BUFFER/2)) {
scaleFactor *= 2;
maxSize *= 2;
}
if ((minSize * scaleFactor) > (CELL_BUFFER/2)) {
// Special correction for objects of near-unity (square-like) aspect ratio,
// which have larger area *and* where it is less essential to find the exact centeroid:
scaleFactor *= 0.5;
}
float stepSize = (1/scaleFactor);
float sumX = 0;
float sumY = 0;
float sumD = 0;
float pictureDensity = 1.0;
for (float x=minX; x<=maxX; x+=stepSize) {
for (float y=minY; y<=maxY; y+=stepSize) {
Vec2D p = new Vec2D(x, y);
if (region.containsPoint(p)) {
// Thanks to polygon clipping, NO vertices will be beyond the sides of imgBlur.
pictureDensity = 255.001 - (brightness(imgBlur.pixels[ round(y)*imgBlur.width + round(x) ]));
sumX += pictureDensity * x;
sumY += pictureDensity * y;
sumD += pictureDensity;
}
}
}
if (sumD > 0) {
sumX /= sumD;
sumY /= sumD;
}
Vec2D center;
float tempX = sumX;
float tempY = sumY;
if ((tempX <= lowBorderX) || (tempX >= highBorderX) || (tempY <= lowBorderY) || (tempY >= highBorderY)) {
// If new centeroid is computed to be outside the visible region, use the geometric centeroid instead.
// This will help to prevent runaway points due to numerical artifacts.
center = region.getCentroid();
tempX = center.x;
tempY = center.y;
// Enforce sides, if absolutely necessary: (Failure to do so *will* cause a crash, eventually.)
if (tempX <= lowBorderX)
tempX = lowBorderX + 1;
if (tempX >= highBorderX)
tempX = highBorderX - 1;
if (tempY <= lowBorderY)
tempY = lowBorderY + 1;
if (tempY >= highBorderY)
tempY = highBorderY - 1;
}
particles[i].x = tempX;
particles[i].y = tempY;
calculatedCellsNumber++;
}
if (calculatedCellsNumber >= totalCellsNumber) {
isVoronoiCalculated = false;
generation++;
println("generation = " + generation);
}
}
void drawParticles() {
int i = 0;
float dotScale = (maxDotSize - minDotSize);
float scaledBRIGHTNESS_CUTOFF = 1 - BRIGHTNESS_CUTOFF;
if (calculatedCellsNumber == 0) {
background(255);
if (generation == 0) {
isTempCellShown = true;
}
if (isCellShown || isTempCellShown) { // Draw voronoi cells, over background.
strokeWeight(1);
noFill();
stroke(200);
i = 0;
for (Polygon2D poly : voronoi.getRegions()) {
gfx.polygon2D(clip.clipPolygon(poly));
}
}
if (isCellShown) {
// Show "before and after" centeroids, when polygons are shown.
strokeWeight (minDotSize); // Normal w/ Min & Max dot size
for ( i = 0; i < MAX_PARTICLES_NUMBER; i++) {
int px = (int)particles[i].x;
int py = (int)particles[i].y;
if ((px >= imgBlur.width) || (py >= imgBlur.height) || (px < 0) || (py < 0)) {
continue;
}
float v = (brightness(imgBlur.pixels[ py*imgBlur.width + px ]))/255;
strokeWeight (maxDotSize - v * dotScale);
point(px, py);
}
}
}
else {
// Stipple calculation is still underway
if (isTempCellShown) {
background(255);
isTempCellShown = false;
}
stroke(0);
for (i = lastCalculatedCellsNumber; i < calculatedCellsNumber; i++) {
int px = (int)particles[i].x;
int py = (int)particles[i].y;
if ((px >= imgBlur.width) || (py >= imgBlur.height) || (px < 0) || (py < 0)) {
continue;
}
float v = (brightness(imgBlur.pixels[py*imgBlur.width + px]))/255;
if (v < scaledBRIGHTNESS_CUTOFF) {
strokeWeight (maxDotSize - v * dotScale);
point(px, py);
}
}
lastCalculatedCellsNumber = calculatedCellsNumber;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment