/** 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); } }