From f014669c85f70e176934429229e8929c84732efb Mon Sep 17 00:00:00 2001 From: Windell Oskay Date: Tue, 5 Jun 2018 13:43:37 -0700 Subject: [PATCH 1/2] Major rewrite of rendering - Rewrote curve generation to use curved splines instead of polylines that sample sine waves. Eliminates aliasing (e.g., at high frequency settings) and improves plot quality. - Added "connect ends" GUI control. Reverses every other line and adds curved endcaps to eliminate pen lifts while plotting - Cap maximum frequency, based on line width. - Reduce maximum value of frequency GUI control - Remove white background from generated PDF files - Added list of contributors - Automated Processing code cleanup --- SquiggleDraw/SquiggleDraw.pde | 412 +++++++++++++++++++++++++++++----- 1 file changed, 355 insertions(+), 57 deletions(-) diff --git a/SquiggleDraw/SquiggleDraw.pde b/SquiggleDraw/SquiggleDraw.pde index 7afe5ed..324ffa2 100644 --- a/SquiggleDraw/SquiggleDraw.pde +++ b/SquiggleDraw/SquiggleDraw.pde @@ -3,8 +3,24 @@ // A processing sketch by Gregg Wygonik // // https://github.com/gwygonik/SquiggleDraw -// -// Contributions + + +/* + +Additional credits + +Contributions by Maksim Surguy +https://github.com/msurguy + +Contributions by Ivan Moroz (sNow) +https://github.com/sNow32/a + +Contributions by Windell H. Oskay +www.evilmadscientist.com +https://github.com/evil-mad/ + +*/ + import controlP5.*; import processing.svg.*; @@ -17,7 +33,6 @@ PShape liner; PImage p1; PImage p2; - int ystep = 160; int ymult = 6; int xstep = 3; @@ -29,9 +44,9 @@ float r = 0.0; float a = 0.0; int strokeWidth = 1; -float startx,starty, z; +float startx, starty, z; -int b,oldb; +int b, oldb; int maxB = 255; int minB = 0; @@ -43,64 +58,70 @@ boolean needsReload = true; boolean invert = false; +boolean connectEnds = false; + //! TODO: scroll bar for big images + String imageName = "Rachel-Carson.jpg"; void setup() { - size(100,100); + size(100, 100); //surface.setResizable(true); loadMainImage(imageName); createSecondaryImage(); gui = new ControlP5(this); - gui.addSlider("sldLines").setSize(130,30).setCaptionLabel("Number of Lines").setPosition(10,20).setRange(10,200).setValue(120).setColorCaptionLabel(color(0)); + gui.addSlider("sldLines").setSize(130, 30).setCaptionLabel("Number of Lines").setPosition(10, 20).setRange(10, 200).setValue(120).setColorCaptionLabel(color(0)); gui.getController("sldLines").getCaptionLabel().align(ControlP5.LEFT, ControlP5.TOP_OUTSIDE); - gui.addToggle("tglInvert").setCaptionLabel("Invert Colors").setPosition(10,80).setValue(false).setMode(ControlP5.SWITCH).setColorCaptionLabel(color(0)); + gui.addToggle("tglInvert").setCaptionLabel("Invert Colors").setPosition(10, 80).setValue(false).setMode(ControlP5.SWITCH).setColorCaptionLabel(color(0)); gui.getController("tglInvert").getCaptionLabel().align(ControlP5.LEFT, ControlP5.TOP_OUTSIDE); - gui.addSlider("sldAmplitude").setSize(130,30).setCaptionLabel("Squiggle Strength").setPosition(10,140).setRange(0,20).setValue(13).setColorCaptionLabel(color(0)); + gui.addToggle("tglConnect").setCaptionLabel("Connect Ends").setPosition(80, 80).setValue(false).setMode(ControlP5.SWITCH).setColorCaptionLabel(color(0)); + gui.getController("tglConnect").getCaptionLabel().align(ControlP5.LEFT, ControlP5.TOP_OUTSIDE); + + gui.addSlider("sldAmplitude").setSize(130, 30).setCaptionLabel("Squiggle Strength").setPosition(10, 140).setRange(0, 20).setValue(13).setColorCaptionLabel(color(0)); gui.getController("sldAmplitude").getCaptionLabel().align(ControlP5.LEFT, ControlP5.TOP_OUTSIDE); - gui.addSlider("sldXSpacing").setSize(130,30).setCaptionLabel("Detail").setPosition(10,200).setRange(1,30).setValue(28).setColorCaptionLabel(color(0)); + gui.addSlider("sldXSpacing").setSize(130, 30).setCaptionLabel("Detail").setPosition(10, 200).setRange(1, 30).setValue(28).setColorCaptionLabel(color(0)); gui.getController("sldXSpacing").getCaptionLabel().align(ControlP5.LEFT, ControlP5.TOP_OUTSIDE); - gui.addSlider("sldXFrequency").setSize(130,30).setCaptionLabel("Frequency").setPosition(10,260).setRange(5.0,256.0).setValue(128.0).setColorCaptionLabel(color(0)); + gui.addSlider("sldXFrequency").setSize(130, 30).setCaptionLabel("Frequency").setPosition(10, 260).setRange(5.0, 200.0).setValue(128.0).setColorCaptionLabel(color(0)); gui.getController("sldXFrequency").getCaptionLabel().align(ControlP5.LEFT, ControlP5.TOP_OUTSIDE); - gui.addSlider("sldImgScale").setSize(130,30).setCaptionLabel("Resolution Scale").setPosition(10,320).setRange(1,3).setValue(3).setColorCaptionLabel(color(0)); + gui.addSlider("sldImgScale").setSize(130, 30).setCaptionLabel("Resolution Scale").setPosition(10, 320).setRange(1, 3).setValue(3).setColorCaptionLabel(color(0)); gui.getController("sldImgScale").getCaptionLabel().align(ControlP5.LEFT, ControlP5.TOP_OUTSIDE); - gui.addSlider("lineWidth").setSize(130,30).setCaptionLabel("Line Width").setPosition(10,380).setRange(1,10).setValue(5).setColorCaptionLabel(color(0)); + gui.addSlider("lineWidth").setSize(130, 30).setCaptionLabel("Line Width").setPosition(10, 380).setRange(1, 10).setValue(5).setColorCaptionLabel(color(0)); gui.getController("lineWidth").getCaptionLabel().align(ControlP5.LEFT, ControlP5.TOP_OUTSIDE); - gui.addSlider("minBrightness").setSize(130,30).setCaptionLabel("Black Point").setPosition(10,440).setRange(0,255).setValue(0).setColorCaptionLabel(color(0)); + gui.addSlider("minBrightness").setSize(130, 30).setCaptionLabel("Black Point").setPosition(10, 440).setRange(0, 255).setValue(0).setColorCaptionLabel(color(0)); gui.getController("minBrightness").getCaptionLabel().align(ControlP5.LEFT, ControlP5.TOP_OUTSIDE); - gui.addSlider("maxBrightness").setSize(130,30).setCaptionLabel("White Point").setPosition(10,500).setRange(0,255).setValue(255).setColorCaptionLabel(color(0)); + gui.addSlider("maxBrightness").setSize(130, 30).setCaptionLabel("White Point").setPosition(10, 500).setRange(0, 255).setValue(255).setColorCaptionLabel(color(0)); gui.getController("maxBrightness").getCaptionLabel().align(ControlP5.LEFT, ControlP5.TOP_OUTSIDE); // added: .setTriggerEvent(Bang.RELEASE) // now you don't have to click 's' to save. save button work fine now. - gui.addBang("bangLoad").setSize(130,30).setTriggerEvent(Bang.RELEASE).setCaptionLabel("Load image").setPosition(10,600).setColorCaptionLabel(color(255)); + gui.addBang("bangLoad").setSize(130, 30).setTriggerEvent(Bang.RELEASE).setCaptionLabel("Load image").setPosition(10, 600).setColorCaptionLabel(color(255)); gui.getController("bangLoad").getCaptionLabel().align(ControlP5.CENTER, ControlP5.CENTER); - gui.addBang("bangSave").setSize(130,30).setCaptionLabel("Save SVG").setPosition(10,660).setColorCaptionLabel(color(255)); + gui.addBang("bangSave").setSize(130, 30).setCaptionLabel("Save SVG").setPosition(10, 660).setColorCaptionLabel(color(255)); gui.getController("bangSave").getCaptionLabel().align(ControlP5.CENTER, ControlP5.CENTER); - + // add 'default' button - gui.addBang("bangDefault").setSize(130,30).setCaptionLabel("Default").setPosition(10,720).setColorCaptionLabel(color(255)); + gui.addBang("bangDefault").setSize(130, 30).setCaptionLabel("Default").setPosition(10, 720).setColorCaptionLabel(color(255)); gui.getController("bangDefault").getCaptionLabel().align(ControlP5.CENTER, ControlP5.CENTER); - + //// add 'fit' button. fit image to window size //gui.addBang("bangFit").setSize(65, 30).setCaptionLabel("Fit").setPosition(10, 780).setColorCaptionLabel(color(255)); //gui.getController("bangFit").getCaptionLabel().align(ControlP5.CENTER, ControlP5.CENTER); - + //// add 'full' button. load orig image size //gui.addBang("bangFull").setSize(65, 30).setCaptionLabel("Full").setPosition(10 + 66, 780).setColorCaptionLabel(color(255)); //gui.getController("bangFull").getCaptionLabel().align(ControlP5.CENTER, ControlP5.CENTER); - + smooth(); background(255); shapeMode(CORNER); @@ -111,31 +132,32 @@ void loadMainImage(String inImageName) { println("loadMainImage"); //isInit = true; p1 = loadImage(inImageName); - + int tempheight = p1.height; if (tempheight < 720 + 120) tempheight = 720 + 120; - + surface.setSize(p1.width + 150, tempheight); // filter image p1.filter(GRAY); - p1.filter(BLUR,2); + p1.filter(BLUR, 2); if (invert) { p1.filter(INVERT); } - + needsReload = true; redrawImage(); } void createSecondaryImage() { - p2 = createImage(p1.width*imageScaleUp,p1.height*imageScaleUp,ALPHA); - p2.copy(p1,0,0,p1.width,p1.height,0,0,p1.width*imageScaleUp,p1.height*imageScaleUp); + p2 = createImage(p1.width*imageScaleUp, p1.height*imageScaleUp, ALPHA); + p2.copy(p1, 0, 0, p1.width, p1.height, 0, 0, p1.width*imageScaleUp, p1.height*imageScaleUp); } void draw() { if (isRunning) { + if (isRecording) { // save to file // was: beginRecord(SVG, "squiggleImage_" + millis() + ".svg"); @@ -162,40 +184,309 @@ void createPic() { createSecondaryImage(); needsReload = false; } - + stroke(0); noFill(); strokeWeight(strokeWidth); - + startx = 0.0; starty = 0.0; - s = createShape(GROUP); - - for (int y=0;y 10 * 2 pi. + */ + float minPhaseIncr = 10 * TWO_PI / (p2.width / xstep); + + /* + Maximum phase increment (frequency cap) is based on line thickness and x step size. + + A full period of oscillation needn't be less than + 2 * strokeWidth in total width. + + The maximum number of full cycles that should be permitted in a + horizontal distance of xstep should be: + N = total width/width per cycle = xstep / (2 * strokeWidth) + + The maximum phase increment in distance xstep should then be: + + maxPhaseIncr = 2 Pi * N = 2 * Pi * xstep / (2 * strokeWidth) + = 2Pi * xstep / strokeWidth + + We do not need to include the scaling factors, since + both the step size and stroke width are scaled the same way. + */ + + + float maxPhaseIncr = TWO_PI * xstep / strokeWidth; + + strokeWeight(strokeWidth * scaleFactor); + + if (connectEnds) + { + beginShape(); + } + + boolean oddRow = false; + boolean finalRow = false; + boolean reverseRow; + float lastX; + float scaledYstep = p2.height/ystep; + + for (int y=0; y= p2.height) + finalRow = true; + + if (connectEnds && !oddRow) + reverseRow = true; + else + reverseRow = false; + + a = 0.0; + + // Add initial "extra" point to give splines a consistent visual endpoint, + // IF we are not connecting rows. + + if (reverseRow) + { + if (!connectEnds || y == 0) + { + // Always add the extra initial point if we're not connecting the ends, or if this is the first row. + curveVertex(xOffset + scaleFactor * (p2.width + 0.1 * xstep), scaleFactor * y); + } + curveVertex(xOffset + scaleFactor * (p2.width), scaleFactor * y); + } else + { + if (!connectEnds || y == 0) + { + // Always add the extra initial point if we're not connecting the ends, or if this is the first row. + curveVertex(xOffset - scaleFactor * ( 0.1 * xstep), y * scaleFactor); + } + curveVertex(xOffset, y * scaleFactor); + } + + + + /* + Step along width of image. + + For each step, get the image brightness for that XY position, + and constrain it to our bright/dark cutoff window. + + Accumulated phase: increment by scaled brightness, so that the frequency + increases in certain areas of the image. Phase only advances with pigment, + not simply by traversing across the image in X. + + Amplitude: A simple multiplier based on local brightness. + + To have high quality generated curves for display and plotting, we would like to: + + (1) Avoid aliasing. Aliasing happens when we plot a signal at a poorly + representative set of points. By undersampling -- e.g., less than once per + period -- you can very easily see what appears to be a sine wave, but does + not actually represent the actual function being sampled. + + Two potential methods to avoid aliasing: + (A) Increase the number of points, to ensure that some minimum number + of points are sampeled per period, or + (B) Plot the function at specific points {x_i} that are determined by + the value of the function f(x) at those points, e.g., at every crest, + trough, and zero crossing. + + (2) Place relatively few control points. + CNC software tends to follow simply defined curves more easily than + paths with a great many closely-spaced points. + Side benefit: Potentially smaller file size. + + (3) Place an upper bound on the maximum frequency. + Above a certain frequency, with a finite-width pen, increasing the frequency + does not make the plot any darker. + + + To achieve these goals, we will try: + + (1) Putting x-points (vertices) at every crest, trough, and zero crossing. + Point x-positions may be approximated as necessary by interpolation. + + (2) Using Processing's curveVertex method, to create curvy lines + (Catmull–Rom splines). These will only approximate sine waves, but + should work well for this particular application. + + (3) Using the GUI line-width control to control the maximum frequency. + + */ + + float phase = 0.0; + float lastPhase = 0; // accumulated phase at previous vertex + float lastAmpl = 0; // amplitude at previous vertex + boolean finalStep = false; + + int x; + + if (reverseRow) + { + x = p2.width; + lastX = p2.width; + } else + { + x = 1; + lastX = 1; + } + + + while (finalStep == false) { // Iterate over each each x-step in the row + + if (reverseRow) + { // Moving left to right on even rows + x -= xstep; + if (x - xstep < 1) + finalStep = true; + else + finalStep = false; + } else + { // Moving right to left as usual + x += xstep; + if (x + xstep >= p2.width) + finalStep = true; + else + finalStep = false; + } + + b = (int)alpha(p2.get(x, y)); + b = max(minB, b); + z = max(maxB-b, 0); // Brightness trimmed to range. + + r = z/ystep*ymult; // ymult: Amplitude + + /* + Enforce a minimum phase increment, to prevent large gaps in splines + This will add extra vertices in flat regions, but the amplitude remains + unaffected (near-zero amplitude), so it does not cause a significant + visual effect. + */ + + float df = z/xsmooth; + if (df < minPhaseIncr) + df = minPhaseIncr; + + /* + Enforce a maximum phase increment -- a frequency cap -- to prevent + unnecessary plotting time. Once the frequency is so high that the line widths + of neighboring crests overlap, there is no added benefit to having higher + frequency; it's just wasting memory (and ink + time, if plotting). + */ + + if (df > maxPhaseIncr) + df = maxPhaseIncr; + + phase += df; // xsmooth: Frequency + + deltaX = x - lastX; // Distance between image sample location x and previous vertex + + deltaAmpl = r - lastAmpl; + + deltaPhase = phase - lastPhase; // Change in phase since last *vertex* + // (Vertices do not fall along the x "grid", but where they need to.) + + if (!finalStep) // Skip to end points if this is the final step. + if (deltaPhase > HALF_PI) // Only add vertices if true. + { + /* + Linearly interpolate phase and amplitude since last vertex added. + This treats the frequency as constant + between subsequent x-samples of the source image. + */ + + int vertexCount = floor( deltaPhase / HALF_PI); // Add this many vertices + + float integerPart = ((vertexCount * HALF_PI) / deltaPhase); + // "Integer" fraction (in terms of pi/2 phase segments) of deltaX. + + float deltaX_truncate = deltaX * integerPart; + // deltaX_truncate: "Integer" part (in terms of pi/2 segments) of deltaX. + + float xPerVertex = deltaX_truncate / vertexCount; + float amplPerVertex = (integerPart * deltaAmpl) / vertexCount; + + // Add the vertices: + for (int i = 0; i < vertexCount; i = i+1) { + + lastX = lastX + xPerVertex; + lastPhase = lastPhase + HALF_PI; + lastAmpl = lastAmpl + amplPerVertex; + + curveVertex(xOffset + scaleFactor * lastX, scaleFactor *(y+sin(lastPhase)*lastAmpl)); + } + } else if (finalStep) + { // Add a vertex if this is the last point on the row, + // even if there's not a full pi/2 of phase. + + curveVertex(xOffset + scaleFactor * x, scaleFactor *(y+sin(phase)*r)); + lastX = x; + } + } + + // Add final "extra" point to give splines a consistent visual endpoint: + if (reverseRow) + { + curveVertex(xOffset, y * scaleFactor); + if (!connectEnds || finalRow) + { + // Always add the extra final point if we're not connecting the ends, or if this is the first row. + curveVertex(xOffset - scaleFactor * ( 0.1 * xstep), y * scaleFactor); + } + } else + { + curveVertex(xOffset + scaleFactor * (p2.width), scaleFactor * y); + if (!connectEnds || finalRow) + { + // Always add the extra final point if we're not connecting the ends, or if this is the first row. + curveVertex(xOffset + scaleFactor * (p2.width + 0.1 * xstep), scaleFactor * y); + } + } + + + if (connectEnds && !finalRow) // Add curvy end connectors + if (reverseRow) + { + curveVertex(xOffset - scaleFactor * ( 0.1 * xstep + scaledYstep/3), (y + scaledYstep/2) * scaleFactor ); + } else + { + curveVertex(xOffset + scaleFactor * (p2.width + 0.1 * xstep + scaledYstep/3), (y + scaledYstep/2) * scaleFactor ); + } + + + if (!connectEnds) + { + endShape(); + } + } + + if (connectEnds) + { + endShape(); + } } + + void fileSelected(File selection) { if (selection == null) { println("Window was closed or the cancel selected."); @@ -232,7 +523,7 @@ void fileSelected(File selection) { loadMainImage(imageName); createSecondaryImage(); redrawImage(); - } else { + } else { // Can't load file println("ERROR: BAD FILE TYPE"); } @@ -271,6 +562,12 @@ void tglInvert(boolean value) { redrawImage(); } +void tglConnect(boolean value) { + connectEnds = value; + needsReload = true; + redrawImage(); +} + void lineWidth(int value) { strokeWidth = value; redrawImage(); @@ -310,18 +607,19 @@ void redrawImage() { } void keyPressed() { - if (key == ' ') { + if (key == ' ') { // nothing here } else if (key == 's') { // save isRecording = true; isRunning = true; redraw(); - } + } } void bangDefault() { gui.getController("sldLines").setValue(120); gui.getController("tglInvert").setValue(0); + gui.getController("tglConnect").setValue(0); gui.getController("sldAmplitude").setValue(13); gui.getController("sldXSpacing").setValue(28); gui.getController("sldXFrequency").setValue(128); @@ -342,4 +640,4 @@ void bangDefault() { // println("Full"); // isInit = false; // loadMainImage(imageName); -//} +//} \ No newline at end of file From fee5ebe3a24024e50b3ac97b377c3d379de45374 Mon Sep 17 00:00:00 2001 From: Windell Oskay Date: Wed, 6 Jun 2018 11:25:09 -0700 Subject: [PATCH 2/2] Remove potentially redundant vertex --- SquiggleDraw/SquiggleDraw.pde | 40 +++++++++++++++-------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/SquiggleDraw/SquiggleDraw.pde b/SquiggleDraw/SquiggleDraw.pde index 324ffa2..5c9b6ed 100644 --- a/SquiggleDraw/SquiggleDraw.pde +++ b/SquiggleDraw/SquiggleDraw.pde @@ -6,20 +6,20 @@ /* - -Additional credits - -Contributions by Maksim Surguy -https://github.com/msurguy - -Contributions by Ivan Moroz (sNow) -https://github.com/sNow32/a - -Contributions by Windell H. Oskay -www.evilmadscientist.com -https://github.com/evil-mad/ - -*/ + + Additional credits + + Contributions by Maksim Surguy + https://github.com/msurguy + + Contributions by Ivan Moroz (sNow) + https://github.com/sNow32/a + + Contributions by Windell H. Oskay + www.evilmadscientist.com + https://github.com/evil-mad/ + + */ import controlP5.*; @@ -193,7 +193,7 @@ void createPic() { starty = 0.0; if (!isRecording) - background(255); + background(255); float scaleFactor = 1.0/imageScaleUp; float xOffset = isRecording ? 0 : 150; @@ -221,7 +221,7 @@ void createPic() { The maximum phase increment in distance xstep should then be: maxPhaseIncr = 2 Pi * N = 2 * Pi * xstep / (2 * strokeWidth) - = 2Pi * xstep / strokeWidth + = 2Pi * xstep / strokeWidth We do not need to include the scaling factors, since both the step size and stroke width are scaled the same way. @@ -405,7 +405,7 @@ void createPic() { deltaPhase = phase - lastPhase; // Change in phase since last *vertex* // (Vertices do not fall along the x "grid", but where they need to.) - if (!finalStep) // Skip to end points if this is the final step. + if (!finalStep) // Skip to end points if this is the last point in the row. if (deltaPhase > HALF_PI) // Only add vertices if true. { /* @@ -434,12 +434,6 @@ void createPic() { curveVertex(xOffset + scaleFactor * lastX, scaleFactor *(y+sin(lastPhase)*lastAmpl)); } - } else if (finalStep) - { // Add a vertex if this is the last point on the row, - // even if there's not a full pi/2 of phase. - - curveVertex(xOffset + scaleFactor * x, scaleFactor *(y+sin(phase)*r)); - lastX = x; } }