Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Points on a Quadratic Curve using Processing (p5.js)

I am using this formula to calculate the points on a quadratic curve:

  cPx2 = (1-t)*(1-t)* x1+2 * (1-t)*t*qcX + t*t*x2;
  cPy2 = (1-t)*(1-t)* y1+2 * (1-t)*t*qcY + t*t*y2;

When I set t = 10 and iterate through the curves, I get this:

points on the quad curve

It looks like it's getting the points on the curve (the flower shape), but also all the points on the "control points" as well.

I used this formula to generate the points:

    flowerArray=[]
    for(let i = 0; i < numVertices+1; i++) {
    angle = i * spacing;
    x = centerX + cos(radians(angle)) * 180;
    y = centerY+ sin(radians(angle)) * 180;

    if(i == 0) {
      flowerArray.push(x,y);
    }else {
        cAngle = angle - spacing/2;
          cX = centerX + cos(radians(cAngle)) * 100;
          cY = centerY+  sin(radians(cAngle)) * 100;
      
    flowerArray.push(cX,cY,x,y)
    }
   }

Question: Is it possible to just get the points on the "flower" and not the outer shape?

I tried skipping through the array several different ways, but I wasn't able to get it to work the way I was hoping.

UPDATE I am using this to draw the points:

    for (i = 0; i < flowerArray.length; i+=2){
        x1=flowerArray[i] 
        y1=flowerArray[i+1]  
        qcX=flowerArray[i+2] 
        qcY=flowerArray[i+3] 
        x2=flowerArray[i+4]
        y2=flowerArray[i+5] 
    for (k=0; k<= steps; k++) {   
      t = k/steps
      cPx2 = (1-t)*(1-t)* x1+2 * (1-t)*t*qcX + t*t*x2;
      cPy2 = (1-t)*(1-t)* y1+2 * (1-t)*t*qcY + t*t*y2;
        circle(cPx2, cPy2,3);    
}
}
like image 464
glaemart Avatar asked Mar 02 '23 03:03

glaemart


1 Answers

What a lovely question.

The one thing that stands out is this part:

if(i == 0) {
      flowerArray.push(x,y);
    }else {
        cAngle = angle - spacing/2;
          cX = centerX + cos(radians(cAngle)) * 100;
          cY = centerY+  sin(radians(cAngle)) * 100;
      
    flowerArray.push(cX,cY,x,y)
    }

Notice that you call flowerArray.push(x,y); where as in any other case you push 4 instead of two values: flowerArray.push(cX,cY,x,y). It's unclear why this condition is necessary in the first place: if(i == 0)

The code works as expected without it:

