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
pull/12/head
Windell Oskay 2018-06-05 13:43:37 -07:00
rodzic 622c822172
commit f014669c85
1 zmienionych plików z 355 dodań i 57 usunięć

Wyświetl plik

@ -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<p2.height;y+=p2.height/ystep) {
liner = createShape(PShape.PATH);
liner.beginShape();
a = 0.0;
liner.vertex(0,y);
for (int x = 1;x<p2.width;x+=xstep) {
b = (int)alpha(p2.get(x,y));
b = max(minB,b);
z = max(maxB-b,0);
r = z/ystep*ymult;
a += z/xsmooth;
liner.vertex(x,y+sin(a)*r);
}
liner.endShape();
s.addChild(liner);
}
if (!isRecording)
background(255);
s.scale(1.0/imageScaleUp);
shape(s,isRecording ? 0 : 150,0);
float scaleFactor = 1.0/imageScaleUp;
float xOffset = isRecording ? 0 : 150;
float deltaPhase;
float deltaX;
float deltaAmpl;
/*
The minimum phase increment should give about 40 vertices minimum
across x. 40 vertices -> 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; y+=scaledYstep) {
if (!connectEnds)
{
beginShape();
}
oddRow = !oddRow;
if (y + (scaledYstep ) >= 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);
//}
//}