/* built with Studio Sketchpad:
* https://sketchpad.cc
*
* observe the evolution of this sketch:
* https://studio.sketchpad.cc/sp/pad/view/ro.9PB3-n7u4uu/rev.6978
*
* authors:
*
*
* Felix Woitzel
*
*
*
*
*
*
*
*
*
*
*
*
*
* license (unless otherwise specified):
* creative commons attribution-share alike 3.0 license.
* https://creativecommons.org/licenses/by-sa/3.0/
*/
int w = 1024, h = 512;
bool initRandomly = true;
int initialPoints = 3;
int[] coordinates = new int[6];
coordinates[0] = 50; // x1
coordinates[1] = 450; // y1
coordinates[2] = 350; // x2
coordinates[3] = 150; // y2
coordinates[4] = 650; // x3
coordinates[5] = 450; // y3
String SELECT = "Move";
String ADD = "Add";
String REMOVE = "Delete";
String mode = ADD;
int hudColor = color(255);
int hudSelectedModeColor = color(255, 128, 0);
ControlPoint listStart = new ControlPoint();
ControlPoint listEnd = listStart;
int controlStroke = color(255);
int controlStrokeSelected = color(255, 0, 0);
int controlStrokeNew = color(255, 255, 255);
int selectionMaskStroke = color(128);
ControlPoint cursorPos = new ControlPoint();
boolean cursorIsActive = false;
int curveColor = color(0, 255, 0);
int curveColorNew = color(0, 255, 0);
int curveColorRemove = color(64, 32, 0);
ControlPoint nearestSegmentAnchor = listStart;
float thresholdDistance = min(w, h) / 3;
boolean fallbackToSelect = false;
public void setup() {
size(1024, 512);
listStart.stroke = controlStroke;
for (int i = 0; i < initialPoints; i++) {
if(initRandomly) {
ControlPoint p = new ControlPoint(random(w - 10), random(h - 50) + 40);
}else{
p = new ControlPoint(coordinates[i*2], coordinates[i*2+1]);
}
p.stroke = controlStroke;
if(i==0){
listStart = p;
}
listEnd.next = p;
listEnd = p;
}
cursorPos.stroke = controlStrokeNew;
textFont(createFont("Arial",20));
updateLength();
serial = serialize();
}
public void draw() {
background(0);
drawHUD();
noFill();
drawCurve();
if(cursorIsActive) {
cursorPos.draw(this);
}
ControlPoint current = listStart;
while(current != null) {
current.draw(this);
current = current.next;
}
if (multiSelect) {
strokeWeight(1);
stroke(selectionMaskStroke);
rect(selectionStartPos.x, selectionStartPos.y,
selectionEndPos.x - selectionStartPos.x,
selectionEndPos.y - selectionStartPos.y);
}
}
int pointCount = initialPoints - 1;
float pathLength = 0;
void updateLength() {
pathLength = 0;
pointCount = 1;
ControlPoint current = listStart;
while(current.next != null) {
pointCount++;
pathLength += dist(current.x, current.y, current.next.x, current.next.y);
current = current.next;
}
pathLength = round(pathLength * 100) / 100;
}
void drawHUD() {
if(mode == SELECT) {
fill(hudSelectedModeColor);
stroke(hudSelectedModeColor);
} else {
fill(hudColor);
stroke(hudColor);
}
text("M", 12, 28);
noFill();
rect(10,10,21,21);
if(mode == ADD) {
fill(hudSelectedModeColor);
stroke(hudSelectedModeColor);
} else {
fill(hudColor);
stroke(hudColor);
}
text("A", 44, 28);
noFill();
rect(40,10,21,21);
if(mode == REMOVE) {
fill(hudSelectedModeColor);
stroke(hudSelectedModeColor);
} else {
fill(hudColor);
stroke(hudColor);
}
text("D", 73, 28);
noFill();
rect(70,10,21,21);
fill(hudColor);
stroke(hudColor);
text(mode, 100, 28);
String length = "L: " + pathLength;
float lengthw = textWidth(length);
text(length, width - lengthw, 28);
String count = "N: " + pointCount;
float countw = textWidth(count);
text(count, width - lengthw - countw - 10, 28);
text(serial, 10, 54);
}
void drawCurve() {
float distFirst = dist(cursorPos.x, cursorPos.y, listStart.x, listStart.y);
float distLast = dist(cursorPos.x, cursorPos.y, listEnd.x, listEnd.y);
float distSegm = getSegmentDistance(nearestSegmentAnchor);
strokeWeight(2);
ControlPoint lastBefore,toRemove;
toRemove= selectionBuffer.get(0);
lastBefore = selectionBuffer.get(1);
ControlPoint previous = null;
ControlPoint current = listStart;
while(current.next != null) {
boolean remove = (mode == REMOVE) && (current.next == toRemove || current == toRemove);
if (remove || (mode == ADD && cursorIsActive && current == nearestSegmentAnchor && distSegm < distFirst && distSegm < distLast) ) {
stroke(curveColorRemove);
}else{
stroke(curveColor);
}
float factor = 0.25;
float helper1x, helper1y, helper2x, helper2y;
if(previous == null) {
helper1x = current.x;
helper1y = current.y;
} else {
helper1x = current.x + (current.next.x - previous.x)*factor;
helper1y = current.y + (current.next.y - previous.y)*factor;
}
if(current.next.next == null) {
helper2x = current.next.x;
helper2y = current.next.y;
} else {
helper2x = current.next.x - (current.next.next.x - current.x)*factor;
helper2y = current.next.y - (current.next.next.y - current.y)*factor;
}
bezier(current.x, current.y, helper1x, helper1y, helper2x, helper2y, current.next.x, current.next.y);
//line(current.x, current.y, current.next.x, current.next.y);
previous = current;
current = current.next;
}
if((mode == ADD || mode == REMOVE) && cursorIsActive && !fallbackToSelect) {
stroke(curveColorNew);
if(distSegm < distFirst && distSegm < distLast) {
line(nearestSegmentAnchor.x, nearestSegmentAnchor.y, cursorPos.x, cursorPos.y);
line(cursorPos.x, cursorPos.y, nearestSegmentAnchor.next.x, nearestSegmentAnchor.next.y);
} else if(distFirst <= distLast) {
line(listStart.x, listStart.y, cursorPos.x, cursorPos.y);
} else {
line(listEnd.x, listEnd.y, cursorPos.x, cursorPos.y);
}
}
if(mode == REMOVE && toRemove != null && toRemove.next != null && lastBefore != toRemove) {
stroke(curveColorNew);
line(lastBefore.x, lastBefore.y, toRemove.next.x, toRemove.next.y);
}
}
void keyPressed() {
if(key == ' ') { // cycle mode on space bar pressed
switch(mode) {
case ADD:
mode = REMOVE;
break;
case REMOVE:
mode = SELECT;
break;
case SELECT:
unselect();
mode = ADD;
break;
}
} else {
switch(key) {
case '1':
case 'm':
mode = SELECT;
break;
case '2':
case 'a':
unselect();
mode = ADD;
break;
case '3':
case 'd':
unselect();
mode = REMOVE;
break;
}
}
}
void unselect()
{
multiSelect = false;
selectionStartPos.set(selectionEndPos);
}
public void mouseMoved() {
if(mouseY < 40) {
return;
}
cursorPos.x = mouseX;
cursorPos.y = mouseY;
float minD = max(w,h);
ControlPoint current = listStart;
while(current.next != null) {
float d = getSegmentDistance(current);
if(d < minD) {
minD = d;
nearestSegmentAnchor = current;
}
current = current.next;
}
if(mode == ADD || mode == REMOVE) {
fallbackToSelect = false;
selectionBuffer.clear();
ControlPoint last = current = listStart;
boolean anyHit = false;
while(current != null) {
if (current.isHit(mouseX, mouseY)) {
anyHit = true
fallbackToSelect = true;
selectionBuffer.add(current);
updateSelection();
selectionBuffer.add(last);
}
last = current;
current = current.next;
}
if(!anyHit){
updateSelection();
}
}
cursorIsActive = !fallbackToSelect && (minD < thresholdDistance) && mode != REMOVE && mode != SELECT;
}
public float getSegmentDistance(ControlPoint segmentAnchor) {
float distance1 = dist(mouseX, mouseY, segmentAnchor.x, segmentAnchor.y);
if(segmentAnchor.next != null) {
float distance2 = dist(mouseX, mouseY, segmentAnchor.next.x, segmentAnchor.next.y);
float distance3 = dist(segmentAnchor.x, segmentAnchor.y, segmentAnchor.next.x, segmentAnchor.next.y);
return (distance1 + distance2 - distance3);
} else {
return distance1;
}
}
ArrayList selection = new ArrayList();
ArrayList selectionBuffer = new ArrayList();
boolean multiSelect;
boolean resizeSelection;
PVector selectionStartPos = new PVector();
PVector selectionEndPos = new PVector();
PVector lastDragPos = new PVector();
public void mousePressed() {
if(mouseY < 40) {
if(mouseX >= 10 && mouseX <= 31){
mode = SELECT;
}
if(mouseX >= 40 && mouseX <= 61){
mode = ADD;
}
if(mouseX >= 70 && mouseX <= 91){
mode = REMOVE;
}
return;
}
lastDragPos.set(mouseX, mouseY);
if(mode == REMOVE) {
if(pointCount > 2) {
ControlPoint lastBefore,toRemove;
toRemove= selectionBuffer.get(0);
lastBefore = selectionBuffer.get(1);
if(listStart == toRemove && listStart.next != null) {
listStart = listStart.next;
} else {
lastBefore.next = toRemove.next;
}
if(toRemove.next == null) {
listEnd = lastBefore;
}
updateLength();
}
return;
}
if(mode == SELECT || fallbackToSelect) {
if (selectionStartPos.x > selectionEndPos.x) {
float h = selectionEndPos.x;
selectionEndPos.x = selectionStartPos.x;
selectionStartPos.x = h;
}
if (selectionStartPos.y > selectionEndPos.y) {
float h = selectionEndPos.y;
selectionEndPos.y = selectionStartPos.y;
selectionStartPos.y = h;
}
boolean pressedInSelection =
mouseX >= selectionStartPos.x &&
mouseY >= selectionStartPos.y &&
mouseX <= selectionEndPos.x &&
mouseY <= selectionEndPos.y;
if(!pressedInSelection)
{
ControlPoint current = listStart;
while(current != null) {
if (current.isHit(mouseX, mouseY)) {
selectionBuffer.clear();
selectionBuffer.add(current);
updateSelection();
multiSelect = false;
resizeSelection = false;
selectionEndPos.set(selectionStartPos);
return;
}
current = current.next;
}
}
multiSelect = true;
resizeSelection = !pressedInSelection;
if(resizeSelection){ // init new selection
selectionStartPos.set(mouseX, mouseY);
selectionEndPos.set(mouseX, mouseY);
selectionBuffer.clear();
updateSelection();
}else{
// assume moving selection
}
}else if(mode == ADD && cursorIsActive) {
ControlPoint newPoint = new ControlPoint(mouseX, mouseY);
newPoint.stroke = controlStroke;
float distFirst = dist(cursorPos.x, cursorPos.y, listStart.x, listStart.y);
float distLast = dist(cursorPos.x, cursorPos.y, listEnd.x, listEnd.y);
float distSegm = getSegmentDistance(nearestSegmentAnchor);
if(distSegm < distFirst && distSegm < distLast) {
newPoint.next = nearestSegmentAnchor.next;
nearestSegmentAnchor.next = newPoint;
} else if(distFirst < distLast) {
newPoint.next = listStart;
listStart = newPoint;a
} else {
listEnd.next = newPoint;
listEnd = newPoint;
}
fallbackToSelect = true;
selectionBuffer.add(newPoint);
updateSelection();
}
updateLength();
}
private void updateSelection() {
// iterate through the old selection
for (int i = selection.size()-1; i >= 0; i--) {
ControlPoint p = (ControlPoint) selection.get(i);
p.stroke = controlStroke;
}
selection.clear();
selection.addAll(selectionBuffer);
// iterate through the new one
for (int i = selection.size()-1; i >= 0; i--) { ControlPoint p = (ControlPoint) selection.get(i);
if(mode == REMOVE) {
p.stroke = curveColorRemove;
} else {
p.stroke = controlStrokeSelected;
}
}
}
public void mouseDragged() {
if(mouseY < 40) {
return;
}
PVector delta = new PVector(mouseX, mouseY);
delta.sub(lastDragPos);
if(mode == SELECT || fallbackToSelect || mode == REMOVE) {
if (resizeSelection) {
selectionEndPos.set(mouseX, mouseY);
selectionBuffer.clear();
ControlPoint current = listStart;
while(current != null) {
if (current.isHit(mouseX, mouseY) || current.isContained(selectionStartPos.x, selectionStartPos.y, selectionEndPos.x, selectionEndPos.y)) {
current.stroke = controlStrokeSelected;
selectionBuffer.add(current);
}
current = current.next;
}
updateSelection();
} else { // move points
for (int i = selection.size()-1; i >= 0; i--) {
ControlPoint p = (ControlPoint) selection.get(i);
p.translate(delta);
}
selectionStartPos.add(delta);
selectionEndPos.add(delta);
}
lastDragPos.add(delta);
}
else if(mode == ADD) {
for (int i = selection.size()-1; i >= 0; i--) {
ControlPoint p = (ControlPoint) selection.get(i);
p.translate(delta);
}
lastDragPos.add(delta);
}
updateLength();
}
public void mouseReleased() {
fallbackToSelect = false;
if (selectionStartPos == selectionEndPos) {
multiSelect = false;
resizeSelection = false;
selectionBuffer.clear();
updateSelection();
}
cursorIsActive = false;
serial = serialize();
}
String serial = "[]";
String serialize(){
String path = "[";
ControlPoint current = listStart;
while(current != null) {
path += current.x + "," + current.y;
current = current.next;
if(current != null) {
path += ",";
}
}
return path + "]";
}
public class ControlPoint {
public float x;
public float y;
public float w = 10;
public float h = 10;
public int fill;
public int stroke;
public ControlPoint next; // since ArrayLists in p5js lack of an add with index, I'll use a linked list implementation instead from now on
public ControlPoint(float x, float y) {
this.x = floor(x);
this.y = floor(y);
}
public ControlPoint set(PVector point) {
this.x = point.x;
this.y = point.y;
return this;
}
public ControlPoint translate(PVector point) {
this.x += point.x;
this.y += point.y;
return this;
}
public ControlPoint sub(PVector point) {
this.x -= point.x;
this.y -= point.y;
return this;
}
public boolean isHit(float x, float y) {
return this.x - w <= x && this.y - h <= y && this.x + w >= x && this.y + h >= y;
}
public boolean isContained(float x1, float y1, float x2, float y2) {
if (x1 > x2) {
float h = x2;
x2 = x1;
x1 = h;
}
if (y1 > y2) {
float h = y2;
y2 = y1;
y1 = h;
}
return x + w/2 >= x1 && y + h/2 >= y1 && x - w/2 <= x2 && y - w/2 <= y2;
}
public void draw(PApplet p5) {
p5.strokeWeight(2);
p5.stroke(stroke);
p5.ellipse(x, y, w, h);
}
}