0

I want to recreate the Parabolic Triangle using Processing: parabolic triangle gif

I've already got the "base" code figured out I think. What I need now is to cycle through my code to get the correct number of triangles/lines.

I'm using n to determine how many triangles should be drawn and as you can see, when changing n, the line positions are correct. I just need to make a cycle so that I the correct number of triangles is drawn.

This is my code:

float x1, y1, x2, y2, x3, y3;
float b, a;

float a1, b1, a2, b2, a3, b3, a4, b4, a5, b5, a6, b6, a7, b7, a8, b8, a9, b9;

void setup () {
  size(600, 600);
  background(255);

  x2=50;
  y2=height-50;

  x3=width-50;
  y3=y2;

  b=x3-x2;
  a=b*sqrt(3)/2;

  x1=x2+b/2;
  y1=y2-a;

  stroke(255, 0, 153);
  line(x1, y1, x2, y2);
  stroke(0, 255, 253);
  line(x2, y2, x3, y3);
  stroke(79, 144, 252);
  line(x3, y3, x1, y1);
}
void draw() {
  float n=4;
  float div=(1/n);

  a1=lerp(x1, x2, div);
  b1=lerp(y1, y2, div);
  a2=lerp(x2, x3, div);
  b2=lerp(y2, y3, div);
  a3=lerp(x3, x1, div);
  b3=lerp(y3, y1, div);

  a4=lerp(x1, x2, div+div);
  b4=lerp(y1, y2, div+div);
  a5=lerp(x2, x3, div+div);
  b5=lerp(y2, y3, div+div);
  a6=lerp(x3, x1, div+div);
  b6=lerp(y3, y1, div+div);

  a7=lerp(x1, x2, div+div+div);
  b7=lerp(y1, y2, div+div+div);
  a8=lerp(x2, x3, div+div+div);
  b8=lerp(y2, y3, div+div+div);
  a9=lerp(x3, x1, div+div+div);
  b9=lerp(y3, y1, div+div+div);

  line(a1, b1, a2, b2);
  line(a2, b2, a3, b3);
  line(a3, b3, a1, b1);

  line(a4, b4, a5, b5);
  line(a5, b5, a6, b6);
  line(a6, b6, a4, b4);

  line(a7, b7, a8, b8);
  line(a8, b8, a9, b9);
  line(a9, b9, a7, b7);
}
George Profenza
  • 50,687
  • 19
  • 144
  • 218
  • 2
    In order to get help here, it's best to make an attempt on your own first, and then ask when you run into problems. – Jacob Stuligross Jun 10 '21 at 01:57
  • 1
    @SergioVazquez As jstl pointed out, ideally you'd post your attempt as a minimal code snippet. stackoveflow doesn't work like a typical forum: it's a lot less verbose which makes it efficient (but also leaves less room for new people joining to adjust). That being said, checkout the [`lerp()`](https://processing.org/reference/lerp_.html): you pass it a start and end number and a normalised value (a value between 0.0 and 1.0) which acts as a percentage (0 = 0%(at the start value), 0.5 = 50% between the start and end value, 1.0(100%) at the end value). You'd use this for both x and the y coords – George Profenza Jun 10 '21 at 04:16
  • 1
    ...to interpolate between every two points of the triangle (e.g. vertex index 0 with 1, 1 with 2, 2 with 0): this will "subdivide"/split the edges into equal points along those lines. The only thing left to do is connect those points (simply loop through the linearly interpolated/subdivided line points and render the lines between those coordinates). Altough slightly different, [this p5.js answer](https://stackoverflow.com/questions/63525491/points-on-a-quadratic-curve-using-processing-p5-js/63531127#63531127) has a few similarities that might help. – George Profenza Jun 10 '21 at 04:20
  • @GeorgeProfenza, I think the code provided now is related to the idea you gave me. – Sergio Vazquez Jun 10 '21 at 05:16
  • 1
    @SergioVazquez Thank you for posting the code: so much clearer now :) You are very close. It would help getting familiar with [`for`](https://processing.org/reference/for.html) loops (and maybe [functions](https://processing.org/reference/void.html), though you can get away without them for now). In a loop some of the coordinates might need fixing, but you're mostly there – George Profenza Jun 10 '21 at 18:06
  • 1
    @LaurentS. This question was edited. Does this look ok for a reopen vote now ? (If not, please suggest how it could be improved) – George Profenza Jun 10 '21 at 18:14
  • 1
    @GeorgeProfenza. I'm familiar with for loops, it's just that I can't find a way to loop it correctly, that's what I need help with. – Sergio Vazquez Jun 10 '21 at 18:58
  • @SergioVazquez Looking over your code again, I can see where the loop would go, however you're doing quite a bit of accidentally redundant interpolation. (Notice if you change the value of n the interpolation doesn't look correct at other levels: ideally it would be, then simply adding a for loop to control n would do the trick). Interpolating between pairs of the triangle vertices should do (e.g. from x1,y1 to x2,y2, then x2,y2 to x3,y3 and finally x3,y3 back to x1,y1). Additionally `float div=(1/n);` isn't quite right, but it's very close... – George Profenza Jun 11 '21 at 13:17
  • @SergioVazquez ...You can run a modified version of your code [here](http://studio.sketchpad.cc/ChFdtaxcsN). (Notice the removal of redundant code). I'd name things slightly differently so it's easier to follow without thinking too much and group reusable blocks in functions. You can see an example of that [here](http://studio.sketchpad.cc/sp/pad/view/U0q0JyyhkW/latest). (Also notice lines can be batched into a single draw call using `beginShape()/endShape()`). The equilateral triangle can generalise to regular polygons: [p5.js demo](https://editor.p5js.org/george.profenza/sketches/u9MGuMfYS) – George Profenza Jun 11 '21 at 13:22
  • you may want to do a different colour mapping (or [interpolation via `lerpColor()`](https://processing.org/reference/lerpColor_.html)), however your main question was focused on the geometry, not the colours. – George Profenza Jun 11 '21 at 13:23

1 Answers1

0

As previously mentioned in the comments, there are a couple of things be aware of:

  1. interpolating more than necessary
  2. correctly mapping the lerp t parameter for interpolation (div in your code)

Regarding interpolation, your code currently has this section:

  a1=lerp(x1, x2, div);
  b1=lerp(y1, y2, div);
  a2=lerp(x2, x3, div);
  b2=lerp(y2, y3, div);
  a3=lerp(x3, x1, div);
  b3=lerp(y3, y1, div);

  a4=lerp(x1, x2, div+div);
  b4=lerp(y1, y2, div+div);
  a5=lerp(x2, x3, div+div);
  b5=lerp(y2, y3, div+div);
  a6=lerp(x3, x1, div+div);
  b6=lerp(y3, y1, div+div);

  a7=lerp(x1, x2, div+div+div);
  b7=lerp(y1, y2, div+div+div);
  a8=lerp(x2, x3, div+div+div);
  b8=lerp(y2, y3, div+div+div);
  a9=lerp(x3, x1, div+div+div);
  b9=lerp(y3, y1, div+div+div);

  line(a1, b1, a2, b2);
  line(a2, b2, a3, b3);
  line(a3, b3, a1, b1);

  line(a4, b4, a5, b5);
  line(a5, b5, a6, b6);
  line(a6, b6, a4, b4);

  line(a7, b7, a8, b8);
  line(a8, b8, a9, b9);
  line(a9, b9, a7, b7);

The first 6 interpolations (between the 3 points of the triangle) deal with one subdivision level. Rendering lines using a1,a2,a3,b1,b2,b3,c1,c2,c3 should do the trick. The rest of the interpolation seem to be manually interpolating at another subdivision level. This is bit copy/pasted manual labour you would want to avoid and improve with a for loop.

Regarding the interpolation parameter if you know the total number of subdivisions you can map it to the t parameter either manually (using division) or making use of the map() function

Here's a modified version of your code to illustrate these points:

float x1, y1, x2, y2, x3, y3;
float b, a;

float a1, b1, a2, b2, a3, b3;

void setup () {
  frameRate(10);
  size(600, 600);
  background(255);

  x2=50;
  y2=height-50;

  x3=width-50;
  y3=y2;

  b=x3-x2;
  a=b*sqrt(3)/2;

  x1=x2+b/2;
  y1=y2-a;
}

void draw() {
  background(255);
  int subdivisions = frameCount % 300;
  for(float n = 1; n < subdivisions; n++){
    float div= n / (subdivisions - 1);
  
    a1=lerp(x1, x2, div);//x1x2
    b1=lerp(y1, y2, div);//y1y2
    a2=lerp(x2, x3, div);//x2x3
    b2=lerp(y2, y3, div);//y2y3
    a3=lerp(x3, x1, div);//x3x1
    b3=lerp(y3, y1, div);//y3y1
    
    stroke(255, 0, 153);
    line(a1, b1, a2, b2);//x1x2,y1y2,x2x3,y2y3
    stroke(0, 255, 253);
    line(a3, b3, a1, b1);//x3x1,y3y1,x1x2,y1y2
    stroke(79, 144, 252);
    line(a2, b2, a3, b3);//x2x3,y2y3,x3x1,y3y1
  }
}

Parabolic Triangle Minimal Code

Personally I'd encapsulate it all in a reusable function (which would make it easier to reuse in other sketches). Here's an example where I'm also grouping all the line drawings in one go as oppose to multiple line() calls. This is a matter of preference mostly and should speed things up a tiny bit, however you should never sacrifice readability/flexibility easily.

float x1, y1, x2, y2, x3, y3;
float b, a;

color c1 = color(255, 0, 153);
color c2 = color(0, 255, 253);
color c3 = color(79, 144, 252);

void setup () {
  size(600, 600);
  background(255);

  x2 = 50;
  y2 = height - 50;

  x3 = width - 50;
  y3 = y2;

  b = x3 - x2;
  a = b * sqrt(3) / 2;

  x1 = x2 + b / 2;
  y1 = y2 - a;
}

void drawSubdivisions(int div, float x1, float y1,
                               float x2, float y2,
                               float x3, float y3,
                               color c1, color c2, color c3){
  
  // alternative rendering option: render the lines in a batch
  beginShape(LINES);
    
  // for each subdivision level
  for(int i = 0; i < div; i++){
    // map the subdivision level to the t interpolation parameter
    float t = map(i, 0, div - 1, 0.0, 1.0);
    // interpolate each coordinate 
    // connecting the current triangle vertex to the next 
    float x1x2 = lerp(x1, x2, t);
    float y1y2 = lerp(y1, y2, t);
    
    float x2x3 = lerp(x2, x3, t);
    float y2y3 = lerp(y2, y3, t);
    // ... and last vertex to first vertex
    float x3x1 = lerp(x3, x1, t);
    float y3y1 = lerp(y3, y1, t);
    
    // render lines for th interpolated shape
    
    stroke(c1);
    vertex(x1x2, y1y2); 
    vertex(x2x3, y2y3);
    
    stroke(c2);
    vertex(x2x3, y2y3); 
    vertex(x3x1, y3y1);
    
    stroke(c3);
    vertex(x3x1, y3y1);
    vertex(x1x2, y1y2);
  } 
  
  endShape();
}

void draw() {
  // map the number of subdivisions
  int subdivisions = round(map(mouseX, 0, width, 2, 90));
  
  background(0);
  drawSubdivisions(subdivisions, x1, y1, x2, y2, x3, y3, c1, c2, c3);
  text("move mouse on X axis to control subdivisions: " + subdivisions, 10, 15);
}

Parabolic Triangle encapsulated

For the sake of completeness here's an example written in p5.js which you can run bellow. I generalises the equilateral triangle to regular polygons and plays with colour mapping:

function setup() {
  createCanvas(900, 900);
  colorMode(HSB, 360, 100, 100);
}

function draw() {
  background(0, 0, 100);
  // map the number of points to Y axis
  let numPoints = round(map(mouseY, 0, height, 3, 6));
  // map the number of subdivisions to the X axis 
  let subdivisions = round(map(mouseX, 0, width, 1, 300));
  // compute regular polygon points
  let regularPolygonPoints = getRegularPolygonPoints(width * 0.5, height * 0.5, numPoints, 300);
  // optional: render regular polygon points
  if(mouseIsPressed) drawPoints(regularPolygonPoints);
  // compute and render interpolation lines between pairs of points
  drawLinearInterpolation(regularPolygonPoints, subdivisions);
  // instructions:
  text(`mouseY -> numPoints:${numPoints}\nmouseX -> subdivisions: ${subdivisions}`, 10, 15);
}

// make a list of points (p5.Vector with x, y properties and conveniently lerp) 
function getRegularPolygonPoints(x, y, sides, radius){
  // output list
  let points = [];
  // calculate the angle per polygon side (same as 360 degrees / sides)
  let angleIncrement = TWO_PI / sides;
  // for each side
  for(let i = 0 ; i < sides; i++){
    // increment the angle (and offset by 90 degrees to point up)
    let angle = (angleIncrement * i) - HALF_PI; 
    // insert the points into the list
    // p5.Vector.fromAngle converts and angle radius to an x and y position (polar to cartesian)
    // we offset(add) by x, y (transalation from 0,0 to center)  
    points.push(p5.Vector.fromAngle(angle, radius).add(x, y));
  }
  // return result
  return points;
}

// draw lines between interpolated pairs of points in sequental side order
function drawLinearInterpolation(points, subdivisions){
  // make strokes thinner the more subdivisions there are
  strokeWeight((300 - subdivisions) / 600);
  let numPoints = points.length;
  beginShape(LINES);
  // for each subdivision
  for(let i = 0; i < subdivisions; i++){
    let amount    = map(i, 0, subdivisions - 1, 0.0, 1.0);
    // for each point index from first to last (and back to first)
    for(let j = 0; j <= numPoints; j++){
      // array access current, previous and next points
      let current   = points[j % numPoints];
      let previous  = points[(j-1) < 0 ? numPoints - 1 : j - 1];
      let next      = points[(j+1) % numPoints];
      // interpolate between pairs of points: previous to current, current to next
      let previousToCurrent = p5.Vector.lerp(previous, current, amount);
      let currentToNext     = p5.Vector.lerp(current, next, amount);
      // play with stroke colour mapping
      stroke((mouseIsPressed ? (i + j) : (i * j)) % 360, 85, 100);
      // pass the point coordinates for shape rendering
      vertex(previousToCurrent.x, previousToCurrent.y);
      vertex(currentToNext.x, currentToNext.y);
    }
  }
  endShape();
}

// render a list of points
function drawPoints(points){
  strokeWeight(9);
  stroke(0, 100, 0);
  let numPoints = points.length;
  beginShape(POINTS);
  for(let i = 0 ; i < numPoints; i++){
    let point = points[i];
    vertex(point.x, point.y);
  }
  endShape();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>

Parabolic Regular Polygon (in p5.js)

You may choose to use a different colour mapping. Functions like lerpColor() and colorMode() may be useful. Have fun!

George Profenza
  • 50,687
  • 19
  • 144
  • 218