Bending a PShape object to make it circular

Hi everyone,

The title says it all, I’d like to bend a PShape object to give it a circular shape:

I tried to project the vertices around a circle (circular translation) but it didn’t work:

Questions:

  • Is it the correct way to achieve that warping effect ? If so, could you tell me what is wrong with the polar coordinates transformation below (line 25-30)?
  • If not, what’s the best way to distort a PShape object ?
PShape object;

void setup(){
    size(1300, 800);
    
    object = createShape();
    object.beginShape();
    object.fill(255, 40, 40);
    object.translate(width/2 - width/8, height/2 - height/8);
    object.vertex(0, 0);
    object.vertex(64, 10);
    object.vertex(87, 5);
    object.vertex(173, 47);
    object.vertex(389, 20);
    object.vertex(380, 125);
    object.vertex(309, 100);
    object.vertex(83, 120);
    object.vertex(47, 175);
    object.vertex(4, 130);
    object.endShape(CLOSE);
    
    float angle = radians(360) / object.getVertexCount();    
    int radius = 100;
    
    //for (int i = 0; i < object.getVertexCount(); i++) {
    //  PVector v = object.getVertex(i);
    //  float x = cos(v.x * angle) * radius;
    //  float y = sin(v.x * angle) * radius;
    //  PVector vec = new PVector(x, y);
    //  object.setVertex(i, vec);
    //}
}
        
void draw(){
    background(255);
    shape(object);     
}

Thank you

1 Like

Hi,

The problem with your solution is that your are only remapping the points and even if you put them where they should go you can’t curve the lines in between as you show on your example. You would need to add way more points to achieve the curve.

What is your end goal? Do you really need a PShape or you can use an image?

Hi @jb4x,

I know, it was just an example picture. The object I’d like to bend have millions of points so I guess that wouldn’t be a problem

The object is a .obj file so I really need to warp the object itself, not the pixels of an image.

And what is the end goal?

The transformation is not trivial at all and there would be several ways to achieve simalr effect I guess.

Is there some constraints, some rules that it should follows?

… for the moment, just to make the object circular. Nothing more.

There are no specific constraints I can think of right now. I just would like to explore the different ways to bend an object.

You can find some really interesting answers on this link: https://math.stackexchange.com/questions/1753227/how-do-i-transpose-an-ellipse-function-to-stretch-the-ellipse-into-curved-space

So, trying to answer my own questions:

  • the circular projection of the coordinates is not correct
  • After giving it some thought I believe a better way to warp an object would be to attach it (with springs ?) to a spline or a point line that could be then distorted at will. Any bending of the spline would consequently affect the object’s shape, thus acting as a “backbone”.

That second suggestion being quite advanced and not necessary in this case (I just want a regular circular shape) I will stick to the circular projection. So back to my first question:

How would you project the vertices of a rather “complex” shape (PShape or group of PShape with an interior part, not a line) around a circle ?

More specifically:

  • what would be the computation of the angle ?
    float angle = radians(360) / object.getVertexCount(); doesn’t seem correct to me

Simpler question.

Say you have points placed on a line at different intervals:

void setup(){
  size(800, 600, P2D);
  strokeWeight(6);
  background(255);
  smooth(8);
  
  int xmin = 200;
  int xmax = 800;
  int ymean = height/2;
  int step = 20;
    
  for (int i = 0; i < step; i++) {
    float x = lerp(xmin, xmax , i/float(step+i));
    float y = lerp(ymean, ymean, i/float(step+i));
    point(x, y);
    }
  }

How would you project them around a circle ?

1 Like

For line-to-circle position:

float angleRadians = TWO_PI * linePosition / lineLength;
float circleX = radius * cos(angleRadians);
float circleY = radius * sin(angleRadians);

If you simply wanted a visual, you might wrap your shape as a texture onto an object. Perhaps a cylinder (included in examples).

2 Likes

I was about to give you the same answer as @noahbuddy.

In this case you just need to map the distance of your point from the first one to an angle between 0 and 2*PI.

That says, you do have constraint and rules that it should follows in this case:

  • The line should be transformed in a complete circle
  • the line of point should wrap completely on that circle
  • the radius of the circle is known

Now to generalize the concept:

  • what happen for points that are a bit further away on that line or before the first point? Are they still mapped to the same circle?
  • What happen for points that are not in that line? Are they also mapped in another circle but bigger or smaller than the first one?
  • The radius was here chosen at random, but should there be a rule to select the radius? In other word how do you characterize this transformation? Is it a point in the space and the closest distance to the line is the radius?
  • The angles also were assumed but should the first point be 0 degree or not?
  • There is also the direction of the line that was assumed to be horizontal but would it always be the case?

