From d287894096503dd0a1a46fe51ed34a59f2070add Mon Sep 17 00:00:00 2001 From: mattmcw Date: Tue, 16 Mar 2021 21:33:50 -0400 Subject: [PATCH] create repo --- .gitignore | 1 + LICENSE.txt | 15 + README.md | 60 +++ example.config.txt | 5 + header.txt | 48 ++ stipple_gen.pde | 1288 ++++++++++++++++++++++++++++++++++++++++++++ stipple_gen.sh | 8 + 7 files changed, 1425 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 example.config.txt create mode 100644 header.txt create mode 100644 stipple_gen.pde create mode 100644 stipple_gen.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b8c14be --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +releases diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..46a1d60 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,15 @@ +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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..89fcf93 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# stipple_gen + +A friendly fork of StippleGen_2 from Evil Mad Scientist Laboratories. + +https://github.com/evil-mad/stipplegen + +This script takes an image, provided as a JPEG, a PNG or a TIFF (generated by Processing) and outputs both an SVG for a plotter as well as a approximation as a JPEG, PNG or TIFF. + +There have also been features added to do the following: + +* **Custom output SVG scale** +* **Custom canvas size** +* **Option to fill in circles** + +# Usage + +`stipple_gen` is designed to be configured and run from the command line, rather than the GUI provided in the original sketch. +By creating a config.txt file in your sketch directory *OR* providing command line arguments to your sketch you can set all of the variables accessible to the GUI in the original StippleGen_2 sketch and then some. + +An example using the helpful `stipple_gen.sh` script provided by this project. + +```bash +bash stipple_gen.sh --display true --inputImage myImage.jpg --outputImage myStippledImage.jpg --outputSVG myStippledDrawing.svg +``` + +This will take your original image `myImage.jpg` and run it through the stippling process using the default variables for everything and output two files for you: `myStippledImage.jpg` and `myStippledDrawing.svg`, the latter of which you can use on your plotter. + +Other variables: + +```java + +public int canvasWidth = 800; +public int canvasHeight = 600; + +public boolean display = true; +public int windowWidth = 800; +public int windowHeight = 600; + +public boolean invert = false; + +public boolean selectInput = false; +public String inputImage; +public String outputImage; +public String outputSVG; + +public int centroidsPerPass = 500; +public int testsPerFrame = 90000; // +public int maxGenerations = 5; //number of generations + +public float MinDotSize = 1.25; //2; +public float MaxDotSize; +public float DotSizeFactor = 4; //5; + +public int maxParticles = 2000; // Max value is normally 10000. +public float cutoff = 0; // White cutoff value + +public boolean gammaCorrection = false; +public float gamma = 1.0; + +``` \ No newline at end of file diff --git a/example.config.txt b/example.config.txt new file mode 100644 index 0000000..5f5d0dd --- /dev/null +++ b/example.config.txt @@ -0,0 +1,5 @@ +display=true +inputImage=grace.jpg +outputImage=output.png +outputSVG=output.svg +maxGenerations=40 \ No newline at end of file diff --git a/header.txt b/header.txt new file mode 100644 index 0000000..c7131f0 --- /dev/null +++ b/header.txt @@ -0,0 +1,48 @@ + + + + + + + + + + image/svg+xml + + + + + + + diff --git a/stipple_gen.pde b/stipple_gen.pde new file mode 100644 index 0000000..aa15c70 --- /dev/null +++ b/stipple_gen.pde @@ -0,0 +1,1288 @@ +/* + + Modified by Matt McWilliams 2021 + + stipple_gen + + This application is intended to replace the original UI of StippleGen_2 with + a simple Processing application that can be run using command line arguments or + a config file. Arguments take precedence over the config file. + + Why do it this way? So that the stippling process can be run headless with a config file + storing the majority of the settings and the command line arguments handling things + such as input and output file names. Why do that? So this process can be tied into + automated image generation processes. + + Begrudgingly but respectfully releasing this in accordance with the original LGPL license, + though I would prefer to use MIT or ISC which I consider to have fewer encumbrances. + + ******************************************************************************* + HISTORY + ******************************************************************************* + + Program is based on StippleGen_2 + + 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 + + ******************************************************************************* + + StippleGen_2 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 Toxic Libs library: + 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 + */ + +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; + +public class Config { + + private String filePath; + private File file; + private String[] data; + + public int canvasWidth = 800; + public int canvasHeight = 600; + + public boolean display = true; + public int windowWidth = 800; + public int windowHeight = 600; + + public boolean invert = false; + + public boolean selectInput = false; + public String inputImage; + public String outputImage; + public String outputSVG; + + public int centroidsPerPass = 500; + public int testsPerFrame = 90000; // + public int maxGenerations = 5; //number of generations + + public float MinDotSize = 1.25; //2; + public float MaxDotSize; + public float DotSizeFactor = 4; //5; + + public int maxParticles = 2000; // Max value is normally 10000. + public float cutoff = 0; // White cutoff value + + public boolean gammaCorrection = false; + public float gamma = 1.0; + + public Config (String inputFile) { + int index; + String[] parts; + file = new File(inputFile); + filePath = file.getAbsolutePath(); + boolean exists = file.isFile(); + + filePath = file.getAbsolutePath(); + exists = file.isFile(); + + index = argIndex("--config"); + if (index == -1) { + index = argIndex("-c"); + } + + if (index > -1) { + file = new File(args[index + 1]); + filePath = file.getAbsolutePath(); + exists = file.isFile(); + } + + if (exists) { + println("Using config " + filePath); + file = new File(filePath); + data = loadStrings(filePath); + } + + if (data != null) { + for (int i = 0; i < data.length; i++) { + parts = splitTokens(data[i], "="); + setVar(parts[0], parts[1], filePath); + } + } + + if (args != null) { + for (int i = 0; i < args.length; i+=2) { + if (args[i].startsWith("--")) { + setVar(args[i].substring(2), args[i+1], "args"); + } else { + setVar(args[i], args[i+1], "args"); + } + } + } + } + + private int argIndex (String arg) { + int index = -1; + if (args != null) { + for (int i = 0; i < args.length; i++) { + if ( args[i].startsWith(arg) ) { + index = i; + } + } + } + return index; + } + + private int intOrDie (String name, String val) { + int intVal = -1; + try { + intVal = parseInt(val); + } catch (Exception e) { + println("Error parsing value " + name); + println(e); + exit(); + } + return intVal; + } + + private boolean boolOrDie (String name, String val) { + String[] truthy = { "true", "on", "t" }; + String[] falsey = { "false", "off", "f" }; + boolean boolVal = true; + String compare = val.trim().toLowerCase(); + for (int i = 0; i < truthy.length; i++) { + if (truthy[i].equals(compare)) { + boolVal = true; + break; + } + } + for (int i = 0; i < falsey.length; i++) { + if (falsey[i].equals(compare)) { + boolVal = false; + break; + } + } + return boolVal; + } + + private float floatOrDie (String name, String val) { + float floatVal = -1; + try { + floatVal = parseFloat(val); + } catch (Exception e) { + println("Error parsing value " + name); + println(e); + exit(); + } + return floatVal; + } + + private String strOrDie (String name, String val) { + return val.trim(); + } + + public void setVar (String name, String val, String source) { + switch (name) { + case "canvasWidth" : + canvasWidth = intOrDie(name, val); + break; + case "canvasHeight" : + canvasHeight = intOrDie(name, val); + break; + case "display" : + display = boolOrDie(name, val); + break; + case "windowWidth" : + windowWidth = intOrDie(name, val); + break; + case "windowHeight" : + windowHeight = intOrDie(name, val); + break; + case "invert" : + invert = boolOrDie(name, val); + break; + case "inputImage" : + inputImage = strOrDie(name, val); + break; + case "outputImage" : + outputImage = strOrDie(name, val); + break; + case "outputSVG" : + outputSVG = strOrDie(name, val); + break; + case "centroidsPerPass" : + centroidsPerPass = intOrDie(name, val); + break; + case "testsPerFrame" : + testsPerFrame = intOrDie(name, val); + break; + case "maxGenerations" : + maxGenerations = intOrDie(name, val); + break; + case "MinDotSize" : + MinDotSize = floatOrDie(name, val); + break; + case "MaxDotSize" : + MaxDotSize = floatOrDie(name, val); + break; + case "DotSizeFactor" : + DotSizeFactor = floatOrDie(name, val); + break; + case "maxParticles" : + maxParticles = intOrDie(name, val); + break; + case "cutoff" : + cutoff = intOrDie(name, val); + break; + case "gammaCorrection" : + gammaCorrection = boolOrDie(name, val); + break; + case "gamma" : + gamma = floatOrDie(name, val); + } + println("[" + source + "] " + name + "=" + val); + } +} + +Config config; + +int cellBuffer = 100; //Scale each cell to fit in a cellBuffer-sized square window for computing the centroid. +int borderWidth = 6; + +float imageRatio; +float mainRatio; +float windowRatio; + +float lowBorderX; +float hiBorderX; +float lowBorderY; +float hiBorderY; + +boolean ReInitiallizeArray; +boolean fileLoaded; +int SaveNow; +String[] FileOutput; + +String StatusDisplay = "Initializing, please wait. :)"; +String lastStatusDisplay = ""; +float millisLastFrame = 0; +float frameTime = 0; + +String ErrorDisplay = ""; +float ErrorTime; +Boolean ErrorDisp = false; + +int Generation; +int lastGeneration = 0; +int particleRouteLength; +int RouteStep; + +boolean showBG; +boolean showPath; +boolean showCells; + +boolean TempShowCells; +boolean FileModeTSP; + +int vorPointsAdded; +boolean VoronoiCalculated; + +// Toxic libs library setup: +Voronoi voronoi; +Polygon2D RegionList[]; + +PolygonClipper2D clip; // polygon clipper + +int cellsTotal, cellsCalculated, cellsCalculatedLast; + +PImage img, imgload, imgblur; +PGraphics canvas; + +Vec2D[] particles; +int[] particleRoute; + +void LoadImageAndScale() { + int tempx = 0; + int tempy = 0; + + img = createImage(config.canvasWidth, config.canvasHeight, RGB); + imgblur = createImage(config.canvasWidth, config.canvasHeight, RGB); + + img.loadPixels(); + + if (config.invert) { + 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 (config.inputImage != null) { + imgload = loadImage(config.inputImage); + fileLoaded = true; + } + + if (config.display && config.selectInput && config.inputImage == null && !fileLoaded ) { + noLoop(); + LOAD_FILE(); + return; + } + 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 + } + + imageRatio = (float) imgload.width / (float) imgload.height; + mainRatio = (float) config.canvasWidth / (float) config.canvasHeight; + windowRatio = (float) config.windowWidth / (float) config.windowHeight; + + println("Image: " + imgload.width + "x" + imgload.height); + println("Ratio: " + imageRatio); + println("Main : " + config.canvasWidth + "x" + config.canvasHeight); + println("Ratio: " + mainRatio); + + //resize the image to fit within canvas size + if ((imgload.width > config.canvasWidth) || (imgload.height > config.canvasHeight)) { + if (imageRatio > mainRatio) { + imgload.resize(config.canvasWidth, 0); + } else { + imgload.resize(0, config.canvasHeight); + } + } + + if (imgload.height < (config.canvasHeight - 2) ) { + tempy = (int) (( config.canvasHeight - imgload.height ) / 2) ; + } + if (imgload.width < (config.canvasWidth - 2)) { + tempx = (int) (( config.canvasWidth - imgload.width ) / 2) ; + } + + img.copy(imgload, 0, 0, imgload.width, imgload.height, tempx, tempy, imgload.width, imgload.height); + + //if (config.invert) { + // img.filter(INVERT); + //} + + if (config.gammaCorrection) { + // Optional gamma correction for background image. + img.loadPixels(); + + float tempFloat; // 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, config.gamma))); + } + + 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 eliminate 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[config.maxParticles]; + + // Fill array by "rejection sampling" + int i = 0; + while (i < config.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 (config.invert) { + 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 settings () { + config = new Config(sketchPath() + "/config.txt"); + if (config.display == true) { + size(config.windowWidth, config.windowHeight, JAVA2D); + } +} + +void setup () { + if (!config.display) { + surface.setVisible(false); + } + canvas = createGraphics(config.canvasWidth, config.canvasHeight, JAVA2D); + gfx = new ToxiclibsSupport(this, canvas); + + lowBorderX = borderWidth; //config.canvasWidth*0.01; + hiBorderX = config.canvasWidth - borderWidth; //config.canvasWidth*0.98; + lowBorderY = borderWidth; // config.canvasHeight*0.01; + hiBorderY = config.canvasHeight - borderWidth; //config.canvasHeight*0.98; + + int innerWidth = config.canvasWidth - 2 * borderWidth; + int innerHeight = config.canvasHeight - 2 * borderWidth; + + clip = new SutherlandHodgemanClipper(new Rect(lowBorderX, lowBorderY, innerWidth, innerHeight)); + + MainArraysetup(); // Main particle array setup + + int leftcolumwidth = 225; + + int GUItop = config.canvasHeight + 15; + int GUI2ndRow = 4; // Spacing for firt row after group heading + int GuiRowSpacing = 14; // Spacing for subsequent rows + int GUIFudge = config.canvasHeight + 19; // I wish that we didn't need ONE MORE of these stupid spacings. + + config.MaxDotSize = config.MinDotSize * (1 + config.DotSizeFactor); //best way to do this? + + ReInitiallizeArray = false; + showBG = false; + showPath = true; + showCells = false; + fileLoaded = false; + SaveNow = 0; + + background(0); +} + +/*** + * Callback for selectInput() in LOAD_FILE. + * Loads file if filetype is acceptable. + ***/ +void fileSelected (File selection) { + String[] acceptedExt = { "GIF", "JPG", "JPEG", "TGA", "PNG" }; + String[] parts; + String loadPath; + boolean fileOK = false; + + if (selection == null) { + println("Window was closed or the user hit cancel."); + } else { + //println("User selected " + selection.getAbsolutePath()); + loadPath = selection.getAbsolutePath(); + + // If a file was selected, print path to file + println("Loaded file: " + loadPath); + + parts = splitTokens(loadPath, "."); + + for (int i = 0; i < acceptedExt.length; i++) { + if ( parts[parts.length - 1].toUpperCase().equals(acceptedExt[i])) { + fileOK = true; + break; + } + } + + 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; + } + } + LoadImageAndScale(); + loop(); +} + +void LOAD_FILE () { + println(":::LOAD JPG, GIF or PNG FILE:::"); + selectInput("Select a file to process:", "fileSelected"); // Opens file chooser +} + +void SAVE_PATH() { + FileModeTSP = true; + SAVE_SVG(); +} + +void SAVE_STIPPLES () { + FileModeTSP = false; + SAVE_SVG(); +} + +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; + exit(); + } else { + config.outputSVG = selection.getAbsolutePath(); + String[] p = splitTokens(config.outputSVG, "."); + boolean fileOK = false; + + if ( p[p.length - 1].equals("SVG") || p[p.length - 1].equals("svg")) { + fileOK = true; + } + + if (fileOK == false) { + config.outputSVG = config.outputSVG + ".svg"; + } + + // If a file was selected, print path to folder + println("Save file: " + config.outputSVG); + SaveNow = 1; + showPath = true; + + ErrorDisplay = "SAVING FILE..."; + ErrorTime = millis(); + ErrorDisp = true; + } + loop(); +} + +void SAVE_SVG () { + noLoop(); + selectOutput("Output .svg file name:", "SaveFileSelected"); +} + +void QUIT(float theValue) { + exit(); +} + +void ORDER_ON_OFF(float theValue) { + if (showPath) { + showPath = false; + } else { + showPath = true; + } +} + +void CELLS_ON_OFF(float theValue) { + if (showCells) { + showCells = false; + } else { + showCells = true; + } +} + +void IMG_ON_OFF(float theValue) { + if (showBG) { + showBG = false; + } else { + showBG = true; + } +} + +void INVERT_IMG(float theValue) { + if (config.invert) { + config.invert = false; + } else { + config.invert = true; + } + ReInitiallizeArray = true; +} + +void Stipples(int inValue) { + if (config.maxParticles != (int) inValue) { + println("Update: Stipple Count -> " + inValue); + ReInitiallizeArray = true; + } +} + +void Min_Dot_Size(float inValue) { + if (config.MinDotSize != inValue) { + println("Update: Min_Dot_Size -> " + inValue); + config.MinDotSize = inValue; + config.MaxDotSize = config.MinDotSize* (1 + config.DotSizeFactor); + } +} + +void Dot_Size_Range(float inValue) { + if (config.DotSizeFactor != inValue) { + println("Update: Dot Size Range -> " + inValue); + config.DotSizeFactor = inValue; + config.MaxDotSize = config.MinDotSize* (1 + config.DotSizeFactor); + } +} + +void White_Cutoff(float inValue) { + if (config.cutoff != inValue) { + println("Update: White_Cutoff -> "+inValue); + config.cutoff = inValue; + RouteStep = 0; // Reset TSP path + } +} + +void DoBackgrounds() { + if (showBG) { + canvas.image(img, 0, 0); // Show original (cropped and scaled, but not blurred!) image in background + } else { + if (config.invert) { + canvas.background(0); + } else { + canvas.background(255); + } + } +} + +void OptimizePlotPath () { + println("Optimizing plot path..."); + int temp; + StatusDisplay = "Optimizing plotting path"; + Vec2D p1; + + if (RouteStep == 0) { + float cutoffScaled = 1 - config.cutoff; + // Begin process of optimizing plotting route, by flagging particles that will be shown. + particleRouteLength = 0; + + boolean particleRouteTemp[] = new boolean[config.maxParticles]; + + for (int i = 0; i < config.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 (config.invert) { + v = 1 - v; + } + + if (v < cutoffScaled) { + particleRouteTemp[i] = true; + particleRouteLength++; + } + } + + particleRoute = new int[particleRouteLength]; + int tempCounter = 0; + for (int i = 0; i < config.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 < config.testsPerFrame; i++) { + 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; + + 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 > config.maxParticles) { + temp = config.maxParticles; + } + + 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 >= config.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"; + + // This line: CentroidsPerPass (Feel free to edit this number.) + // Higher values give slightly faster computation, but a less responsive GUI. + // Default value: 500 + temp = cellsCalculated + config.centroidsPerPass; + + if (temp > cellsTotal) { + temp = cellsTotal; + } + + for (int i=cellsCalculated; i< temp; i++) { + float xMax = 0; + float xMin = config.canvasWidth; + float yMax = 0; + float yMin = config.canvasHeight; + 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 (config.invert) { + 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++; + frameTime = (millis() - millisLastFrame)/1000; + millisLastFrame = millis(); + } + } +} + +void draw () { + int i = 0; + int temp; + int scaledDimension; + float dotScale = (config.MaxDotSize - config.MinDotSize); + float cutoffScaled = 1 - config.cutoff; + + canvas.beginDraw(); + canvas.smooth(); + canvas.noStroke(); + + + if (ReInitiallizeArray) { + MainArraysetup(); + ReInitiallizeArray = false; + } + + doPhysics(); + + if ( showPath ) { + canvas.stroke(128, 128, 255); // Stroke color (blue) + canvas.strokeWeight (1); + + for ( i = 0; i < (particleRouteLength - 1); ++i) { + Vec2D p1 = particles[particleRoute[i]]; + Vec2D p2 = particles[particleRoute[i + 1]]; + + canvas.line(p1.x, p1.y, p2.x, p2.y); + } + } + + if (config.invert) { + canvas.stroke(255); + } else { + canvas.stroke(0); + } + + // 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. + canvas.strokeWeight(1); + canvas.noFill(); + + if (config.invert && (showBG == false)) { // TODO -- if config.invert AND NOT background + canvas.stroke(100); + } else { + canvas.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. + // Normal w/ Min & Max dot size + strokeWeight(config.MinDotSize); + for ( i = 0; i < config.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 (config.invert) + //v = 1 - v; + //strokeWeight (config.MaxDotSize - v * dotScale); + canvas.point(px, py); + } + } + } else { + // Stipple calculation is still underway + if (TempShowCells) { + DoBackgrounds(); + TempShowCells = false; + } + // stroke(0); // Stroke color + + if (config.invert) { + canvas.stroke(255); + } else { + canvas.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 (config.invert) { + v = 1 - v; + } + + if (v < cutoffScaled) { + canvas.strokeWeight(config.MaxDotSize - v * dotScale); + canvas.point(px, py); + } + } + + cellsCalculatedLast = cellsCalculated; + } + + canvas.endDraw(); + + if (config.display) { + if (mainRatio >= windowRatio) { + scaledDimension = round((float) height * mainRatio); + image(canvas, 0, (height - scaledDimension) / 2, width, scaledDimension); + } else { + scaledDimension = round((float) width / mainRatio); + image(canvas, (width - scaledDimension) / 2, 0, scaledDimension, height); + } + } + + if (Generation != lastGeneration) { + if (!TempShowCells && config.outputImage != null) { + canvas.save(config.outputImage); + } + println("Generation completed: " + Generation); + println("Generation time: " + frameTime + " s"); + lastGeneration = Generation; + } + + if (ErrorDisp) { + println(ErrorDisplay); + if ((millis() - ErrorTime) > 8000) { + ErrorDisp = false; + } + } else { + if (!lastStatusDisplay.equals(StatusDisplay)) { + println(StatusDisplay); + lastStatusDisplay = StatusDisplay; + } + } + + if (Generation == config.maxGenerations) { + SaveNow = 1; + } + + if (SaveNow > 0 && config.display && config.outputSVG == null) { + SAVE_SVG(); + return; + } + + if (SaveNow > 0 && config.outputSVG != null) { + OptimizePlotPath(); + StatusDisplay = "Saving SVG File"; + FileOutput = loadStrings("header.txt"); + String rowTemp; + + //Need to get some background on this. + //what are these magic numbers? + float SVGscale = (800.0 / (float) config.canvasHeight); + int xOffset = (int) (1600 - (SVGscale * config.canvasWidth / 2)); + int yOffset = (int) (400 - (SVGscale * config.canvasHeight / 2)); + + if (FileModeTSP) { + // Plot the PATH between the points only. + println("Saving TSP File (SVG)"); + println(config.outputSVG); + // Path header:: + rowTemp = ""); // End path description + } else { + println("Saving Stipple File (SVG)"); + println(config.outputSVG); + 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 (config.invert) { + v = 1 - v; + } + + float dotrad = (config.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(config.outputSVG, 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; + } else if (SaveNow > 0 && config.outputSVG == null) { + println("Exiting without exporting SVG"); + } + + if (SaveNow > 0) { + exit(); + } +} diff --git a/stipple_gen.sh b/stipple_gen.sh new file mode 100644 index 0000000..dc04b24 --- /dev/null +++ b/stipple_gen.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# processing-java requires the full path of the sketch +SKETCH=`realpath ./` + +# pass all arguments on bash script to stipple_gen.pde sketch +processing-java --sketch="${SKETCH}" --run \ + "${@}"