> show canvas only <


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