Introducing randomness and asymmetry to shape

To come back to the 1st point, I want to stress out the fact that the main reason to not draw too many points is not related to performance but rather to visual quality.
Using too many points will no necessarily result in a less choppy shape for the reasons I mentioned in my previous post.
But it is also true that using too few will also result in a choppy shape as the shape is only approximated by straight lines.
So there is a middle ground to be found.
Another trick is to use curves to approximate the shape like bezier, catmull-rom splines, etc. For example in my first post, the shape is using only 34 points in total (vs the 600+ currently used in the second version with the delta angle = 0.01) → That might still be a bit too much: something around 80-100 points should probably be enough.

Now for the second point.
In your code you have this section to calculate the coordinate of a point:

for (float a = 0; a < TWO_PI; a = a + 0.0001) {
  float r = 50;
  float x = c + r*pow(cos(a), 3);
  float y= d + r*sin(a)*2;
  vertex(x, y);
}

It looks like polar coordinates since you are using a radius r and an angle a but it is not really since, in that case, you would calculate x and y with those formulas:

x = r * cos(a);
y = r * sin(a);

Because it is not the case, your angle a does not really mean the angle from the x-axis (like it would with polar coordinates) and the same goes for r.
To have more control over the effect and fully understand the impact of the random values, I needed to convert everything to true polar coordinates.

Let’s take the following exemple to illustrate:

In purple you have the shape and the points defining the shape.
In your code, I could extract, for each point, the (x, y) coordinate - in blue on the figure.
This list of coordinates where imported into excel and based on those values I could compute the (r, theta) components of the polar coordinates using the formula in the bottom right.
So what I did was simply translate the (x, y) coordinates given by your program to (r, theta) coordinates if they where given in polar coordinates.

The next step was plotting the curve of the points (r, theta) - theta being the “x-axis” - and finding a function f(theta) that would match as closely as possible the original points.

Here’s how it looks in excel:

  • The blue columns x and y are the (x, y) coordinates computed by your code for a given shape.
  • The yellow columns are the translation to (r, theta) polar coordinates of the (x, y) coordinates. (the “theta mod” is a simple formula with some modulo to take into account the symmetry of your shape)
  • The green columns are the computed r values of different candidate functions used to approximate the real r values. To simplify, the less delta there is between the column r and one of the function column the better it is.

The excel graph I showed you above was simply the r value plotting against theta and one of the 5 test functions.

To simplify my life, I gave each function some parameters that I could change on the fly and see straight away the result on the graph:

Hope it helps.

3 Likes

By varying the value of mult in the xDrift function in the p5.js solution, you can control the degree of irregularity of the figures.

Here’s the function with comments:

function xDrift(x, y) {
  // let mult = 50.0; // low amount of irregularity
  // let mult = 100.0; // moderate amount of irregularity
  let mult = 200.0; // high amount of irregularity
  let div = 100.0;
  let n = noise(x / div, y / div) * mult;
  n = n - n / 2;
  return n;
}

The following is a capture of three figures with mult set to 50:

The following were created with mult set to 200:

But in my opinion, it would be a good idea to work with the solution that @jb4x posed in the previous post. It produces asymmetrical, but smoother, shapes.

2 Likes

Wow!! Yes, @jb4x, it helps so much. Thank you for all the explanations and incredibly good help! I can’t believe there are so many helpful people out there! Thanks again, also to you @javagar and @Chrisir

I will have to spend some more time going through your version of the code @jb4x, to try to understand everything, and then hopefully continuing working with this.

One (more) question… Not sure if it is possible, but if I wanted to change the tip of the shape to make it less pointy (or rounder, as in the process you described above with the curveCertex functions) - how would you go about this?

3 Likes

@javagar agree, I will work with the other solution, but this is still super helpful and cool, thank you so much!

1 Like

I would also go with the curveVertex since I think you will have more control over the final shape.

I mage a small program to add and position the vertices on the canvas:

ArrayList<Point> pts;
boolean hidePts;

void setup() {
  size (602, 700);
  
  pts = new ArrayList<Point>();
  hidePts = false;
}

