diff --git a/StippleGen/README.txt b/StippleGen/README.txt
new file mode 100644
index 0000000..a890991
--- /dev/null
+++ b/StippleGen/README.txt
@@ -0,0 +1,70 @@
+
+ StippleGen_2_31
+
+ SVG Stipple Generator, v. 2.31
+ Copyright (C) 2013 by Windell H. Oskay, www.evilmadscientist.com
+
+ Full Documentation: http://wiki.evilmadscientist.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
+
+ *******************************************************************************
+
+ Change Log:
+
+
+ v 2.3
+ * Forked from 2.1.1
+ * Fixed saving bug
+
+ v 2.20
+ * [Cancelled development branch.]
+
+ v 2.1.1
+ * Faster now, with number of stipples calculated at a time.
+
+ v 2.1.0
+ * Now compiling in Processing 2.0b6
+ * selectInput() and selectOutput() calls modified for Processing 2.
+
+ v 2.02
+ * Force files to end in .svg
+ * Fix bug that gave wrong size to stipple files saved white stipples on black background
+
+ v 2.01:
+ * Improved handling of Save process, to prevent accidental "not saving" by users.
+
+ v 2.0:
+ * Add tone reversal option (white on black / black on white)
+ * Reduce vertical extent of GUI, to reduce likelihood of cropping on small screens
+ * Speling corections
+ * Fixed a bug that caused unintended cropping of long, wide images
+ * Reorganized GUI controls
+ * Fail less disgracefully when a bad image type is selected.
+
+ *******************************************************************************
+
+
+
+ Program is based on the Toxic Libs Library ( http://toxiclibs.org/ )
+ & 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 Libs library:
+ http://www.sojamo.de/libraries/controlP5/
+ http://hg.postspectacular.com/toxiclibs/downloads
+
\ No newline at end of file
diff --git a/StippleGen/StippleGen2.pde b/StippleGen/StippleGen2.pde
new file mode 100644
index 0000000..bc97cb1
--- /dev/null
+++ b/StippleGen/StippleGen2.pde
@@ -0,0 +1,1411 @@
+/**
+
+ StippleGen_2_31
+
+ SVG Stipple Generator, v. 2.31
+ Copyright (C) 2013 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
+
+ *******************************************************************************
+
+ Change Log:
+
+
+ v 2.3
+ * Forked from 2.1.1
+ * Fixed saving bug
+
+ v 2.20
+ * [Cancelled development branch.]
+
+ v 2.1.1
+ * Faster now, with number of stipples calculated at a time.
+
+ v 2.1.0
+ * Now compiling in Processing 2.0b6
+ * selectInput() and selectOutput() calls modified for Processing 2.
+
+ v 2.02
+ * Force files to end in .svg
+ * Fix bug that gave wrong size to stipple files saved white stipples on black background
+
+ v 2.01:
+ * Improved handling of Save process, to prevent accidental "not saving" by users.
+
+ v 2.0:
+ * Add tone reversal option (white on black / black on white)
+ * Reduce vertical extent of GUI, to reduce likelihood of cropping on small screens
+ * Speling corections
+ * Fixed a bug that caused unintended cropping of long, wide images
+ * Reorganized GUI controls
+ * Fail less disgracefully when a bad image type is selected.
+
+ *******************************************************************************
+
+
+
+ Program is based on the Toxic Libs Library ( http://toxiclibs.org/ )
+ & 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 Libs 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 Libs 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.
+
+
+// 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;
+
+String ErrorDisplay = "";
+float ErrorTime;
+Boolean ErrorDisp = false;
+
+
+int Generation;
+int particleRouteLength;
+int RouteStep;
+
+boolean showBG;
+boolean showPath;
+boolean showCells;
+boolean invertImg;
+boolean TempShowCells;
+boolean FileModeTSP;
+
+int vorPointsAdded;
+boolean VoronoiCalculated;
+
+// Toxic libs library setup:
+Voronoi voronoi;
+Polygon2D RegionList[];
+
+PolygonClipper2D clip; // polygon clipper
+
+int cellsTotal, cellsCalculated, cellsCalculatedLast;
+
+
+// ControlP5 GUI library variables setup
+Textlabel ProgName;
+Button OrderOnOff, ImgOnOff, CellOnOff, InvertOnOff, PauseButton;
+ControlP5 cp5;
+
+
+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();
+
+ if (invertImg)
+ for (int i = 0; i < img.pixels.length; i++) {
+ img.pixels[i] = color(0);
+ }
+ else
+ 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 (((float) imgload.width / (float)imgload.height) > ((float) mainwidth / (float) 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!
+
+
+ /*
+ // Optional gamma correction for background image.
+ img.loadPixels();
+
+ float tempFloat;
+ float GammaValue = 1.0; // Normally in the range 0.25 - 4.0
+
+ for (int i = 0; i < img.pixels.length; i++) {
+ tempFloat = brightness(img.pixels[i])/255;
+ img.pixels[i] = color(floor(255 * pow(tempFloat,GammaValue)));
+ }
+ img.updatePixels();
+ */
+
+
+ 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 (invertImg)
+ {
+ p = 1 - p;
+ }
+
+ if (random(1) >= p ) {
+ Vec2D p1 = new Vec2D(fx, fy);
+ particles[i] = p1;
+ i++;
+ }
+ }
+
+ 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 = 110;
+
+// size(mainwidth, mainheight + ctrlheight, JAVA2D);
+
+ 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));
+
+
+ cp5 = new ControlP5(this);
+
+ int leftcolumwidth = 225;
+
+ int GUItop = mainheight + 15;
+ int GUI2ndRow = 4; // Spacing for firt row after group heading
+ int GuiRowSpacing = 14; // Spacing for subsequent rows
+ int GUIFudge = mainheight + 19; // I wish that we didn't need ONE MORE of these stupid spacings.
+
+
+ ControlGroup l3 = cp5.addGroup("Primary controls (Changing will restart)", 10, GUItop, 225);
+
+ cp5.addSlider("Stipples", 10, 10000, maxParticles, 10, GUI2ndRow, 150, 10).setGroup(l3);
+
+ InvertOnOff = cp5.addButton("INVERT_IMG", 10, 10, GUI2ndRow + GuiRowSpacing, 190, 10).setGroup(l3);
+ InvertOnOff.setCaptionLabel("Black stipples, White Background");
+
+
+ Button LoadButton = cp5.addButton("LOAD_FILE", 10, 10, GUIFudge + 3*GuiRowSpacing, 175, 10);
+ LoadButton.setCaptionLabel("LOAD IMAGE FILE (.PNG, .JPG, or .GIF)");
+
+ cp5.addButton("QUIT", 10, 205, GUIFudge + 3*GuiRowSpacing, 30, 10);
+
+ cp5.addButton("SAVE_STIPPLES", 10, 25, GUIFudge + 4*GuiRowSpacing, 160, 10);
+ cp5.controller("SAVE_STIPPLES").setCaptionLabel("Save Stipple File (.SVG format)");
+
+ cp5.addButton("SAVE_PATH", 10, 25, GUIFudge + 5*GuiRowSpacing, 160, 10);
+ cp5.controller("SAVE_PATH").setCaptionLabel("Save \"TSP\" Path (.SVG format)");
+
+
+ ControlGroup l5 = cp5.addGroup("Display Options - Updated on next generation", leftcolumwidth+50, GUItop, 225);
+
+ cp5.addSlider("Min_Dot_Size", .5, 8, 2, 10, 4, 140, 10).setGroup(l5);
+ cp5.controller("Min_Dot_Size").setValue(MinDotSize);
+ cp5.controller("Min_Dot_Size").setCaptionLabel("Min. Dot Size");
+
+ cp5.addSlider("Dot_Size_Range", 0, 20, 5, 10, 18, 140, 10).setGroup(l5);
+ cp5.controller("Dot_Size_Range").setValue(DotSizeFactor);
+ cp5.controller("Dot_Size_Range").setCaptionLabel("Dot Size Range");
+
+ cp5.addSlider("White_Cutoff", 0, 1, 0, 10, 32, 140, 10).setGroup(l5);
+ cp5.controller("White_Cutoff").setValue(cutoff);
+ cp5.controller("White_Cutoff").setCaptionLabel("White Cutoff");
+
+
+ ImgOnOff = cp5.addButton("IMG_ON_OFF", 10, 10, 46, 90, 10);
+ ImgOnOff.setGroup(l5);
+ ImgOnOff.setCaptionLabel("Image BG >> Hide");
+
+ CellOnOff = cp5.addButton("CELLS_ON_OFF", 10, 110, 46, 90, 10);
+ CellOnOff.setGroup(l5);
+ CellOnOff.setCaptionLabel("Cells >> Hide");
+
+ PauseButton = cp5.addButton("Pause", 10, 10, 60, 190, 10);
+ PauseButton.setGroup(l5);
+ PauseButton.setCaptionLabel("Pause (to calculate TSP path)");
+
+ OrderOnOff = cp5.addButton("ORDER_ON_OFF", 10, 10, 74, 190, 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;
+ invertImg = false;
+ showPath = true;
+ showCells = false;
+ fileLoaded = false;
+ SaveNow = 0;
+}
+
+
+//void setup() {
+// selectInput("Select a file to process:", "fileSelected");
+//}
+
+
+void fileSelected(File selection) {
+ if (selection == null) {
+ println("Window was closed or the user hit cancel.");
+ }
+ else {
+ //println("User selected " + selection.getAbsolutePath());
+
+ String loadPath = selection.getAbsolutePath();
+
+ // If a file was selected, print path to file
+ println("Loaded file: " + loadPath);
+
+
+ String[] p = splitTokens(loadPath, ".");
+ boolean fileOK = false;
+
+ if ( p[p.length - 1].equals("GIF"))
+ fileOK = true;
+ if ( p[p.length - 1].equals("gif"))
+ fileOK = true;
+ if ( p[p.length - 1].equals("JPG"))
+ fileOK = true;
+ if ( p[p.length - 1].equals("jpg"))
+ fileOK = true;
+ if ( p[p.length - 1].equals("TGA"))
+ fileOK = true;
+ if ( p[p.length - 1].equals("tga"))
+ fileOK = true;
+ if ( p[p.length - 1].equals("PNG"))
+ fileOK = true;
+ if ( p[p.length - 1].equals("png"))
+ fileOK = true;
+
+ println("File OK: " + fileOK);
+
+ if (fileOK) {
+ imgload = loadImage(loadPath);
+ fileLoaded = true;
+ // MainArraysetup();
+ ReInitiallizeArray = true;
+ }
+ else {
+ // Can't load file
+ ErrorDisplay = "ERROR: BAD FILE TYPE";
+ ErrorTime = millis();
+ ErrorDisp = true;
+ }
+ }
+}
+
+
+
+void LOAD_FILE(float theValue) {
+ println(":::LOAD JPG, GIF or PNG FILE:::");
+
+ selectInput("Select a file to process:", "fileSelected"); // Opens file chooser
+} //End Load File
+
+
+
+void SAVE_PATH(float theValue) {
+ FileModeTSP = true;
+ SAVE_SVG(0);
+}
+
+
+
+void SAVE_STIPPLES(float theValue) {
+ FileModeTSP = false;
+ SAVE_SVG(0);
+}
+
+
+
+
+void SavefileSelected(File selection) {
+ if (selection == null) {
+ // If a file was not selected
+ println("No output file was selected...");
+ ErrorDisplay = "ERROR: NO FILE NAME CHOSEN.";
+ ErrorTime = millis();
+ ErrorDisp = true;
+ }
+ else {
+
+ savePath = selection.getAbsolutePath();
+ String[] p = splitTokens(savePath, ".");
+ boolean fileOK = false;
+
+ if ( p[p.length - 1].equals("SVG"))
+ fileOK = true;
+ if ( p[p.length - 1].equals("svg"))
+ fileOK = true;
+
+ if (fileOK == false)
+ savePath = savePath + ".svg";
+
+
+ // If a file was selected, print path to folder
+ println("Save file: " + savePath);
+ SaveNow = 1;
+ showPath = true;
+
+ ErrorDisplay = "SAVING FILE...";
+ ErrorTime = millis();
+ ErrorDisp = true;
+ }
+}
+
+
+
+
+void SAVE_SVG(float theValue) {
+
+ if (pausemode != true) {
+ Pause(0.0);
+ ErrorDisplay = "Error: PAUSE before saving.";
+ ErrorTime = millis();
+ ErrorDisp = true;
+ }
+ else {
+
+ selectOutput("Output .svg file name:", "SavefileSelected");
+
+
+ }
+}
+
+
+
+
+void QUIT(float theValue) {
+ exit();
+}
+
+
+void ORDER_ON_OFF(float theValue) {
+ if (showPath) {
+ showPath = false;
+ OrderOnOff.setCaptionLabel("Plotting path >> Hide");
+ }
+ else {
+ showPath = true;
+ OrderOnOff.setCaptionLabel("Plotting path >> Shown while paused");
+ }
+}
+
+void CELLS_ON_OFF(float theValue) {
+ if (showCells) {
+ showCells = false;
+ CellOnOff.setCaptionLabel("Cells >> Hide");
+ }
+ else {
+ showCells = true;
+ CellOnOff.setCaptionLabel("Cells >> Show");
+ }
+}
+
+
+
+void IMG_ON_OFF(float theValue) {
+ if (showBG) {
+ showBG = false;
+ ImgOnOff.setCaptionLabel("Image BG >> Hide");
+ }
+ else {
+ showBG = true;
+ ImgOnOff.setCaptionLabel("Image BG >> Show");
+ }
+}
+
+
+void INVERT_IMG(float theValue) {
+ if (invertImg) {
+ invertImg = false;
+ InvertOnOff.setCaptionLabel("Black stipples, White Background");
+ cp5.controller("White_Cutoff").setCaptionLabel("White Cutoff");
+ }
+ else {
+ invertImg = true;
+ InvertOnOff.setCaptionLabel("White stipples, Black Background");
+ cp5.controller("White_Cutoff").setCaptionLabel("Black Cutoff");
+ }
+
+ ReInitiallizeArray = true;
+ pausemode = false;
+}
+
+
+
+
+void Pause(float theValue) {
+ // Main particle array setup (to be repeated if necessary):
+
+ if (pausemode)
+ {
+ pausemode = false;
+ println("Resuming.");
+ PauseButton.setCaptionLabel("Pause (to calculate TSP path)");
+ }
+ else
+ {
+ pausemode = true;
+ println("Paused. Press PAUSE again to resume.");
+ PauseButton.setCaptionLabel("Paused (calculating TSP path)");
+ }
+ RouteStep = 0;
+}
+
+
+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);
+ 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 {
+
+ if (invertImg)
+ fill(0);
+ 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 (invertImg)
+ v = 1 - v;
+
+
+ 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 + 500; // This line: VoronoiPointsPerPass (Feel free to edit this number.)
+ if (temp > maxParticles)
+ temp = maxParticles;
+
+// for (int i = vorPointsAdded; i < temp; ++i) {
+ for (int i = vorPointsAdded; i < temp; i++) {
+
+
+ // Optional, for diagnostics:::
+ // println("particles[i].x, particles[i].y " + particles[i].x + ", " + particles[i].y );
+
+ 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;
+ }
+ }
+ else
+ { // Part II: Calculate weighted centroids of cells.
+ // float millisBaseline = millis();
+ // println("fps = " + frameRate );
+
+ StatusDisplay = "Calculating weighted centroids";
+
+ temp = cellsCalculated + 500; // This line: CentroidsPerPass (Feel free to edit this number.)
+ // Higher values give slightly faster computation, but a less responsive GUI.
+ // Default value: 500
+
+ // Time/frame @ 100: 2.07 @ 50 frames in
+ // Time/frame @ 200: 1.575 @ 50
+ // Time/frame @ 500: 1.44 @ 50
+
+ 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;
+
+
+ if (invertImg)
+ 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)) {
+
+ // Thanks to polygon clipping, NO vertices will be beyond the sides of imgblur.
+ PicDensity = 0.001 + (brightness(imgblur.pixels[ round(y)*imgblur.width + round(x) ]));
+
+ xSum += PicDensity * x;
+ ySum += PicDensity * y;
+ dSum += PicDensity;
+ }
+ }
+ }
+ else
+ 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)) {
+
+ // Thanks to polygon clipping, NO vertices will be beyond the sides of imgblur.
+ PicDensity = 255.001 - (brightness(imgblur.pixels[ round(y)*imgblur.width + round(x) ]));
+
+
+ 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) cp5.controller("Stipples").value(); // Only change this here!
+
+ MainArraysetup();
+ ReInitiallizeArray = false;
+ }
+
+ 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);
+ }
+ }
+
+
+ if (invertImg)
+ stroke(255);
+ else
+ stroke(0);
+
+ 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;
+
+ if (invertImg)
+ v = 1 - v;
+
+ 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();
+
+
+ if (invertImg && (showBG == false)) // TODO -- if invertImg AND NOT background
+ stroke(100);
+ else
+ stroke(200);
+
+ // 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 four lines, if you wish to display the "before" dots at weighted sizes.
+ //float v = (brightness(imgblur.pixels[ py*imgblur.width + px ]))/255;
+ //if (invertImg)
+ //v = 1 - v;
+ //strokeWeight (MaxDotSize - v * dotScale);
+ point(px, py);
+ }
+ }
+ }
+ }
+ else {
+ // Stipple calculation is still underway
+
+ if (TempShowCells)
+ {
+ DoBackgrounds();
+ TempShowCells = false;
+ }
+
+
+ // stroke(0); // Stroke color
+
+
+ if (invertImg)
+ stroke(255);
+ else
+ stroke(0);
+
+ 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 (invertImg)
+ v = 1 - v;
+
+ 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 + 35, 205, 20) )
+ {
+ fill(150);
+ rect(TextColumnStart - 10, mainheight + 35, 205, 20);
+ }
+
+ fill(255); // Text color
+
+ text("StippleGen 2 (v. 2.1.0)", TextColumnStart, mainheight + 15);
+ text("by Evil Mad Scientist Laboratories", TextColumnStart, mainheight + 30);
+ text("www.evilmadscientist.com/go/stipple2", TextColumnStart, mainheight + 50);
+
+ text("Generations completed: " + Generation, TextColumnStart, mainheight + 85);
+ text("Time/Frame: " + frameTime + " s", TextColumnStart, mainheight + 100);
+
+
+ if (ErrorDisp)
+ {
+ fill(255, 0, 0); // Text color
+ text(ErrorDisplay, TextColumnStart, mainheight + 70);
+ if ((millis() - ErrorTime) > 8000)
+ ErrorDisp = false;
+ }
+ else
+ text("Status: " + StatusDisplay, TextColumnStart, mainheight + 70);
+
+
+
+ if (SaveNow > 0) {
+
+ 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;
+
+ if (invertImg)
+ v = 1 - v;
+
+ 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
+
+ if (FileModeTSP)
+ ErrorDisplay = "TSP Path .SVG file Saved";
+ else
+ ErrorDisplay = "Stipple .SVG file saved ";
+
+ ErrorTime = millis();
+ ErrorDisp = true;
+ }
+}
+
+
+
+void mousePressed() {
+
+ // rect(TextColumnStart, mainheight, 200, 75);
+
+ if (overRect(TextColumnStart - 15, mainheight + 35, 205, 20) )
+ link("http://www.evilmadscientist.com/go/stipple2");
+}
+
+
+
+
+void keyPressed() {
+ if (key == 'x')
+ { // If this program doesn't run slowly enough for you,
+ // simply press the 'x' key on your keyboard. :)
+ cp5.controller("Stipples").setMax(50000.0);
+ }
+}
+
diff --git a/StippleGen/data/grace.jpg b/StippleGen/data/grace.jpg
new file mode 100644
index 0000000..4083902
Binary files /dev/null and b/StippleGen/data/grace.jpg differ
diff --git a/StippleGen/data/header.txt b/StippleGen/data/header.txt
new file mode 100644
index 0000000..c7131f0
--- /dev/null
+++ b/StippleGen/data/header.txt
@@ -0,0 +1,48 @@
+
+
+
+