The reason I was asking the end goal and if there was some rules or constraints to follow is because, as you can see, the subject is really big. There is plenty of ways to deform an object and it really depend on what you want to do with it.

How you want to control it is really important because it says a lot on what are the parameters for it.

One easy way is to use the concept they use in the link I posted earlier.
The basic idea behind it is to take a “square” space and to bend it with parabole equation. This way you can easily get the position of every points in the bent space using those equation. The cool thing about it is that you can use the equation you want to bend your space. After it’s just a matter of understanding how to control the bending.

1 Like

@noahbuddy – Thank you, that’s exactly what I was looking for.

@jb4x – Let me explain what I’m trying to do. I might be wrong but at least you’ll get the big picture.

There are 2 main approaches (not 1 as you’re implying) mentionned in the SE thread you are referring to. One is to compute the transformation of the whole object (thing that is mathematically difficult to do accurately as stated several times in the thread), and the other one is to approximate that transformation by bending a spline (or a volumetric space) to which the object is attached to.

It is that second option (that I’ve mentionned before) that I’ve finally decided to try to implement.
Here’s the idea:

  • 1/ Take an object and put an imaginary line inside of it. That line crosses the object from min(x) to max(x) at height = mean(y)

  • 2/ Project the x coordinate of every vertices on that line and place a point.

  • 3/ Convert these reference points to curvePoint() and compute their tangents up to their original y coordinate using curveTangent()

We now have the coordinates of every vertices based on all the tangents to the central line. Now, if I’m not mistaken, if we curve that central line then all the vertices should be curved accordingly. The object has been warped ! (I wish)


In the same way, If I project the reference points around a circle (hence my previous question) the object should be distorted circularly.

Of course I don’t know if all that is possible and I would be very curious to hear your opinion(s). Also if you have details (step-by-step explanations or pseudo-code) or ideas regarding a bending method based on a volumetric space instead of on a line/spline in Processing, I would also love to hear that.

2 Likes

You already described all the process! You have done the hard part!

Here is an example with a bezier curve. You can play around with the handles.:

PVector[] originalPoints;
float[][] param;
int xMin, xMax;
float deltaX, deltaY;
Point[] p;

void setup() {
  size(800, 600);
  noFill();
  background(20);
  stroke(230);

  initialyzeoriginalPoints();
  initializeparam();
  initializeBezierCurve();

  beginShape();
  for (int i = 0; i < originalPoints.length; i++) {
    vertex(originalPoints[i].x, originalPoints[i].y);
  }
  endShape(CLOSE);
}

void draw() {
  background(20);
  
  // Draw the deformed shape
  stroke(200, 25, 25);
  strokeWeight(2);
  noFill();
  drawShape();
  
  // Draw the control lines
  stroke(50);
  strokeWeight(2);
  line(p[0].pos().x, p[0].pos().y, p[1].pos().x, p[1].pos().y);
  line(p[2].pos().x, p[2].pos().y, p[3].pos().x, p[3].pos().y);
  
  // Draw the bezier curves
  noFill();
  stroke(200);
  strokeWeight(2);
  bezier(p[0].pos().x, p[0].pos().y, p[1].pos().x, p[1].pos().y, p[2].pos().x, p[2].pos().y, p[3].pos().x, p[3].pos().y);
  
  // Draw the points
  for (int i = 0; i < 4; i++) {
    p[i].show();
  }
  
  
}

void mousePressed() {
  // Check if mouse is over a point
  for (int i = 0; i < 4; i++) {
    if (p[i].isHit(mouseX, mouseY)) {
      p[i].lock();
      deltaX = mouseX - p[i].pos().x;
      deltaY = mouseY - p[i].pos().y;
      return;
    }
  }
}


void mouseReleased() {
  for (int i = 0; i < 4; i++) {
    p[i].unlock();
  }
}


void mouseDragged() {
  for (int i = 0; i < 4; i++) {
    if (p[i].isLocked()) {
      p[i].move(mouseX - deltaX, mouseY - deltaY);
    }
  }
}

void initialyzeoriginalPoints() {
  originalPoints = new PVector[7];
  originalPoints[0] = new PVector(100, 320);
  originalPoints[1] = new PVector(340, 210);
  originalPoints[2] = new PVector(530, 240);
  originalPoints[3] = new PVector(700, 420);
  originalPoints[4] = new PVector(420, 330);
  originalPoints[5] = new PVector(340, 370);
  originalPoints[6] = new PVector(210, 400);
  
  xMin = 100;
  xMax = 700;
}


