Animated curves with beginShape()

I have a series of points forming a complex curve that I’d like to animate – essentially we see the curve being drawn point-by-point. But if I use curveVertex() and begin/endShape() but end drawing the line early, the un-drawn points aren’t used and the curve looks slightly different. (And when animated this is much more visible, making the line wiggle a bit.)

My initial thought was to actually draw the entire line, but to change the stroke color to clear partway through. You can’t do this with the standard renderer but it does work with P2D, as this little demo shows:

int whichPoint = 0;
PVector[] points;

void setup() {
  size(600,600, P2D);
  surface.setLocation(0, 0);
  frameRate(4);
  
  points = new PVector[20];
  for (int i=0; i<points.length; i++) {
    points[i] = new PVector(random(50,width-50), random(50,height-50));
  }
}

void draw() {
  background(50);
  
  noFill();
  stroke(255);
  strokeWeight(3);
  beginShape();
  vertex(points[0].x, points[0].y);
  for (int i=0; i<points.length; i++) {
    PVector pt = points[i];
    curveVertex(pt.x, pt.y);
    
    if (i == whichPoint) {
      stroke(0, 0);
      curveVertex(pt.x, pt.y);
    }
  }
  vertex(points[points.length-1].x, points[points.length-1].y);
  endShape();
  
  whichPoint += 1;
  if (whichPoint == points.length) {
    whichPoint = 0;
  }
}

But freaky stuff happens: I get these weird glitchy trails coming off the line! (And strokeCap() and strokeJoin() don’t seem to work, and the line looks a lot less smooth.)

Any ideas on how I could do this without the P2D renderer?

1 Like

Update: seems to have something to do with the number of vertices in the line. Running at 20 (like above) works ok, not great. But running at 4–5 gives really weird trails and glitches.

1 Like

I don’t know about the P2D part,

but for drawing a partial curveVertex curve – this is a byproduct of how curvevertex interpolates. You need four stable points – if any change, the interior point locations change. If you know the points ahead of time then you can do progressive rendering using

https://processing.org/reference/curvePoint_.html

to advance over the length of the curve in a loop and solve for x and y at each % amount. Then draw line segments between x,y and px,py.

3 Likes

A (rough) go at the algorithm suggested by @jeremydouglass above:

float margin = 50.0;
PMatrix3D crb = new PMatrix3D();
float curveTightness = 0.01;
int count = 20;
int draw = 250;
float animRate = 0.005;
PVector[] points;
PVector[] anim;
boolean closedLoop = false;

void setup() {
  size(600, 600, P2D);
  strokeCap(ROUND);
  strokeJoin(ROUND);
  curveTightness(curveTightness);
  catmullBasis(curveTightness, crb);

  points = new PVector[count];
  for (int i = 0; i < count; ++i) {
    points[i] = new PVector();
  }

  anim = new PVector[draw];
  for (int i = 0; i < draw; ++i) {
    anim[i] = new PVector();
  }

  randomizePoints();
}

void mouseReleased() {
  if (mouseButton == LEFT) {
    randomizePoints();
  }
  if (mouseButton == RIGHT) {
    closedLoop = !closedLoop;
  }
}

