/** * Uses IgnoCodeLib 0.3 Processing library, http://paulhertz.net/ignocodelib/ * not compatible with earlier versions. * * Demo applet that creates blobby shapes as BezShapes, general shapes composed of vertices and * Bezier vertices, defined in my IgnoCodeLib. Also shows how to sort an array of BezShapes, * how to rotate each shape around its center, how to draw control and anchor points, how to save to file. * Sorry, I have not yet written a Bezier shape editor to move those points and control points around * interactively, though the DrawCurves sample code that comes with the IgnoCodeLib library shows how you * might start to do create one. * * Click to create a blob. * Type 'k' to show/hide control and anchor points. * Type 'o' to change sort order of shapes (steps through 4 different orders). * Type 'r' to rotate shapes. * */ import net.paulhertz.aifile.*; import net.paulhertz.util.RandUtil; import net.paulhertz.geom.*; import java.awt.geom.Point2D; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.ListIterator; int bgFillColor; /** a random number generator */ RandUtil rand; /** * Given a circle that is divided into 4 sectors of 90 degrees: * KAPPA = (distance between Bezier anchor and its associated control point) / (circle radius) * see http://www.whizkidtech.redprince.net/bezier/circle/kappa/ */ float kappa = 0.5522847498f; /** modifiable version of kappa, try multiplying kappa by different factors */ float kk = kappa; /** array to hold shapes */ ArrayList shapes; /** variance in angular measurement of sectors (positive float) */ float sectorVariance = 0.2f; /** variance in amount of curvature (positive float) */ float curveVariance = 0.1f; /** variance in length of radius at each Bezier anchor point (positive float) */ float radiusVariance = 0.16f; /** flag whether to show or hide control points and blob skeleton */ boolean showSkeleton = false; /** select sorting technique based on value of sortSelector */ int sortSelector = 0; /** document to save */ DocumentComponent document; /** white color */ int white = color(255, 255, 255); /** IgnoCodeLib library */ IgnoCodeLib igno; public void setup() { size(640, 480); smooth(); bgFillColor = color(255); rand = new RandUtil(); // Set up the library, which will store a reference to the host PApplet // for other classes in the library to use. igno = new IgnoCodeLib(this); shapes = new ArrayList(); PApplet.println("Click to create a blob."); PApplet.println("Type 'k' to show/hide control and anchor points."); PApplet.println("Type 'o' to change sort order of shapes (steps through 4 different orders)."); PApplet.println("Press 'r' to rotate shapes."); } public void draw() { background(bgFillColor); for (BezShape bez: shapes) { drawBezShape(bez, showSkeleton); } } public void mouseReleased() { // prepare to create a random blob int sectors = rand.randomInRange(4, 13); float rad = (float) rand.gauss(60, 0.25); rad = rad * PApplet.map(mouseY, 0, height, 0.5f, 2.0f); do { sectorVariance = ((float) rand.quickGauss(0.1, 0.001)); } while (sectorVariance < 0); curveVariance = abs((float) rand.quickGauss(0.0, 0.001)); radiusVariance = abs((float) rand.quickGauss(0.0, 0.005)); float angle = PApplet.radians(rand.randomInRange(0, 90)); int[] channelValues = {64, 96, 128, 160, 192, 224}; int farb = Palette.randColor(channelValues); float fade = map(mouseY, 0, height, 0.3f, 1); farb = lerpColor(white, farb, fade); BezShape shape = createBlob(sectors, rad, sectorVariance, curveVariance, radiusVariance, angle, mouseX, mouseY, farb); shapes.add(shape); } public void keyPressed() { if (key == 'r' || key == 'R') { // rotate shapes for (BezShape bez : shapes) { bez.rotateShape(PApplet.radians(3)); } } } public void keyReleased() { if (key == 'k' || key == 'K') { // hide/show Bezier control and anchor points showSkeleton = !showSkeleton; } else if (key == 'o' || key == 'O') { // sort shapes if (0 == sortSelector) { Collections.sort(shapes, new CompareX()); PApplet.println("sorting on X coordinate"); } else if (1 == sortSelector) { Collections.sort(shapes, new CompareY()); PApplet.println("sorting on Y coordinate"); } else if (2 == sortSelector) { Collections.sort(shapes, new CompareCtr(mouseX, mouseY)); println("sorting on mouse location: " + mouseX +" "+ mouseY); } else if (3 == sortSelector) { Collections.sort(shapes, new CompareAng(mouseX, mouseY)); println("sorting by angle on mouse location: " + mouseX +" "+ mouseY); } sortSelector = (sortSelector + 1) % 4; } else if (key == 's' || key == 'S') { createDocument(); saveAI("blobby.ai", document); } } /** * creates a blobby Bezier shape * sectorVariance of 0 sets all sectors equal, value > 1 may result in negative sector angles * curveVariance of 0 sets all curvatures to approximate a circle * radiusVariance of 0 sets all radial distances equal (to radius variable) * for example, createBlob(5, radius, 0.1, 0.0, 0.0) will draw circles with 5 slightly irregular sectors * @param sectors number of sectors * @param rad radius * @param sectorVariance variance in angular measurement of sectors (positive float) * @param curveVariance variance in amount of curvature (positive float) * @param radiusVariance variance in length of radius at each Bezier anchor point (positive float) * @param rot radians to rotate sectors * @param dx x-coordinate of center point (used for transforms) * @param dy y-coordinate of center point (used for transforms) * @param farb fill color for blob */ public BezShape createBlob(int sectors, float rad, float sectorVariance, float curveVariance, float radiusVariance, float rot, float dx, float dy, int farb) { float[] sects = new float[sectors]; float sum = 0; // fill the sects array with a series of increasing numbers at a nearly uniform // distance from each other, with a variability determined by sectorVariance for (int i = 0; i < sectors; i++) { float num = (float) rand.quickGauss(2, sectorVariance); sum = sum + num; sects[i] = num; // println("sects[" + i + "] = " + num); } // println("sum = " + sum + "\n"); // scale the sects array to fill a range of 2 * pi, its values will determine // the angles between anchor points with respect to the center point dx, dy sects = reapportion(sects, PApplet.TWO_PI); // calculate the factor k that determines the distance between // a Bezier anchor point and its associated control points float k = (4 * kk)/PApplet.TWO_PI; float d; // local variables: starting point, control point 1, control point 2, anchor point Point2D.Float pt, cp1, cp2, ap1; // cumulative rotation from the starting point float theta = 0; float kfac = (float) rand.quickGauss(1, curveVariance); float rfac = (float) rand.quickGauss(1, radiusVariance); float r = rad * rfac; float r0 = r; // starting point pt = GeomUtils.rotateCoor(r0, 0, rot); // an array for the points, one to start with, then three more for each Bezier vertex ArrayList bezPoints = new ArrayList(); // translate starting point by dx, dy and add it to the array bezPoints.add(GeomUtils.translateCoor(pt, dx, dy)); for (int i = 0; i < sectors; i++) { kfac = (float) rand.quickGauss(1, curveVariance); // calculate distance between anchor point and control points d = sects[i] * k * r * kfac; // first control point cp1 = GeomUtils.rotateCoor(r, d, theta + rot); bezPoints.add(GeomUtils.translateCoor(cp1, dx, dy)); theta += sects[i]; if (i != sectors - 1) { rfac = (float) rand.quickGauss(1, radiusVariance); r = rad * rfac; } else { r = r0; } // second control point cp2 = GeomUtils.rotateCoor(r, -d, theta + rot); bezPoints.add(GeomUtils.translateCoor(cp2, dx, dy)); // anchor point ap1 = GeomUtils.rotateCoor(r, 0, theta + rot); bezPoints.add(GeomUtils.translateCoor(ap1, dx, dy)); } // we have the points, now make a Bezier shape pt = bezPoints.get(0); BezShape bezo = new BezShape(pt.x, pt.y); bezo.setCenter(dx, dy); bezo.setFillColor(farb); bezo.setNoStroke(); for (int i = 1; i < bezPoints.size(); i += 3) { cp1 = bezPoints.get(i); cp2 = bezPoints.get(i + 1); ap1 = bezPoints.get(i + 2); bezo.append(cp1.x, cp1.y, cp2.x, cp2.y, ap1.x, ap1.y); } return bezo; } /** * @param series an array of floats, all > 1.0 * @param range the desired scaled sum of the values in series * @return the array with its values scaled so they sum to range */ public float[] reapportion(float[] series, float range) { float sum = 0; for (int i = 0; i < series.length; i++) { sum += series[i]; } float d = range/sum; for (int i = 0; i < series.length; i++) { series[i] *= d; } sum = 0; for (int i = 0; i < series.length; i++) { float num = series[i]; sum += num; // println("sects[" + i + "] = " + num); } // println("sum = " + sum + "\n"); return series; } /** * draws a BezShape, optionally showing its construction lines * @param bShape the BezShape to draw * @param showSkeleton true if you want Bezier knots and other construction lines to show */ void drawBezShape(BezShape bShape, boolean showSkeleton) { bShape.draw(); if (showSkeleton) { Point2D.Float pt; float dx = bShape.xctr(); float dy = bShape.yctr(); pt = new Point2D.Float(bShape.x(), bShape.y()); stroke(246, 233, 0); line(dx, dy, pt.x, pt.y); ListIterator it = bShape.curveIterator(); while (it.hasNext()) { Vertex2DINF bv = it.next(); strokeWeight(0.5f); if (BezShape.CURVE_SEGMENT == bv.segmentType()) { float[] coords = bv.coords(); stroke(64); fill(160); line(pt.x, pt.y, coords, coords); line(coords, coords, coords, coords); noStroke(); ellipse(coords, coords, 5, 5); ellipse(coords, coords, 5, 5); pt.setLocation(coords, coords); stroke(255); line(dx, dy, pt.x, pt.y); } } } } /** * Builds a document for the shapes we created. */ public void createDocument() { document = new DocumentComponent("Blobs"); // get lots of feedback as we save document.setVerbose(true); document.setCreator("Ignotus"); document.setOrg("IgnoStudio"); document.setWidth(width); document.setHeight(height); // get the palette for the document Palette pal = document.getPalette(); // add default black, white and gray colors pal.addBlackWhiteGray(); // now add each of our fill colors. // Palette is a set (LinkedHashSet) that will not repeat a color. for (BezShape bez : shapes) { pal.addColor(bez.fillColor()); } // create a layer LayerComponent shapeLayer = new LayerComponent("Shapes"); // background rectangle BezRectangle rect = BezRectangle.makeLeftTopWidthHeight(0, 0, width, height); rect.setFillColor(bgFillColor); // add background rectangle and shapes to layer shapeLayer.add(rect); shapeLayer.add(shapes); // add layer to document document.add(shapeLayer); } /** * Saves a document in Adobe Illustrator 7.0 format, transforming it into AI coordinate system. * @param aiFilename name of the file * @param doc a DocumentComponent to save */ private void saveAI(String aiFilename, DocumentComponent doc) { PrintWriter output = createWriter(aiFilename); // In IgnoCodeLib 0.3, write automatically applies a geometric transform // (reflect on horizontal axis) so that the saved file has the same orientation // as the Processing display. Call writeNoTransform to write without the transform. document.write(output); } /************* Comparator classes, used for sorting shapes *************/ /** * sorts shapes by center point increasing on x-axis */ public class CompareX implements Comparator { public int compare(BezShape sh1, BezShape sh2) { float x1 = sh1.xctr(); float y1 = sh1.yctr(); float x2 = sh2.xctr(); float y2 = sh2.yctr(); if (x1 < x2) return -1; if (x1 > x2) return 1; if (y1 < y2) return -1; if (y1 > y2) return 1; return 0; } } /** * sorts shapes by center point increasing on y-axis */ public class CompareY implements Comparator { public int compare(BezShape sh1, BezShape sh2) { float x1 = sh1.xctr(); float y1 = sh1.yctr(); float x2 = sh2.xctr(); float y2 = sh2.yctr(); if (y1 < y2) return -1; if (y1 > y2) return 1; if (x1 < x2) return -1; if (x1 > x2) return 1; return 0; } } /** * Sorts shapes based on distance of center from initial point; * farthest points will come before nearest points. */ public class CompareCtr implements Comparator { float xctr = 0; float yctr = 0; public CompareCtr(float xctr, float yctr) { this.xctr = xctr; this.yctr = yctr; } public int compare(BezShape sh1, BezShape sh2) { float x1 = sh1.xctr(); float y1 = sh1.yctr(); float x2 = sh2.xctr(); float y2 = sh2.yctr(); // compare squares of distances float d1 = (x1 - xctr) * (x1 - xctr) + (y1 - yctr) * (y1 - yctr); float d2 = (x2 - xctr) * (x2 - xctr) + (y2 - yctr) * (y2 - yctr); if (d1 > d2) return -1; if (d1 < d2) return 1; return 0; } } /** * Compares angles two points make with intial point. */ public class CompareAng implements Comparator { float xctr = 0; float yctr = 0; public CompareAng(float xctr, float yctr) { this.xctr = xctr; this.yctr = yctr; } public int compare(BezShape sh1, BezShape sh2) { float x1 = sh1.xctr(); float y1 = sh1.yctr(); float x2 = sh2.xctr(); float y2 = sh2.yctr(); float ang1 = getAngle(x1, y1); float ang2 = getAngle(x2, y2); if (ang1 > ang2) return -1; if (ang1 < ang2) return 1; float d1 = (x1 - xctr) * (x1 - xctr) + (y1 - yctr) * (y1 - yctr); float d2 = (x2 - xctr) * (x2 - xctr) + (y2 - yctr) * (y2 - yctr); if (d1 > d2) return -1; if (d1 < d2) return 1; return 0; } float getAngle(float x, float y) { // return the angle of rotation measured clockwise from 0 // degrees in radians double ang; float dx = x - xctr; float dy = y - yctr; if (dx != 0) { ang = Math.atan(Math.abs(dy/dx)); } else if (dy != 0) { ang = PI / 2; } else { ang = 0; } if (dx < 0) { if (dy < 0) { ang = ang + PI; } else { ang = PI - ang; } } else if (dy < 0) { ang = 2 * PI - ang; } return (float) ang; } } public boolean pointInShape(BezShape blob, float x, float y) { BezRectangle b = (blob.boundsRect()); if (b.containsPoint(x, y)) { if (blob.containsPoint(x, y)) { return true; } } return false; }