void initializeparam() {
  param = new float[7][2];
  for (int i = 0; i < originalPoints.length; i++) {
    param[i][0] = (originalPoints[i].x - xMin) / (float)(xMax - xMin);
    param[i][1] = (float)((height / 2.0) - originalPoints[i].y);
  }
}

void initializeBezierCurve() {
  p = new Point[4];
  p[0] = new Point(xMin, (height / 2.0), color(200, 200, 10), 10);
  p[1] = new Point((xMin + xMax) / 2.0, (height / 2.0), color(200, 200, 10), 10);
  p[2] = new Point((xMin + xMax) / 2.0, (height / 2.0), color(200, 200, 10), 10);
  p[3] = new Point(xMax, (height / 2.0), color(200, 200, 10), 10);
}

void drawShape() {
  
  beginShape();
  for (int i = 0; i < param.length; i++) {
    float bezierX = bezierPoint(p[0].pos().x, p[1].pos().x, p[2].pos().x, p[3].pos().x, param[i][0]);
    float bezierY = bezierPoint(p[0].pos().y, p[1].pos().y, p[2].pos().y, p[3].pos().y, param[i][0]);
    PVector dir = getTan(param[i][0]);
    if (i == 3) {
      println(dir.y);
    }
    dir.rotate(HALF_PI);
    dir.normalize();
    dir.mult(param[i][1]);
    vertex(bezierX + dir.x, bezierY + dir.y);
  }
  endShape(CLOSE);
}

PVector getTan(float t) {
  float resultX, resultY;
  
  resultX = evaluateDerivativeX(t);
  resultY = evaluateDerivativeY(t);
  
  return new PVector(resultX, resultY);
}

float evaluateDerivativeX(float t) {
  float temp1, temp2, temp3, temp4, temp5, temp6;
  
  temp1 = -3 * p[0].pos().x * (1 - t) * (1 - t);
  temp2 = 3 * p[1].pos().x * (1 - t) * (1 - t);
  temp3 = -6 * p[1].pos().x * t * (1 - t);
  temp4 = -3 * p[2].pos().x * t * t;
  temp5 = 6 * p[2].pos().x * t * (1 - t);
  temp6 = 3 * p[3].pos().x * t * t;
  
  return temp1 + temp2 + temp3 + temp4 + temp5 + temp6;
}

float evaluateDerivativeY(float t) {
  float temp1, temp2, temp3, temp4, temp5, temp6;
  
  temp1 = -3 * p[0].pos().y * (1 - t) * (1 - t);
  temp2 = 3 * p[1].pos().y * (1 - t) * (1 - t);
  temp3 = -6 * p[1].pos().y * t * (1 - t);
  temp4 = -3 * p[2].pos().y * t * t;
  temp5 = 6 * p[2].pos().y * t * (1 - t);
  temp6 = 3 * p[3].pos().y * t * t;
  
  return temp1 + temp2 + temp3 + temp4 + temp5 + temp6;
}



class Point {
  
  private PVector pos;
  private color col;
  private int size;
  private boolean locked;
 
  Point(float x, float y, color p_col, int p_size) {
    pos = new PVector(x, y);
    col = p_col;
    size = p_size;
    locked = false;
  }
  
  void move(float x, float y) {
   pos.x = x;
   pos.y = y;
  }
  
  void show() {
    fill(col);
    noStroke();
    ellipse(pos.x, pos.y, size, size);
  }
  
  PVector pos() {
    return pos;
  }
  
  void lock() {
    locked = true;
  }
  
  void unlock() {
    locked = false;
  }
  
  boolean isLocked() {
    return locked;
  }
  
  boolean isHit(float x, float y) {
    return (pos.x - x) * (pos.x - x) + (pos.y - y) * (pos.y - y) < (size * size) / 4.0;
  }
}
2 Likes

So I am trying to implement the ideas of my previous post and have an issue with curvePoint() and curveTangent()

In the following example sketch, the point in the middle is not taken into account by curvePoint() and curveTangent(). As a result, out of the 16 original points only 15 are displayed on the screen.

Why is that point being skipped ?

EDIT: I’ve changed some bits in the code but I still have a point missing. I believe I’m not using curvePoint() and curveTangent() correctly.