void draw() {
  surface.setTitle(nfs(frameRate, 1, 1));

  float t = (frameCount * animRate);
  t -= floor(t);

  background(#fff7d5);
  noFill();
  stroke(#202020);
  strokeWeight(3.0);

  beginShape();
  for (PVector p : points) {
    curveVertex(p.x, p.y);
  }

  if (closedLoop) {
    curveVertex(points[0].x, points[0].y);
    curveVertex(points[1].x, points[1].y);
    curveVertex(points[2].x, points[2].y);
    endShape(CLOSE);
  } else {
    endShape();
  }

  strokeWeight(7.5);
  stroke(#007fff);

  PVector prev = points[1];
  for (int i = 0; i < draw; ++i) {
    float prc = i / (draw - 1.0);
    float sclprc = t * prc;
    PVector a = anim[i];
    curvePoints(crb, closedLoop, points, sclprc, a);
    line(prev.x, prev.y, a.x, a.y);
    prev = a;
  }
}

void randomizePoints() {
  for (PVector p : points) {
    p.set(
      random(margin, width - margin),
      random(margin, height - margin));
  }
}

PVector curvePoints(
  PMatrix3D cb,
  boolean closedLoop,
  PVector[] pts,
  float t,
  PVector target) {

  int len = pts.length;
  float tScaled = 0.0;
  int i = 0;

  PVector a;
  PVector b;
  PVector c;
  PVector d;

  if (closedLoop) {

    tScaled = (t - floor(t)) * len;
    i = (int) tScaled;
    a = pts[mod(i, len)];
    b = pts[mod(i + 1, len)];
    c = pts[mod(i + 2, len)];
    d = pts[mod(i + 3, len)];
    
  } else {

    if (t <= 0.0) {
      return target.set(pts[1]);
    }

    if (t >= 1.0) {
      return target.set(pts[len - 2]);
    }

    tScaled = t * (len - 3);
    i = (int) tScaled;
    a = pts[i];
    b = pts[i + 1];
    c = pts[i + 2];
    d = pts[i + 3];
  }

  return curvePoint(cb, a, b, c, d, tScaled - i, target);
}

PVector curvePoint(
  PMatrix3D cb,
  PVector a,
  PVector b,
  PVector c,
  PVector d,
  float t,
  PVector target) {

  if ( target == null ) {
    target = new PVector();
  }

  float tt = t * t;
  float ttt = tt * t;

  float acoeff = ttt * cb.m00 + tt * cb.m10 + t * cb.m20 + cb.m30;
  float bcoeff = ttt * cb.m01 + tt * cb.m11 + t * cb.m21 + cb.m31;
  float ccoeff = ttt * cb.m02 + tt * cb.m12 + t * cb.m22 + cb.m32;
  float dcoeff = ttt * cb.m03 + tt * cb.m13 + t * cb.m23 + cb.m33;

  return target.set(
    a.x * acoeff + b.x * bcoeff + c.x * ccoeff + d.x * dcoeff,
    a.y * acoeff + b.y * bcoeff + c.y * ccoeff + d.y * dcoeff,
    a.z * acoeff + b.z * bcoeff + c.z * ccoeff + d.z * dcoeff);
}

public PMatrix3D catmullBasis(float s, PMatrix3D target) {

  if ( target == null ) {
    target = new PMatrix3D();
  }

  float u = 1.0 - s;
  float th = (s - 1.0) * 0.5;
  float uh = u * 0.5;
  float v = (s + 3.0) * 0.5;

  target.set(
    th, v, -v, uh,
    u, ( -5.0 - s ) * 0.5, s + 2.0, th,
    th, 0.0, uh, 0.0,
    0.0, 1.0, 0.0, 0.0);

  return target;
}

int mod(int a, int b) {
  int result = a - b * ( a / b );
  return result < 0 ? result + b : result;
}

I found it helpful to look at curveInit and curvePoint to see how they worked. One of the ambiguities I ran into was how to handle a closed loop.

If you’re approximating a curve with line segments, you’d probably want to refine so that more line segments are alotted to covering a long curve as the percentage increases; and fewer to a shorter curve or minimal percentage.

I got some jittering with JAVA2D vs. P2D.

Best,
Jeremy

2 Likes

Thanks @behreajj and @jeremydouglass! I need to dig into @behreajj’s code more to understand what’s going on, but I def appreciate the head-start. I hadn’t noticed curvePoint() before but that seems like a good option for my particular application.

Am I understanding correctly that with curvePoint I can use a previous point as the control point? One of the hard things about bezier curves is the need to specify points that are off the line to get the curve you want – something that works well in Illustrator but I’ve never been able to use in a generative project successfully.

Unfortunately, curvepoint takes either the x or y components of a curve vertex and performs a lerp – that means it needs two control point components, a and d, and two real point components, b and c.