/**
StippleGen
SVG Stipple Generator, v 1.0
Copyright (C) 2012 by Windell H. Oskay, www.evilmadscientist.com
Documentation: http://www.evilmadscientist.com/go/stipple
An implementation of Weighted Voronoi Stippling:
http://mrl.nyu.edu/~ajsecord/stipples.html
Program is based on the Toxic Labs Library
& example code:
http://forum.processing.org/topic/toxiclib-voronoi-example-sketch
Additional inspiration:
Stipple Cam from Jim Bumgardner
http://joyofprocessing.com/blog/2011/11/stipple-cam/
and
MeshLibDemo.pde - Demo of Lee Byron's Mesh library, by
Marius Watz - http://workshop.evolutionzone.com/
Requires ControlP5 library and Toxic Labs library:
http://www.sojamo.de/libraries/controlP5/
http://hg.postspectacular.com/toxiclibs/downloads
*/
/*
*
* 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
*/
// You need the controlP5 library from http://www.sojamo.de/libraries/controlP5/
import controlP5.*;
//You need the Toxic Labs library: http://hg.postspectacular.com/toxiclibs/downloads
import toxi.geom.*;
import toxi.geom.mesh2d.*;
import toxi.util.datatypes.*;
import toxi.processing.*;
// helper class for rendering
ToxiclibsSupport gfx;
import javax.swing.UIManager;
import javax.swing.JFileChooser;
// Feel free to play with these three default settings:
int maxParticles = 2000; // Max value is normally 10000. Press 'x' key to allow 50000 stipples. (SLOW)
float MinDotSize = 1.75; //2;
float DotSizeFactor = 4; //5;
float cutoff = 0; // White cutoff value
int cellBuffer = 100; //Scale each cell to fit in a cellBuffer-sized square window for computing the centroid.
//float kSpeed;
// Display window and GUI area sizes:
int mainwidth;
int mainheight;
int borderWidth;
int ctrlheight;
int TextColumnStart;
float lowBorderX;
float hiBorderX;
float lowBorderY;
float hiBorderY;
float MaxDotSize;
boolean ReInitiallizeArray;
boolean pausemode;
boolean fileLoaded;
int SaveNow;
String savePath;
String[] FileOutput;
String StatusDisplay = "Initializing, please wait. :)";
float millisLastFrame = 0;
float frameTime = 0;
int Generation;
int particleRouteLength;
int RouteStep;
boolean showBG;
boolean showPath;
boolean showCells;
boolean TempShowCells;
boolean FileModeTSP;
int vorPointsAdded;
boolean VoronoiCalculated;
// Toxic labs library setup:
Voronoi voronoi;
Polygon2D RegionList[];
PolygonClipper2D clip; // polygon clipper
int cellsTotal, cellsCalculated, cellsCalculatedLast;
// ControlP5 library variables setup
Textlabel ProgName;
Button OrderOnOff, ImgOnOff, CellOnOff;
// GUI variables:
ControlP5 controlP5;
PImage img, imgload, imgblur;
Vec2D[] particles;
int[] particleRoute;
void LoadImageAndScale() {
int tempx = 0;
int tempy = 0;
img = createImage(mainwidth, mainheight, RGB);
imgblur = createImage(mainwidth, mainheight, RGB);
img.loadPixels();
for (int i = 0; i < img.pixels.length; i++) {
img.pixels[i] = color(255);
}
img.updatePixels();
if ( fileLoaded == false) {
// Load a demo image, at least until we have a "real" image to work with.
imgload = loadImage("grace.jpg"); // Load demo image
// Image source: http://commons.wikimedia.org/wiki/File:Kelly,_Grace_(Rear_Window).jpg
}
if ((imgload.width > mainwidth) || (imgload.height > mainheight)) {
if ((imgload.width / imgload.height) > (mainwidth/mainheight))
{
imgload.resize(mainwidth, 0);
}
else
{
imgload.resize(0, mainheight);
}
}
if (imgload.height < (mainheight - 2) ) {
tempy = (int) (( mainheight - imgload.height ) / 2) ;
}
if (imgload.width < (mainwidth - 2)) {
tempx = (int) (( mainwidth - imgload.width ) / 2) ;
}
img.copy(imgload, 0, 0, imgload.width, imgload.height, tempx, tempy, imgload.width, imgload.height);
// For background image!
imgblur.copy(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height);
// This is a duplicate of the background image, that we will apply a blur to,
// to reduce "high frequency" noise artifacts.
imgblur.filter(BLUR, 1); // Low-level blur filter to elminate pixel-to-pixel noise artifacts.
imgblur.loadPixels();
}
void MainArraysetup() {
// Main particle array initialization (to be called whenever necessary):
LoadImageAndScale();
// image(img, 0, 0); // SHOW BG IMG
particles = new Vec2D[maxParticles];
// Fill array by "rejection sampling"
int i = 0;
while (i < maxParticles)
{
float fx = lowBorderX + random(hiBorderX - lowBorderX);
float fy = lowBorderY + random(hiBorderY - lowBorderY);
float p = brightness(imgblur.pixels[ floor(fy)*imgblur.width + floor(fx) ])/255;
// OK to use simple floor_ rounding here, because this is a one-time operation,
// creating the initial distribution that will be iterated.
if (random(1) >= p ) {
Vec2D p1 = new Vec2D(fx, fy);
particles[i] = p1;
i++;
}
}
if (showBG)
image(img, 0, 0); // Show original (cropped and scaled, but not blurred!) image in background
else
background(255);
particleRouteLength = 0;
Generation = 0;
millisLastFrame = millis();
RouteStep = 0;
VoronoiCalculated = false;
cellsCalculated = 0;
vorPointsAdded = 0;
voronoi = new Voronoi(); // Erase mesh
TempShowCells = true;
FileModeTSP = false;
}
void setup()
{
borderWidth = 6;
mainwidth = 800;
mainheight = 600;
ctrlheight = 135;
size(mainwidth, mainheight + ctrlheight, JAVA2D);
gfx = new ToxiclibsSupport(this);
lowBorderX = borderWidth; //mainwidth*0.01;
hiBorderX = mainwidth - borderWidth; //mainwidth*0.98;
lowBorderY = borderWidth; // mainheight*0.01;
hiBorderY = mainheight - borderWidth; //mainheight*0.98;
int innerWidth = mainwidth - 2 * borderWidth;
int innerHeight = mainheight - 2 * borderWidth;
clip=new SutherlandHodgemanClipper(new Rect(lowBorderX, lowBorderY, innerWidth, innerHeight));
MainArraysetup(); // Main particle array setup
frameRate(24);
smooth();
noStroke();
fill(153); // Background fill color, for control section
textFont(createFont("SansSerif", 10));
controlP5 = new ControlP5(this);
int leftcolumwidth = 225;
ControlGroup l1 = controlP5.addGroup("Master", 10, mainheight + 15, leftcolumwidth);
Button LoadButton = controlP5.addButton("LOAD_FILE", 10, 10, 5, 150, 10);
LoadButton.setGroup(l1);
LoadButton.setCaptionLabel("LOAD IMAGE FILE (PNG/JPG/GIF)");
controlP5.addButton("SAVE_SVG", 10, 20, 20, 150, 10).setGroup(l1);
controlP5.controller("SAVE_SVG").setCaptionLabel("Save Stipple File (.SVG format)");
controlP5.addButton("SAVE_PATH", 10, 20, 35, 150, 10).setGroup(l1);
controlP5.controller("SAVE_PATH").setCaptionLabel("Save \"TSP\" Path (.SVG format)");
controlP5.addButton("QUIT", 10, 195, 5, 30, 10).setGroup(l1);
ControlGroup l2 = controlP5.addGroup("Pause (calculate path) or Restart Scan", 10, mainheight + 80, leftcolumwidth);
controlP5.addButton("Pause", 10, 10, 5, 60, 10).setGroup(l2);
controlP5.addButton("Restart", 10, 75, 5, 60, 10).setGroup(l2);
ControlGroup l3 = controlP5.addGroup("Stipple Count (Changing will restart)", 10, mainheight + 115, 225);
controlP5.addSlider("Stipples", 10, 10000, maxParticles, 10, 5, 150, 10).setGroup(l3);
ControlGroup l5 = controlP5.addGroup("Display Options (updated on next generation)", leftcolumwidth+50, mainheight + 15, 225);
controlP5.addSlider("Min_Dot_Size", .5, 8, 2, 10, 5, 140, 10).setGroup(l5);
controlP5.controller("Min_Dot_Size").setValue(MinDotSize);
controlP5.controller("Min_Dot_Size").setCaptionLabel("Min. Dot Size");
controlP5.addSlider("Dot_Size_Range", 0, 20, 5, 10, 20, 140, 10).setGroup(l5);
controlP5.controller("Dot_Size_Range").setValue(DotSizeFactor);
controlP5.controller("Dot_Size_Range").setCaptionLabel("Dot Size Range");
controlP5.addSlider("White_Cutoff", 0, 1, 0, 10, 35, 140, 10).setGroup(l5);
controlP5.controller("White_Cutoff").setValue(cutoff);
controlP5.controller("White_Cutoff").setCaptionLabel("White Cutoff");
ImgOnOff = controlP5.addButton("IMG_ON_OFF", 10, 10, 50, 175, 10);
ImgOnOff.setGroup(l5);
ImgOnOff.setCaptionLabel("Target Image Background >> Hide");
CellOnOff = controlP5.addButton("CELLS_ON_OFF", 10, 10, 65, 175, 10);
CellOnOff.setGroup(l5);
CellOnOff.setCaptionLabel("Voronoi Cells >> Hide");
OrderOnOff = controlP5.addButton("ORDER_ON_OFF", 10, 10, 80, 175, 10);
OrderOnOff.setGroup(l5);
OrderOnOff.setCaptionLabel("Plotting path >> shown while paused");
TextColumnStart = 2 * leftcolumwidth + 100;
MaxDotSize = MinDotSize * (1 + DotSizeFactor);
ReInitiallizeArray = false;
pausemode = false;
showBG = false;
showPath = true;
showCells = false;
fileLoaded = false;
SaveNow = 0;
}
void LOAD_FILE(float theValue) {
println(":::LOAD JPG, GIF or PNG FILE:::");
String loadPath = selectInput(); // Opens file chooser
if (loadPath == null) {
// If a file was not selected
println("No file was selected...");
}
else {
// If a file was selected, print path to file
println("Loaded file: " + loadPath);
imgload = loadImage(loadPath);
fileLoaded = true;
// MainArraysetup();
ReInitiallizeArray = true;
}
} //End Load File
void SAVE_SVG(float theValue) {
savePath = selectOutput("Output .svg file name:"); // Opens file chooser
if (savePath == null) {
// If a file was not selected
println("No output file was selected...");
}
else {
// If a file was selected, print path to folder
println("Save file: " + savePath);
SaveNow = 1;
showPath = true;
if (pausemode != true)
Pause(0.0);
}
}
void SAVE_PATH(float theValue) {
FileModeTSP = true;
SAVE_SVG(0);
}
void SAVE_PDF(float theValue) {
// TODO: Implement this function
}
void QUIT(float theValue) {
exit();
}
void ORDER_ON_OFF(float theValue) {
if (showPath) {
showPath = false;
OrderOnOff.setCaptionLabel("Plotting path >> Hiden");
}
else {
showPath = true;
OrderOnOff.setCaptionLabel("Plotting path >> shown while paused");
}
}
void CELLS_ON_OFF(float theValue) {
if (showCells) {
showCells = false;
CellOnOff.setCaptionLabel("Voronoi Cells >> Hide");
}
else {
showCells = true;
CellOnOff.setCaptionLabel("Voronoi Cells >> Show");
}
}
void IMG_ON_OFF(float theValue) {
if (showBG) {
showBG = false;
ImgOnOff.setCaptionLabel("Target Image Background >> Hide");
}
else {
showBG = true;
ImgOnOff.setCaptionLabel("Target Image Background >> Show");
}
}
void Pause(float theValue) {
// Main particle array setup (to be repeated if necessary):
if (pausemode)
{
pausemode = false;
println("Resuming.");
}
else
{
pausemode = true;
println("Paused. Press PAUSE again to resume.");
}
RouteStep = 0;
}
void Restart(float theValue) {
// Main particle array setup (to be repeated if necessary):
// MainArraysetup();
ReInitiallizeArray = true;
pausemode = false;
}
boolean overRect(int x, int y, int width, int height)
{
if (mouseX >= x && mouseX <= x+width &&
mouseY >= y && mouseY <= y+height) {
return true;
}
else {
return false;
}
}
void Stipples(int inValue) {
if (maxParticles != (int) inValue) {
println("Update: Stipple Count -> " + inValue);
//maxParticles = (int) inValue;
// MainArraysetup();
ReInitiallizeArray = true;
pausemode = false;
}
}
void Min_Dot_Size(float inValue) {
if (MinDotSize != inValue) {
println("Update: Min_Dot_Size -> "+inValue);
MinDotSize = inValue;
MaxDotSize = MinDotSize* (1 + DotSizeFactor);
}
}
void Dot_Size_Range(float inValue) {
if (DotSizeFactor != inValue) {
println("Update: Dot Size Range -> "+inValue);
DotSizeFactor = inValue;
MaxDotSize = MinDotSize* (1 + DotSizeFactor);
}
}
void White_Cutoff(float inValue) {
if (cutoff != inValue) {
println("Update: White_Cutoff -> "+inValue);
cutoff = inValue;
RouteStep = 0; // Reset TSP path
}
}
void DoBackgrounds() {
if (showBG)
image(img, 0, 0); // Show original (cropped and scaled, but not blurred!) image in background
else {
fill(255);
rect(0, 0, mainwidth, mainheight);
}
}
void OptimizePlotPath()
{
int temp;
// Calculate and show "optimized" plotting path, beneath points.
StatusDisplay = "Optimizing plotting path";
/*
if (RouteStep % 100 == 0) {
println("RouteStep:" + RouteStep);
println("fps = " + frameRate );
}
*/
Vec2D p1;
if (RouteStep == 0)
{
float cutoffScaled = 1 - cutoff;
// Begin process of optimizing plotting route, by flagging particles that will be shown.
particleRouteLength = 0;
boolean particleRouteTemp[] = new boolean[maxParticles];
for (int i = 0; i < maxParticles; ++i) {
particleRouteTemp[i] = false;
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 < cutoffScaled) {
particleRouteTemp[i] = true;
particleRouteLength++;
}
}
particleRoute = new int[particleRouteLength];
int tempCounter = 0;
for (int i = 0; i < maxParticles; ++i) {
if (particleRouteTemp[i])
{
particleRoute[tempCounter] = i;
tempCounter++;
}
}
// These are the ONLY points to be drawn in the tour.
}
if (RouteStep < (particleRouteLength - 2))
{
// Nearest neighbor ("Simple, Greedy") algorithm path optimization:
int StopPoint = RouteStep + 1000; // 1000 steps per frame displayed; you can edit this number!
if (StopPoint > (particleRouteLength - 1))
StopPoint = particleRouteLength - 1;
for (int i = RouteStep; i < StopPoint; ++i) {
p1 = particles[particleRoute[RouteStep]];
int ClosestParticle = 0;
float distMin = Float.MAX_VALUE;
for (int j = RouteStep + 1; j < (particleRouteLength - 1); ++j) {
Vec2D p2 = particles[particleRoute[j]];
float dx = p1.x - p2.x;
float dy = p1.y - p2.y;
float distance = (float) (dx*dx+dy*dy); // Only looking for closest; do not need sqrt factor!
if (distance < distMin) {
ClosestParticle = j;
distMin = distance;
}
}
temp = particleRoute[RouteStep + 1];
// p1 = particles[particleRoute[RouteStep + 1]];
particleRoute[RouteStep + 1] = particleRoute[ClosestParticle];
particleRoute[ClosestParticle] = temp;
if (RouteStep < (particleRouteLength - 1))
RouteStep++;
else
{
println("Now optimizing plot path" );
}
}
}
else
{ // Initial routing is complete
// 2-opt heuristic optimization:
// Identify a pair of edges that would become shorter by reversing part of the tour.
for (int i = 0; i < 90000; ++i) { // 1000 tests per frame; you can edit this number.
int indexA = floor(random(particleRouteLength - 1));
int indexB = floor(random(particleRouteLength - 1));
if (Math.abs(indexA - indexB) < 2)
continue;
if (indexB < indexA)
{ // swap A, B.
temp = indexB;
indexB = indexA;
indexA = temp;
}
Vec2D a0 = particles[particleRoute[indexA]];
Vec2D a1 = particles[particleRoute[indexA + 1]];
Vec2D b0 = particles[particleRoute[indexB]];
Vec2D b1 = particles[particleRoute[indexB + 1]];
// Original distance:
float dx = a0.x - a1.x;
float dy = a0.y - a1.y;
float distance = (float) (dx*dx+dy*dy); // Only a comparison; do not need sqrt factor!
dx = b0.x - b1.x;
dy = b0.y - b1.y;
distance += (float) (dx*dx+dy*dy); // Only a comparison; do not need sqrt factor!
// Possible shorter distance?
dx = a0.x - b0.x;
dy = a0.y - b0.y;
float distance2 = (float) (dx*dx+dy*dy); // Only a comparison; do not need sqrt factor!
dx = a1.x - b1.x;
dy = a1.y - b1.y;
distance2 += (float) (dx*dx+dy*dy); // Only a comparison; do not need sqrt factor!
if (distance2 < distance)
{
// Reverse tour between a1 and b0.
int indexhigh = indexB;
int indexlow = indexA + 1;
// println("Shorten!" + frameRate );
while (indexhigh > indexlow)
{
temp = particleRoute[indexlow];
particleRoute[indexlow] = particleRoute[indexhigh];
particleRoute[indexhigh] = temp;
indexhigh--;
indexlow++;
}
}
}
}
frameTime = (millis() - millisLastFrame)/1000;
millisLastFrame = millis();
}
void doPhysics()
{ // Iterative relaxation via weighted Lloyd's algorithm.
int temp;
int CountTemp;
if (VoronoiCalculated == false)
{ // Part I: Calculate voronoi cell diagram of the points.
StatusDisplay = "Calculating Voronoi diagram ";
// float millisBaseline = millis(); // Baseline for timing studies
// println("Baseline. Time = " + (millis() - millisBaseline) );
if (vorPointsAdded == 0)
voronoi = new Voronoi(); // Erase mesh
temp = vorPointsAdded + 200; // This line: VoronoiPointsPerPerPass (Feel free to edit this number.)
if (temp > maxParticles)
temp = maxParticles;
for (int i = vorPointsAdded; i < temp; ++i) {
// println("particles[i].x, particles[i].y " + particles[i].x + ", " + particles[i].y ); // Optional, for diagnostics
voronoi.addPoint(new Vec2D(particles[i].x, particles[i].y ));
vorPointsAdded++;
}
if (vorPointsAdded >= maxParticles)
{
// println("Points added. Time = " + (millis() - millisBaseline) );
cellsTotal = (voronoi.getRegions().size());
vorPointsAdded = 0;
cellsCalculated = 0;
cellsCalculatedLast = 0;
RegionList = new Polygon2D[cellsTotal];
int i = 0;
for (Polygon2D poly : voronoi.getRegions()) {
RegionList[i++] = poly; // Build array of polygons
}
VoronoiCalculated = true;
// println("RegionList Built. Time = " + (millis() - millisBaseline) );
}
}
else
{ // Part II: Calculate weighted centroids of cells.
// float millisBaseline = millis();
// println("fps = " + frameRate );
StatusDisplay = "Calculating weighted centroids";
temp = cellsCalculated + 100; // This line: CentroidsPerPass (Feel free to edit this number.)
if (temp > cellsTotal)
{
temp = cellsTotal;
}
for (int i=cellsCalculated; i< temp; i++) {
float xMax = 0;
float xMin = mainwidth;
float yMax = 0;
float yMin = mainheight;
float xt, yt;
Polygon2D region = clip.clipPolygon(RegionList[i]);
for (Vec2D v : region.vertices) {
xt = v.x;
yt = v.y;
if (xt < xMin)
xMin = xt;
if (xt > xMax)
xMax = xt;
if (yt < yMin)
yMin = yt;
if (yt > yMax)
yMax = yt;
}
float xDiff = xMax - xMin;
float yDiff = yMax - yMin;
float maxSize = max(xDiff, yDiff);
float minSize = min(xDiff, yDiff);
float scaleFactor = 1.0;
// Maximum voronoi cell extent should be between
// cellBuffer/2 and cellBuffer in size.
while (maxSize > cellBuffer)
{
scaleFactor *= 0.5;
maxSize *= 0.5;
}
while (maxSize < (cellBuffer/2))
{
scaleFactor *= 2;
maxSize *= 2;
}
if ((minSize * scaleFactor) > (cellBuffer/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 centroid:
scaleFactor *= 0.5;
}
float StepSize = (1/scaleFactor);
float xSum = 0;
float ySum = 0;
float dSum = 0;
float PicDensity = 1.0;
for (float x=xMin; x<=xMax; x += StepSize) {
for (float y=yMin; y<=yMax; y += StepSize) {
Vec2D p0 = new Vec2D(x, y);
if (region.containsPoint(p0)) {
int px = round(x); // pixel location in original image
int py = round(y); // pixel location in original image
// Thanks to polygon clipping, NO vertices will be beyond the sides of imgblur.
PicDensity = 256.0 - (brightness(imgblur.pixels[ py*imgblur.width + px ]));
// MINIMUM value of PicDensity will be 1.0
xSum += PicDensity * x;
ySum += PicDensity * y;
dSum += PicDensity;
}
}
}
if (dSum > 0)
{
xSum /= dSum;
ySum /= dSum;
}
Vec2D centr;
float xTemp = (xSum);
float yTemp = (ySum);
if ((xTemp <= lowBorderX) || (xTemp >= hiBorderX) || (yTemp <= lowBorderY) || (yTemp >= hiBorderY)) {
// If new centroid is computed to be outside the visible region, use the geometric centroid instead.
// This will help to prevent runaway points due to numerical artifacts.
centr = region.getCentroid();
xTemp = centr.x;
yTemp = centr.y;
// Enforce sides, if absolutely necessary: (Failure to do so *will* cause a crash, eventually.)
if (xTemp <= lowBorderX)
xTemp = lowBorderX + 1;
if (xTemp >= hiBorderX)
xTemp = hiBorderX - 1;
if (yTemp <= lowBorderY)
yTemp = lowBorderY + 1;
if (yTemp >= hiBorderY)
yTemp = hiBorderY - 1;
}
particles[i].x = xTemp;
particles[i].y = yTemp;
cellsCalculated++;
}
// println("cellsCalculated = " + cellsCalculated );
// println("cellsTotal = " + cellsTotal );
if (cellsCalculated >= cellsTotal)
{
VoronoiCalculated = false;
Generation++;
println("Generation = " + Generation );
frameTime = (millis() - millisLastFrame)/1000;
millisLastFrame = millis();
}
}
}
void draw()
{
int i = 0;
int temp;
float dotScale = (MaxDotSize - MinDotSize);
float cutoffScaled = 1 - cutoff;
if (ReInitiallizeArray) {
maxParticles = (int) controlP5.controller("Stipples").value(); // Only change this here!
MainArraysetup();
ReInitiallizeArray = false;
}
// noFill();
if (pausemode && (VoronoiCalculated == false))
OptimizePlotPath();
else
doPhysics();
if (pausemode)
{
DoBackgrounds();
// Draw paths:
if ( showPath ) {
stroke(128, 128, 255); // Stroke color (blue)
strokeWeight (1);
for ( i = 0; i < (particleRouteLength - 1); ++i) {
Vec2D p1 = particles[particleRoute[i]];
Vec2D p2 = particles[particleRoute[i + 1]];
line(p1.x, p1.y, p2.x, p2.y);
}
}
stroke(0); // Stroke color
for ( i = 0; i < particleRouteLength; ++i) {
// Only show "routed" particles-- those above the white cutoff.
Vec2D p1 = particles[particleRoute[i]];
int px = (int) p1.x;
int py = (int) p1.y;
float v = (brightness(imgblur.pixels[ py*imgblur.width + px ]))/255;
strokeWeight (MaxDotSize - v * dotScale);
point(px, py);
}
}
else
{ // NOT in pause mode. i.e., just displaying stipples.
if (cellsCalculated == 0) {
DoBackgrounds();
if (Generation == 0)
{
TempShowCells = true;
}
if (showCells || TempShowCells) { // Draw voronoi cells, over background.
strokeWeight(1);
noFill();
stroke(200);
i = 0;
for (Polygon2D poly : voronoi.getRegions()) {
//RegionList[i++] = poly;
gfx.polygon2D(clip.clipPolygon(poly));
}
}
if (showCells) {
// Show "before and after" centroids, when polygons are shown.
strokeWeight (MinDotSize); // Normal w/ Min & Max dot size
for ( i = 0; i < maxParticles; ++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;
{
//Uncomment the following two lines, if you wish to display the "before" dots at weighted sizes.
//float v = (brightness(imgblur.pixels[ py*imgblur.width + px ]))/255;
//strokeWeight (MaxDotSize - v * dotScale);
point(px, py);
}
}
}
}
else {
// Stipple calculation is still underway
if (TempShowCells)
{
DoBackgrounds();
TempShowCells = false;
}
stroke(0); // Stroke color
for ( i = cellsCalculatedLast; i < cellsCalculated; ++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 < cutoffScaled) {
strokeWeight (MaxDotSize - v * dotScale);
point(px, py);
}
}
}
cellsCalculatedLast = cellsCalculated;
}
}
noStroke();
fill(100); // Background fill color
rect(0, mainheight, mainwidth, height); // Control area fill
// Underlay for hyperlink:
if (overRect(TextColumnStart - 10, mainheight + 40, 205, 20) )
{
fill(150);
rect(TextColumnStart - 10, mainheight + 40, 205, 20);
}
fill(255); // Text color
text("StippleGen v. 1.0", TextColumnStart, mainheight + 15);
text("by Evil Mad Scientist Laboratories", TextColumnStart, mainheight + 35);
text("www.evilmadscientist.com/go/stipple", TextColumnStart, mainheight + 55);
text("Status: " + StatusDisplay, TextColumnStart, mainheight + 90);
text("Generations completed: " + Generation, TextColumnStart, mainheight + 105);
text("Time/Frame: " + frameTime + " s", TextColumnStart, mainheight + 120);
if (SaveNow > 0) {
StatusDisplay = "Optimizing file path for saving";
if (RouteStep >= (particleRouteLength - 2))
SaveNow++;
if (SaveNow > 10) {
// Optimize path-- at least a little bit --before saving file!
StatusDisplay = "Saving SVG File";
SaveNow = 0;
FileOutput = loadStrings("header.txt");
String rowTemp;
float SVGscale = (800.0 / (float) mainheight);
int xOffset = (int) (1600 - (SVGscale * mainwidth / 2));
int yOffset = (int) (400 - (SVGscale * mainheight / 2));
if (FileModeTSP)
{ // Plot the PATH between the points only.
println("Save TSP File (SVG)");
// Path header::
rowTemp = ""); // End path description
}
else {
println("Save Stipple File (SVG)");
for ( i = 0; i < particleRouteLength; ++i) {
Vec2D p1 = particles[particleRoute[i]];
int px = floor(p1.x);
int py = floor(p1.y);
float v = (brightness(imgblur.pixels[ py*imgblur.width + px ]))/255;
float dotrad = (MaxDotSize - v * dotScale)/2;
float xTemp = SVGscale*p1.x + xOffset;
float yTemp = SVGscale*p1.y + yOffset;
rowTemp = "";
// Typ:
FileOutput = append(FileOutput, rowTemp);
}
}
// SVG footer:
FileOutput = append(FileOutput, "");
saveStrings(savePath, FileOutput);
FileModeTSP = false; // reset for next time
}
}
}
void mousePressed() {
// rect(TextColumnStart, mainheight, 200, 75);
if (overRect(TextColumnStart - 15, mainheight + 40, 205, 20) )
link("http://www.evilmadscientist.com/go/stipple");
}
void keyPressed() {
if (key == 'x')
{ // If this program doesn't run slowly enough for you,
// simply press the 'x' key on your keyboard. :)
controlP5.controller("Stipples").setMax(50000.0);
}
}