void draw() {
  background(0);
  
  for(int i = 0; i < pts.size(); i++) {
    pts.get(i).update();
  }
  
  noFill();
  stroke(230);
  strokeWeight(2);
  beginShape();
  for(int i = 0; i < pts.size(); i++) {
    Point p = pts.get(i);
    curveVertex(p.pos.x, p.pos.y);
  }
  endShape();
  
  if (!hidePts) {
    for(int i = 0; i < pts.size(); i++) {
      pts.get(i).show();
    }
  }
}

void mousePressed() {
  for(int i = 0; i < pts.size(); i++) {
    Point p = pts.get(i);
    if (p.isHoovered()) {
      p.setSelect(true);
      p.computeDeltaFromMouse();
      return;
    }
  }
}

void mouseReleased() {
  for(int i = 0; i < pts.size(); i++) {
    pts.get(i).setSelect(false);
  }
}

void keyPressed() {
  if (key == 'n') {
    pts.add(new Point(50, 50));
  }
  if (key == 'h') {
    hidePts = !hidePts;
  }
  if (key == 'p') {
    for(int i = 0; i < pts.size(); i++) {
      println(pts.get(i).pos);
    }
  }
}

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(255, 0, 0);
    size = 10;
    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);
    ellipse(pos.x, pos.y, size, size);
  }
}
  • “n” to add a new point (you need at least 3 for the shape to begin drawing something)
  • “h” to hide the points and just leave the outline
  • “p” to print in the console the coordinates of the points

What I did in my first post was to put a background image to help me position the points.

Once you get the shape that you want, you can output the points in the console and copy them in a txt file like so.
Use the fact that you have 2 axes of symmetry to you advantage here.

-56:0
0:0
18:20
30:49
44:77
55:113
82:139
117:168
136:215
142:257
144:324
126:383
103:436
66:484
34:514
15:532
9:557
-9:557
-15:532
-34:514
-66:484
-103:436
-126:383
-144:324
-142:257
-136:215
-117:168
-82:139
-55:113
-44:77
-30:49
-18:20
0:0
56:0

Then I had another program loading that file, reading the points coordinates to add the vertices:

ArrayList<PVector> pts;
ArrayList<Integer> disp;
final int xOffset = 200;
final int yOffset = 21;

void setup() {
  size(400, 600);
  background(20);
  
  pts = new ArrayList<PVector>();
  disp = new ArrayList<Integer>();
  
  String[] lines = loadStrings("basePts.txt"); // The name of the file in which you stored the points
  for (int i = 0 ; i < lines.length; i++) {
    String[] pos = split(lines[i], ':');
    int x = Integer.valueOf(pos[0]) + xOffset;
    int y = Integer.valueOf(pos[1]) + yOffset;
    pts.add(new PVector(x, y));
  }
  
  lines = loadStrings("disp.txt"); // I used another file to store the maximum amount each vertices where allowed to move in the x direction.
  for (int i = 0 ; i < lines.length; i++) {
    disp.add(Integer.valueOf(lines[i]));
  }
  
  noFill();
  stroke(230);
  strokeWeight(2);
  beginShape();
  for (int i = 0 ; i < lines.length; i++) {
    PVector p = pts.get(i);
    curveVertex(p.x + random(disp.get(i)), p.y);
  }
  endShape();
}

Now of course you need to customize it so that instead of having only 1 outline, you get the 4 that you had and also it would be nice to have your shapes centered around (0, 0) so it is easy after to translate them where you want on the screen.

3 Likes

Wow, this is amazing! Looking forward to working with this. Thank you so much!

3 Likes

Hello @krissnutt,

An example of adding oscillations to a curve:

// Undulating Waves
// v1.0.0
// GLV 2022-02-13

void setup() 
  {
  size(640, 360);
  }

void draw() 
  {
  background(255);
  strokeWeight(2);
  
  circle(width/3, height/4, 100);
  circle(2*width/3, height/4, 100);
 
  float dir = 5*sin((frameCount%360)*TAU/360);
  float amp2 = 1*sin((frameCount%360)*TAU/360);
  //dir = 1;
  float ps2 = dir*((frameCount%360)*TAU/360);
  println(ps2);
  
  for (int i=0; i<width; i++)
    {
    float amp = 10*sin(i*(TAU/2)/width);
    float x = i*5*TAU/width;  
    float y = amp2*amp*sin(x+dir) + height/2 + 5*amp;
    point(i, y+100);
    point(i, height-y+100);
    }
 }

