Created
August 26, 2013 16:20
-
-
Save hysysk/6343370 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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