def setup():
    size(800, 800, P2D)
    background(255)
    smooth(8)

    xmin, xmax = 200, 800
    ymean = height/2
    step = 20
    
    points = []
    n_points = 16
    factor = 10
   
    
    #Bending points
    for i in range(n_points):
        x = lerp(xmin, xmax , i/float(step))
        y = lerp(ymean, ymean, i/float(step))
        points.append(PVector(x, y+sin(i)*factor))
        
        #Draw points
        strokeWeight(4)
        point(x, y+sin(i)*factor +30)
        
        #Draw number
        fill(0)
        textSize(10)
        text(i, x, y+sin(i)*10 +40)

    #Convert original points to curvePoint + compute tangent
    for i in range(len(points)-1):
        
        t = i / float(points-1)
        
        x = curvePoint(points[i].x, points[i].x, points[i+1].x, points[i+1].x, t)
        y = curvePoint(points[i].y, points[i].y, points[i+1].y, points[i+1].y, t)
        
        tx = curveTangent(points[i].x, points[i].x, points[i+1].x, points[i+1].x, t)
        ty = curveTangent(points[i].y, points[i].y, points[i+1].y, points[i+1].y, t)
        
        a = atan2(ty, tx)
        a -= PI/2.0
        
        #Draw curvepoints
        strokeWeight(4)
        stroke(255, 30, 30)
        point(x, y)

        #Draw tangents
        stroke(80, 80, 205)
        strokeWeight(1)
        line(x, y, cos(a)*28 + x, sin(a)*28 + y)

The following seems to work, not sure why though…

def setup():
    size(800, 800, P2D)
    background(255)
    smooth(8)

    xmin, xmax = 200, 800
    ymean = height/2
    step = 20
    
    points = []
    n_points = 16
    factor = 10
   
    
    #Bending points
    for i in range(n_points):
        x = lerp(xmin, xmax , i/float(step))
        y = lerp(ymean, ymean, i/float(step))
        points.append(PVector(x, y+sin(i)*factor))
        
        #Draw points
        strokeWeight(4)
        point(x, y+sin(i)*factor +30)
        
        #Draw number
        fill(0)
        textSize(10)
        text(i, x, y+sin(i)*10 +40)
        
        
    #Adding last point to pointlist again - will serve as ending control point
    points.insert(-1, points[-1])
    

    #Convert original points to curvePoint + compute tangent
    for i in range(len(points)-1):
        
        t = i / float(len(points)*n_points)
        
        x = curvePoint(points[i].x, points[i].x, points[i+1].x, points[i+1].x, t)
        y = curvePoint(points[i].y, points[i].y, points[i+1].y, points[i+1].y, t)
        
        tx = curveTangent(points[i].x, points[i].x, points[i+1].x, points[i+1].x, t)
        ty = curveTangent(points[i].y, points[i].y, points[i+1].y, points[i+1].y, t)
        
        a = atan2(ty, tx)
        a -= PI/2.0
        
        #Draw curvepoints
        strokeWeight(4)
        stroke(255, 30, 30)
        point(x, y)

        #Draw tangents
        stroke(80, 80, 205)
        strokeWeight(1)
        line(x, y, cos(a)*28 + x, sin(a)*28 + y)

Here is a proof of concept.

Problem is I still haven’t figure out how to correctly use curvePoint() with curveTangent(). Here I’m multiplying xmax by 80 (number found at random) to push curvePoints (in red) to the left until they meet their corresponding tangent (blue lines). It does the trick but I shouldn’t be doing this normally.

All is happening here:

 #Convert reference points to curvePoint + compute tangent
    for i in range(len(points)-1):
        
        t = map(points[i].x, xmin, xmax*80, 0, 1) #multiplying by 80... just because it works
        
 
        x = curvePoint(points[i].x, points[i].x, points[i+1].x, points[i+1].x, t)
        y = curvePoint(points[i].y, points[i].y, points[i+1].y, points[i+1].y, t)

        tx = curveTangent(points[i].x, points[i].x, points[i+1].x, points[i+1].x, t)
        ty = curveTangent(points[i].y, points[i].y, points[i+1].y, points[i+1].y, t)
        
        a = atan2(ty, tx)
        a -= PI/2.0

It’s not really a big deal but that would be great if someone could spot the problem so we have a perfectly working example to share with everyone.

bending Manhattan

EDIT: script is now corrected

Full code
points, idx = [], []
X, Y = [], []
vertices = [[264, 290], [287, 295], [373, 267], [433, 207], 
            [589, 280], [580, 325], [509, 300], [309, 330], 
            [283, 320], [247, 375], [200, 330]]
nvertices = [0 for e in range(len(vertices))]
refpoints = [0 for e in range(len(vertices))]