image

I wrote this quickly… morning workout for my brain.

undulate

:)

2 Likes

@jb4x how did this texfile look like? apologies already if this is super obvious

It’s just 1 value per line

0
0
2
2
5
5
5
7
...
2 Likes

Hi once again @jb4x, I still cannot get this to work, it draws only parts of the shape? That being said, I do not fully understand everything in the code, so I assume that is the reason :wink:

What’s the current state of your version of the code?
And which part do you not fully understand?
My advice would be to start simple and then add complexities. Start by just drawing 1 shape, then the second, the third and the 4th. Finally start to add random values (for the first only then the second and so on…)

Thank you. I mean that I do not understand what kind of values I should put in the text-file so the whole shape is displayed when running the second program. I have not made any adjustments to this one yet. I have not worked with Arraylists or even vectors before so currently watching Daniel Shiffman tutorials and hope that will help me understand better!

The text files simply contains the coordinates of my curve vertices.

All the magic happens here:

String[] lines = loadStrings("basePts.txt");
for (int i = 0 ; i < lines.length; i++) {
  String[] pos = split(lines[i], ':');
  int x = Integer.valueOf(pos[0]);
  int y = Integer.valueOf(pos[1]);
  pts.add(new PVector(x, y));
}

The first thing is to load the file containing the vertices coordinates:

String[] lines = loadStrings("basePts.txt");

Each line of that string is stored in a different spot of the lines array. So with my previous file we would have:

line[0] = -56:0
line[1] = 0: 0
line[2] = 18:20
...

Then we loop through each elements of the lines array, so we loop through each point coordinates:

for (int i = 0 ; i < lines.length; i++) {
  ...
}

Since each coordinate is composed of 2 values but we have it still in a string format, we need to convert it. That’s the goal of this line:

String[] pos = split(lines[i], ':');

Here we are asking to find the character : and to return what’s on the left and what’s on the right in the pos array.
For example:

String[] pos = split(line[2], ':') = split("18:20", ':')

pos[0] = "18"
pos[1] = "20"

We still need one more step though as they are still String values and not Integer values:

int x = Integer.valueOf(pos[0]);
int y = Integer.valueOf(pos[1]);

Finally I’m storing the coordinates of the vertices as PVector so I can reuse them later the way I want:

pts.add(new PVector(x, y));

Thank you, this is a great explanation. I have made some (beautiful) shapes with the first program you wrote and loaded the file containing the vertices.

What I struggle with is understanding and using is the text-file you have called “disp.txt”

Ha ok. I’m using this file to set the maximum number of pixels I’m willing to move each points.

So if the file looks like this:

0
0
2
2
3
...

It means that I’m willing to offset the first point (compare to it’s normal position set in the other file) by 0 pixel, then the second by 0 pixel, then the third by 2, the 4th by 2, the 5th by 3 and so on…

Again, the idea is to not move the points that are on the top and bottom of the shape as it is quite crucial that they don’t move and to allow the side points to be moved a bit more.

Ok, I see! Depending on which values I put in this file I either get the message: “IndexOutOfBoundsException: Index 24 out of bounds for length 24” or "NumberFormatException: For input string: “” "

It will be hard to help you without seeing the file that is causing the issue as well as your code if you modified it.

yes, true, sorry. Now the file looks like this:

0
0
2
2
3
3
4
4
3
0
0
2
3
4
5
6
6
5
5
0
0

Maybe I am not adding the correct number of values?

What is the part of your code that throw the error?

Based on the error, I presume it is this part:

  for (int i = 0 ; i < lines.length; i++) {
    PVector p = pts.get(i);
    curveVertex(p.x + random(disp.get(i)), p.y); // This line
  }

If you have only 2 values in your file and you are trying to access the 3 value, it does not exit and it will throw an out of bound exception.

For your second issue: "NumberFormatException: For input string: “” " it feels like the file was not containing only numbers but also characters so conversion to integer was not working.

It is this line:

  for (int i = 0 ; i < lines.length; i++) {
    disp.add(Integer.valueOf(lines[i])); // This line
  }