Funky curveVertx results

I’m drawing a series of lines that diverge in the middle of the screen using a gaussian function (getGaussian below). Usually this is fine as you can see in theis picture (only two lines here to see the issue.


The problem is that when I use curveVertex to draw it, I sometimes get a weird line piece at the beginning and the end. This seems to happen if the curve is shallow and when there is a big jump from the previous y value to the first of the curve section.

Using plain vertex just causes a straight line jutting up. I have tried adding points in between so the jump is less dramatic but a similar appearence happens.

I’m really stuck on how to make this have a smooth transition. Part of it is the the getGaussian function just returns a high y value causing that jump. Is there a way to ensure the function returns a better y value or a way to scale all the y values to prevent this? or is there some other aesthetic hack i can make with curveVertex?

here’s the full code so you can see the parts:

int windWidth = 1000;
int widthMargin = 150;
int windHeight = 600;
int heightMargin = 50;
int numLines = 2;
int numPts = 200;
float incX = (windWidth - (widthMargin*2)) / float(numPts);
PVector stdRange = new PVector(1.0, 5.0);

void setup(){
  size(1000, 600, P3D);
  background(255);
  smooth(8);
  noLoop();
}

void draw() {
  background(255);
  for(int i=0; i<numLines; i++){
    float mean = random(-1.25, 1.25);
    float stdd = random(stdRange.x, stdRange.y);
    float yOff = random(4);
    float yStart = 0.0;
    float yDir = random(0,1) < 0.5 ? -1:1;
    noFill();
    beginShape();
    strokeWeight(2);
    stroke(50);
    curveVertex(0, height/2);
    curveVertex(0, height/2);
    
    for(int j=0; j<width-(widthMargin*2); j++){
      float xPos = widthMargin + j;
      float xVal = map(xPos, widthMargin, width-widthMargin, -stdRange.y, stdRange.y);
      float yVal = getGaussian(xVal, mean, stdd);
      float yPos = 0.0;
      if(i%2 == 0){
        yStart = height/2 - yOff;
        yPos = map(yVal, 0, 1, yStart, 0+heightMargin);
      } else {
        yStart = height/2 + yOff;
        yPos = map(yVal, 0, 1, yStart, height-heightMargin);
      }
      
      if(j == 0){
        curveVertex(widthMargin-1, yStart);
        curveVertex(widthMargin, yPos);
      } else if(j == (width-(widthMargin*2))-1){
        curveVertex(width-widthMargin-1, yPos);
        curveVertex(width-widthMargin, yStart);
      } else{
        curveVertex(xPos, yPos);
        //println(xPos + " -- " + yPos);
      }
    }

    curveVertex(width, height/2);
    curveVertex(width, height/2);
    endShape();
  }
}

float getGaussian(float x, float mean, float variance) {
  return (1 / sqrt(TWO_PI * variance)) * exp(-sq(x - mean) / (2 * variance));
}

as far as I know (not 100% accurate) most curve related functions and verteces require an “anchor” point. It allows for more flexible curves. Try to find some curve editors on internet to see how it works.

Before you ask, I have no idea how do they work.

Hi @travis.stdenis,

This is due to how the CurveVertex algorithm works under the hood. Depending on the configuration of your points you can create loops in your curve. I think it happens mainly when some points are really close to each other while other are really far appart. If all your point are evenly spaced then this issue should be limited.

I made this GIF to illustrate:

Here’s the code I used to make the GIF. It lets you grab the anchor points and move them around so you can have a better filling of how cerveVertex works.

Code
ArrayList<Point> pts = new ArrayList<Point>();

void setup() {
  size(800, 600);
  pts.add(new Point(200, 250));
  pts.add(new Point(300, 350));
  pts.add(new Point(400, 250));
  pts.add(new Point(500, 350));
  pts.add(new Point(550, 250));
}

void draw() {
  background(20);
  
  stroke(230);
  strokeWeight(2);
  beginShape();
  noFill();
  for (Point p : pts) {
    curveVertex(p.getPos().x, p.getPos().y);
  }
  endShape();
  
  for (Point p : pts) {
    p.update();
    p.show();
  }
}

void mousePressed() {
  for (Point p : pts) {
    if (p.isHoovered()) {
      p.setSelect(true);
      p.computeDeltaFromMouse();
      return;
    }
  }
}

void mouseReleased() {
  for (Point p : pts) {
    p.setSelect(false);
  }
}


class Point {
  PVector pos, deltaFromMouse;
  color col;
  float size;
  boolean isSelected;
  boolean movable;

  Point(float x, float y) {
    pos = new PVector(x, y);
    col = color(200, 20, 20);
    size = 15;
    isSelected = false;
    movable = true;
    deltaFromMouse = new PVector(0, 0);
  }

  Point(float x, float y, color c, float s) {
    pos = new PVector(x, y);
    col = c;
    size = s;
    isSelected = false;
    movable = false;
    deltaFromMouse = new PVector(0, 0);
  }

  PVector getPos() {
    return pos;
  }

  void setMovable(boolean b) {
    movable = b;
  }

  boolean isHoovered() {
    return ((mouseX - pos.x) * (mouseX - pos.x) + (mouseY - pos.y) * (mouseY - pos.y) - (size/2) * (size/2) < 0);
  }

  void setSelect(boolean b) {
    isSelected = b;
  }

  void computeDeltaFromMouse() {
    deltaFromMouse.x = mouseX - pos.x;
    deltaFromMouse.y = mouseY - pos.y;
  }

  void update() {
    if (isSelected == false || movable == false) {
      return;
    }

    pos.x = mouseX - deltaFromMouse.x;
    pos.y = mouseY - deltaFromMouse.y;
  }

  void show() {
    if (movable && isHoovered()) {
      noStroke();
      int r = (col >> 16) & 0xFF;
      int g = (col >> 8) & 0xFF;
      int b = col & 0xFF; 
      fill(color(r, g, b, 100));
      ellipse(pos.x, pos.y, size * 1.4, size * 1.4);
    }

    noStroke();
    fill(col, 200);
    ellipse(pos.x, pos.y, size, size);
  }
}

If you want more info you can go to the processing reference or go to this wikipedia page to have to have a look at the Catmull–Rom spline which curveVertex is an implementation of.

i had tried adding points a pixel apart in between which didn’t remove the visual issue. also, we are talking about a 4-7 pixel difference causing the warped line and even at a pixel or two distant, you can see that it is doing the same thing just kind of hidden with the overlap of a thicker than 1 pixel line.

In your case, the issue are those 2 lines:

  • curveVertex(widthMargin-1, yStart);
  • curveVertex(width-widthMargin, yStart);

Since yStart is randomly offset by up to 4 pixels, it is enough to break the smoothness of the curves and get that extreme result.

Now that’s said, the problem is just hidden if you remove those lines. Again you are in a situation where you have a big gap followed by a small gap.
Your first point is on the far left and the second point is at widthMargin pixels away. So you still get that weird result but because your points are so close after and quite smooth you can’t really see it with your parameters.

Now, remove the 2 lines, change ‘widthMargin’ to 300 and instead of ‘j++’ put ‘j += 3
0’ and now you can see the artifact again…

Proof in picture:
image

with this code:
int windWidth = 1000;
int widthMargin = 300;
int windHeight = 600;
int heightMargin = 50;
int numLines = 2;
int numPts = 200;
float incX = (windWidth - (widthMargin*2)) / float(numPts);
PVector stdRange = new PVector(1.0, 5.0);

void setup(){
  size(1000, 600, P3D);
  background(255);
  smooth(8);
  noLoop();
}

void draw() {
  background(255);
  for(int i=0; i<numLines; i++){
    float mean = random(-1.25, 1.25);
    float stdd = random(stdRange.x, stdRange.y);
    float yOff = random(4);
    float yStart = 0.0;
    float yDir = random(0,1) < 0.5 ? -1:1;
    noFill();
    beginShape();
    strokeWeight(2);
    stroke(50);
    curveVertex(0, height/2);
    curveVertex(0, height/2);
    
    for(int j=0; j<width-(widthMargin*2); j+=30){
      float xPos = widthMargin + j;
      float xVal = map(xPos, widthMargin, width-widthMargin, -stdRange.y, stdRange.y);
      float yVal = getGaussian(xVal, mean, stdd);
      float yPos = 0.0;
      if(i%2 == 0){
        yStart = height/2 - yOff;
        yPos = map(yVal, 0, 1, yStart, 0+heightMargin);
      } else {
        yStart = height/2 + yOff;
        yPos = map(yVal, 0, 1, yStart, height-heightMargin);
      }
      
      if(j == 0){
        //curveVertex(widthMargin-1, yStart);
        curveVertex(widthMargin, yPos);
      } else if(j == (width-(widthMargin*2))-1){
        curveVertex(width-widthMargin-1, yPos);
        //curveVertex(width-widthMargin, yStart);
      } else{
        curveVertex(xPos, yPos);
        //println(xPos + " -- " + yPos);
      }
    }

    curveVertex(width, height/2);
    curveVertex(width, height/2);
    endShape();
  }
}

float getGaussian(float x, float mean, float variance) {
  return (1 / sqrt(TWO_PI * variance)) * exp(-sq(x - mean) / (2 * variance));
}

If you are more reasonable with the size of your width and spacing it should be ok. Like 50 for the margin and 30 for sopacing.

curveVertew is used for interpolation anyway. What’s the point of using it if you are so precise with your points? As you can see in the pic above even with a spacing of 30 the result is ok but of course it is not the real exact curve, just an interpolation.

If you want to be more precise and use 1 pixel increment, why not using just straight lines? Your points are so close to each other that the outcome is almost identical:

Code
int windWidth = 1000;
int widthMargin = 150;
int windHeight = 600;
int heightMargin = 50;
int numLines = 2;
int numPts = 200;
float incX = (windWidth - (widthMargin*2)) / float(numPts);
PVector stdRange = new PVector(1.0, 5.0);

void setup(){
  size(1000, 600, P3D);
  background(255);
  smooth(8);
  noLoop();
}

void draw() {
  background(255);
  for(int i=0; i<numLines; i++){
    float mean = random(-1.25, 1.25);
    float stdd = random(stdRange.x, stdRange.y);
    float yOff = random(4);
    float yStart = 0.0;
    float lastX, lastY;
    
    noFill();
    strokeWeight(2);
    stroke(50);
    lastX = 0;
    lastY = height/2;
     
    for(int j = 0; j < width-(widthMargin*2); j++){
      float xPos = widthMargin + j;
      float xVal = map(xPos, widthMargin, width-widthMargin, -stdRange.y, stdRange.y);
      float yVal = getGaussian(xVal, mean, stdd);
      float yPos = 0.0;
      if(i%2 == 0){
        yStart = height/2 - yOff;
        yPos = map(yVal, 0, 1, yStart, 0+heightMargin);
      } else {
        yStart = height/2 + yOff;
        yPos = map(yVal, 0, 1, yStart, height-heightMargin);
      }
      
      line(lastX, lastY, xPos, yPos);
      lastX = xPos;
      lastY = yPos;
    }
    
    line(lastX, lastY, width, height/2);
  }
}

float getGaussian(float x, float mean, float variance) {
  return (1 / sqrt(TWO_PI * variance)) * exp(-sq(x - mean) / (2 * variance));
}