def setup():
    global object, xmin, xmax, ymean
    size(800, 600, P2D)
    smooth(8)
    
    #Creating original object from vertices
    object = createShape()
    object.beginShape()
    object.stroke(0, 40)
    object.strokeWeight(1)
    object.noFill()
    for v in vertices:
        object.vertex(v[0], v[1])
    object.endShape(CLOSE)


    #Computing mid-line coordinates
    for i, v in enumerate(vertices):
        X.append(v[0])
        Y.append(v[1])
        
    xmin, xmax = min(X), max(X)
    ymean = sum(Y) / len(Y)
    
    #Displaying reference points (on mid-line)
    for x in sorted(X):
        idx.append(X.index(x))
        points.append(PVector(x, ymean))
        
    #Adding last point to pointlist again / will serve as ending control point
    points.insert(-1, points[-1])
  
    
def draw():
    background(255)
    
    #Drawing original object
    shape(object)
    
    
    #Bending mid-line (curving reference points with sin())
    for i, p in enumerate(points):
        p.y += cos(i*.4 + frameCount * .1) * 4
        fill(255, 30, 30)
        noStroke()
        rectMode(CENTER)
        rect(p.x, p.y, 6, 6)

        points[i] = PVector(p.x, p.y) 
        
    #Replacing ending control point
     points[-1] = points[-2]
            

    #Convert reference points to curvePoint + compute tangent
    for i in range(len(points)-1):
       
        x = points[i].x
        y = points[i].y

        tx = points[i+1].x - points[i].x
        ty = points[i+1].y - points[i].y
        
        a = atan2(ty, tx)
        a -= PI/2.0
        
        #computing tangent's height (ymean - original p.y)
        h = ymean - vertices[idx[i]][1] 
        
        #Drawing tangents and new vertices
        strokeWeight(1)
        stroke(30, 30, 255)
        line(x, y, cos(a)*h + x, sin(a)*h + y)
        strokeWeight(8)
        stroke(0, 205, 0)
        point(cos(a)*h + x, sin(a)*h + y)
        
        #Storing new vertices in their original order
        nvertices[idx[i]] = PVector(cos(a)*h + x, sin(a)*h + y) 
        
        #Storing reference points
        refpoints[i] = PVector(x, y)
                
        
    #Building object from new vertices
    beginShape()
    fill(200, 100, 0, 30)
    stroke(0)
    strokeWeight(1)
    for i in range(len(nvertices)):
        vertex(nvertices[i].x, nvertices[i].y)
    endShape(CLOSE)
    
    #Drawing backbone (mid-line)
    for i in range(len(refpoints)-1):
        strokeWeight(.6)
        line(refpoints[i].x, refpoints[i].y, refpoints[i+1].x, refpoints[i+1].y)
 
    #Text
    fill(0)
    textSize(14)
    for i in range(len(nvertices)): text(i, nvertices[i].x, nvertices[i].y-10)
2 Likes

Starting here:

t = map(points[i].x, xmin, xmax*80, 0, 1) #multiplying by 80 but don't know what this number corresponds to

As “xmax*n” increases, points[i].x will appear closer to “xmin”.
Therefore t tends toward zero.

If t is zero then:

x = curvePoint(points[i].x, points[i].x, points[i+1].x, points[i+1].x, 0.0)
y = curvePoint(points[i].y, points[i].y, points[i+1].y, points[i+1].y, 0.0)

Since points[i] is the start point to those curves:

x = points[i].x
y = points[i].y

Then:

tx = curveTangent(points[i].x, points[i].x, points[i+1].x, points[i+1].x, t)
ty = curveTangent(points[i].y, points[i].y, points[i+1].y, points[i+1].y, t)

Since there are two points instead of four (start, end, pair of control points), the tangent can be quickly estimated with the slope of a line segment.

Imagine a line between two points on a curve, gradually decrease the distance between those two points, and we get what looks like the tangent.
See Tangent reference, specifically the part of using secant.

Forgive me if you already know this.

With ‘t’ equal to zero, we are looking at points[i] and the slope to the next point:

tx = points[i+1].x - points[i].x
ty = points[i+1].y - points[i].y

Depending on your math instructor, you may have heard this as “rise over run”.
The function atan2(ty,tx) will then get the angle of the line segment. Subtracting PI/2 (90 degrees) will give a perpendicular angle.

The variable ‘t’ is no longer needed, at least in this case.

As an aside, even without the visual, the phrase “Bending Manhattan” paints a picture… :slight_smile:

2 Likes

@noahbuddy – Thanks a ton for the helpful clarifications. I wanted so badly to stick to the reference examples that I totally lost sight of the underlying logic.

Script above has been updated.

Big thanks to all of you.