function setup() {
  
  createCanvas(512, 512);
  background(226, 255, 204);
  
  let flowerArray = [];
  let centerX = 256;
  let centerY = 256;
  let numVertices = 7;
  let steps = 11;
  let spacing = 360 / numVertices;

  
  for (let i = 0; i < numVertices + 1; i++) {
    
    angle = i * spacing;
    
    x = centerX + cos(radians(angle)) * 180;
    y = centerY + sin(radians(angle)) * 180;
  
    cAngle = angle - spacing/2;
      
    cX = centerX + cos(radians(cAngle)) * 100;
    cY = centerY+  sin(radians(cAngle)) * 100;
  
    flowerArray.push(cX, cY, x, y);
  }

  for (i = 0; i < flowerArray.length; i+=2) {
    
    x1=flowerArray[i];
    y1=flowerArray[i+1];  
    
    qcX=flowerArray[i+2];
    qcY=flowerArray[i+3];
    
    x2=flowerArray[i+4];
    y2=flowerArray[i+5];
    
    for (k=0; k <= steps; k++) {
      t = k/steps;
      cPx2 = (1-t)*(1-t)* x1+2 * (1-t)*t*qcX + t*t*x2;
      cPy2 = (1-t)*(1-t)* y1+2 * (1-t)*t*qcY + t*t*y2;
      
      circle(cPx2, cPy2, 3);
    }
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"></script>

quadratic curves rendered between points of a 7 pointed star as circles interpolated between points

Personally I recommend getting into the habit of formatting code: it makes it easier to read code and spot issues. The more you program and the larger the programs get the more time you spend reading code hence making code readable will definitely pay off.

Another suggestion is encapsulating the quadratic bezier formula in a function:

function quadLerp(p0, p1, p2, t){
  return ((1-t)*(1-t)) * p0 + 2 * ((1-t) * t * p1) + t * t * p2;
}

calling it like so:

  cPx2 = quadLerp(x1, qcX, x2, t);
  cPy2 = quadLerp(y1, qcY, y2, t);

One cool thing about quadratic bezier curves is that you can compute them by interpolating two linear interpolations:

Illustration of quadratic Bézier curves in string art. In each case, end points marked with black circles and the control point marked with an X define the quadratic Bézier curve shown as a dotted line.

Illustration of quadratic Bézier curves in string art. In each case, end points marked with black circles and the control point marked with an X define the quadratic Bézier curve shown as a dotted line by Wikipedia user Cmglee

Given that you can compute linear interpolation in p5.js via lerp() you can compute quadratic interpolation as:

function quadLerp(p0, p1, p2, t){
  return lerp(lerp(p0, p1, t),
              lerp(p1, p2, t),
              t);
}

It's nice that p5.js supports various curve drawing functions such as bezier() or curve() (and similar functions such as bezierPoint()/curvePoint() to compute the interpolated value you could use for custom rendering)

Update Based on your comment I understand you want to only draw the inner shape.

Your code is handling outer points of a regular polygon and the inner mid points, drawing a star like shape and the next outer points and using them as anchor / control points to draw circles on quadratic bezier curves between these points. As if this wasn't enough complexity, there is one array that stores all the anchor and control points mixed into a single list and you must keep track of the indices to draw correctly. Oh, and also you're using polar to cartesian coordinate system conversion to draw the regular polygon/star in the first place.

There's a lot going on, so let's try to break it down.

Starting of with drawing the star and math behind it: this is similar to islia's question and you can see my detailed answer here.

Notice the star example in her question: it's not a bad place to start since we don't have to worry about quadratic bezier points. It does introduce push()/pop() which you may not be familiar yet. It's useful to know, but can skip it for now. Let's look at a simplified version of that snippet:

function setup() {
  createCanvas(512, 512);
}

function draw() {
  background(102);

  star(width * 0.5, height * 0.5, 80, 100, 7);
}

function star(x, y, radius1, radius2, npoints) {
  let angle = TWO_PI / npoints;
  let halfAngle = angle / 2.0;
  beginShape();
  for (let a = 0; a < TWO_PI; a += angle) {
    let sx = x + cos(a) * radius2;
    let sy = y + sin(a) * radius2;
    vertex(sx, sy);
    sx = x + cos(a + halfAngle) * radius1;
    sy = y + sin(a + halfAngle) * radius1;
    vertex(sx, sy);
  }
  endShape(CLOSE);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"></script>

Now let's look at the same thing is more obvious variable names:

function setup() {
  createCanvas(512, 512);
}

function draw() {
  background(102);

  star(width * 0.5, height * 0.5, 80, 100, 7);
}

function star(x, y, innerRadius, outerRadius, npoints) {
  let angle = TWO_PI / npoints;
  let halfAngle = angle / 2.0;
  beginShape();
  
  for (let a = 0; a < TWO_PI; a += angle) {
    
    let xOuter = x + cos(a) * outerRadius;
    let yOuter = y + sin(a) * outerRadius;
    vertex(xOuter, yOuter);
    
    let xInner = x + cos(a + halfAngle) * innerRadius;
    let yInner = y + sin(a + halfAngle) * innerRadius;
    vertex(xInner, yInner);
  }
  
  endShape();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"></script>

Hopefully this makes it easier to understand which point is which.

To draw the quadratic bezier points you'd need the current outer point and next outer point as anchor points and the current inner point (in between them with the smaller radius) as the control point.

Here's a modified version of the sketch where star() function is repurposed to draw the flower:

function setup() {
  createCanvas(512, 512);
}

function draw() {
  background(226, 255, 204);

  flower(width * 0.5, height * 0.5, mouseX, 100, 7);
  
  text("innerRadius = " + mouseX, 10, 15);  
}

function flower(x, y, innerRadius, outerRadius, npoints) {
  let angleIncrement = TWO_PI / npoints;
  let halfAngle = angleIncrement / 2.0;
  // increment by point index
  for (let i = 0; i < npoints; i++) {
    // calculate the current angle around the circle
    let angle = angleIncrement * i;
    // calculate current outer point
    let xOuter = x + cos(angle) * outerRadius;
    let yOuter = y + sin(angle) * outerRadius;
    // calculate current inner point
    let xInner = x + cos(angle + halfAngle) * innerRadius;
    let yInner = y + sin(angle + halfAngle) * innerRadius;
    
    // next angle increment
    let angleNext = angleIncrement * (i+1);
    // calculate next outer point
    let xOuterNext = x + cos(angleNext) * outerRadius;
    let yOuterNext = y + sin(angleNext) * outerRadius;
    // draw quad bezier between current and outer points with inner point as control point
    quadBezierCircles(xOuter, yOuter, xInner, yInner, xOuterNext, yOuterNext, 11);
    
    // for debug purposes only: render 
    if(mouseIsPressed){
      circle(xInner,yInner,9);
      circle(xOuter,yOuter,9);
    }
  }
}

function quadBezierCircles(anchorX1, anchorY1, controlX, controlY, anchorX2, anchorY2, steps){
  for (let k = 0 ; k <= steps; k++) {
    
    t = k / steps;
    
    x = quadLerp(anchorX1, controlX, anchorX2, t);
    y = quadLerp(anchorY1, controlY, anchorY2, t);
      
    circle(x, y, 3);
  }
}

function quadLerp(p0, p1, p2, t){
  return lerp(lerp(p0, p1, t),
              lerp(p1, p2, t),
              t);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"></script>

You can move the mouse to control the inner radius. If you hold the mouse pressed you can see the anchor/control points.

bezier points

bezier points with visualised anchor and control points

The same could've have been drawn as quad bezier points between current and next inner points as anchor points with the current outer point as an anchor as well.

like image 50
George Profenza Avatar answered Apr 28 '23 16:04

George Profenza