Pattern Attractors

this is the image

What is your problem then, can you be more specific?

Hi jakejake,

This looked neat, so I gave it a go here.

I know nothing about Grasshopper, but this Generative Landscapes tutorial laid out the process fairly well. Between that and the steps @jb4x laid out, I had a path forward. To repeat and rephrase what’s already been said:

  • I took an ideal grid of the range ([0 - 1], [0 - 1]).

  • I subdivided that range by a count, storing the result in an array of vectors.

  • I used a sine function to generated an array of vectors.

  • How did I know which distance from the cell to the curve is smallest? Here’s why the sketch is slow, and where there may be room to improve.

    • I wanted the array of vectors for the curve to remain unchanged, so I could keep drawing it. I made a copy of the array; then I sorted the copy according to custom criteria. The point in the curve nearest to the cell would be the first element.
  • The distance info needs to be compared against a maximum possible distance to be converted to a range [0 - 1]. Once that’s through, I can supply that range to lerp or lerpColor, etc.

Sorting an array by a custom criterion requires a bit more gear in Java, but the idea remains the same.

import java.util.Comparator;
import java.util.Arrays;

static void drawEllipses(PGraphics r, 
  PVector[] grid, PVector[] curve, 
  float left, float right, 
  float bottom, float top, 
  float padding, float minScale, 
  color clr0, color clr1, int clrMode, 
  DistSort sorter) {
  /* ... */
  for (int i = 0; i < len; ++i) {
    /* ... */
    Arrays.sort(curve, sorter);
    /* ... */
  }
  /* ... */
}

static class DistSort implements Comparator < PVector > {
  /* ... */
  int compare(PVector a, PVector b) {
     /* Return either -1, 0, or 1. */
  }
}

Hope that helps out. Best.

4 Likes

If I understand correctly, what you’re saying is you know how to find the distance of a point from say a circle, but you don’t know how to find a point on a curve.

If that’s the case, I can point you towards Bezier curves, which are really neat! (And which are built into Processing with bezier())

If you have any more questions, just ask.

1 Like

Haha, I’ve actually been playing around with bezier curves recently and this is a LOT harder than I thought it would be.

Be gentle, it’s not finished

3 Likes

Thanks for the replys guys! sorry as a new user I got locked out for a little bit.

jb4x, your breakdown helped me wrap my head around the concept. It sounds like with attractor shape will need to be represented by xy points, where distance is checked for each point in the grid.

Yes tony, I wasn’t sure how to check distance from a curve / shape, but it seems like checking an array of xy coordinates is the best way to go. I could probably do a pshape or import an svg with geomerative and use the polygonizer too.

The only success I have had was blurring shapes in photoshop and mapping the brightness of each coordinate to a circle size which is easy to code, but not ideal.

thanks for making this @behreajj! I appreciate the notes as well, I will have to study this a bit to understand whats going on, but that is the idea of what I’m trying to achieve.

One approach is to take a small number of points along the curve to reduce the calculation of distance. You’d probably have to run some tests to find out the optimal balance of points vs perfermance.

1 Like

As @tony said, finding the shortest distance from a point to a bezier curve is not that trivial.

One way one can approach would be the following.

First let’s consider

  • P0(x0,y0)
  • P1(x1,y1)
  • P2(x2,y2)
  • P3(x3,y3)

the control points of the bezier curve.

The curve is then defined by all the points P(t) for 0 <= t <= 1 as followed:
P(t) = P0*(1-t)^3 + 3*P1*t*(1-t)^2+3*P2*t^2*(1-t)+P3*t^3

Now let’s consider a point D(x, y)
The distance from D to the bezier curve is: min(for all P of the curve) ||D-P||

I am not developing the formula here because it would be too long but it is easy to get ||D-P||² with all the parameters given above.

It would be feasible to get d(||D-P||²)/dt and then use a root finding algorithm to get the point were the smallest distance is.

Hope it can help :slight_smile:

PS: Is there a way to properly display math formulas?

1 Like

Luckily, Processing also offers the bezierPoint() function.

Before you use this I would definitely do some reading on bezier curves. Though they are a little daunting at first, the main thing you need to understand is how control points relate to the curve.

Woah, another shameless plug to a bezier curve maker ! Play around with this sketch. Control points and anchors sound confusing, but seeing how you can manipulate a curve with them, hands on, is pretty intuitive.

2 Likes

That’s a pretty cool function, it can be really useful!

This is one of the reason why they became so famous. That and the fact that you can store a whole curve with just 4 points!

Hey @tony,

I played around with your sketch and I got some points of interest drawn on the curve. There are still some cases where Interesting features are left over but it could maybe be a nice start to efficiently compute the minimum distance.

I was thinking about a recursive method where you take a start point and a end point of the curve, split it in section, find the section where the minimum is and use the start point and end point of that section to apply the function again.

Anyway, here is the modified version of your code:

Bezier bez;
Point selectedPoint;

// --- Palette ---
color red = #d04648;
color dark = #140c1c;
color yellow = #dad45e;
color light_blue = #6dc2ca;
color peppermint = #deeed6;

void setup() {
  
	//fullScreen();
	size(600, 600);
  //rectMode(CENTER);
  smooth();
  
  
  bez = new Bezier(width/2, height/2);
}

void draw() {
  background(dark);
	stroke(peppermint);
	strokeWeight(10);
	rect(0,0,width,height);
	
  bez.draw();
}

//=========================================
// Event Call-backs
//-----------------

void mousePressed() {
  selectedPoint = getPointUnderMouse();
}

void mouseDragged() {
  if (selectedPoint != null) {
    selectedPoint.x = mouseX;
    selectedPoint.y = mouseY;
    
    bez.recalculateCenter();
  }
}

void mouseReleased() {
  selectedPoint = null;
	println('bezier(' + 
					bez.start.x + ", " + bez.start.y + ", " + 
					bez.ctrlStart.x + ", " + bez.ctrlStart.y + ", " + 
					bez.ctrlEnd.x + ", " + bez.ctrlEnd.y + ", " + 
					bez.end.x + ", " + bez.end.y + ")");
}


//=========================================
// Classes
//------------------

class Bezier {
  Point start;
  Point end;
  Point ctrlStart;
  Point ctrlEnd;
	Point p1;
	Point p2;
	Point p3;
	Point p4;
  
  Bezier(float x, float y) {
    float len = 100;
    p1 = new Point(0, 0, peppermint);
		p2 = new Point(0, 0, peppermint);
		p3 = new Point(0, 0, peppermint);
		p4 = new Point(0, 0, peppermint);
		p5 = new Point(0, 0, peppermint);
    start = new Point(x - len, y - len, light_blue);
    end = new Point(x + len, y - len, yellow);
    ctrlStart = new Point(x - len, y + len, light_blue);
    ctrlEnd = new Point(x + len, y + len, yellow);
		recalculateCenter();
  }
  
  void recalculateCenter() {
		float a;
		float b;
		float c;
		float delta;
		float sqrtDelta;
		float x;
		float y;
		float t1;
		float t2;
		
		p1.x = 10;
		p1.y = 10;
		p2.x = 10;
		p2.y = 10;
		p3.x = 10;
		p3.y = 10;
		p4.x = 10;
		p4.y = 10;
		
		// Calculation for x
		a = (-start.x) + 3 * ctrlStart.x - 3 * ctrlEnd.x + end.x;
		b = 2 * (start.x - 2 * ctrlStart.x + ctrlEnd.x);
		c = (-start.x) + ctrlStart.x;
		
		delta = b*b - 4 * a * c;
		
		t1 = -1;
		t2 = -1;
		if (delta >= 0) {
			sqrtDelta = sqrt(delta);
			t1 = ((-b) - sqrtDelta) / (2 * a);
			t2 = ((-b) + sqrtDelta) / (2 * a);
		}
		
		if (t1 >= 0 && t1 <= 1) {
			p1.x = bezierPoint(start.x, ctrlStart.x, ctrlEnd.x, end.x, t1);
			p1.y = bezierPoint(start.y, ctrlStart.y, ctrlEnd.y, end.y, t1);
		}
		if (t2 >= 0 && t2 <= 1) {
			p2.x = bezierPoint(start.x, ctrlStart.x, ctrlEnd.x, end.x, t2);
			p2.y = bezierPoint(start.y, ctrlStart.y, ctrlEnd.y, end.y, t2);
		}
		
		
		// Calculation for y
		a = (-start.y) + 3 * ctrlStart.y - 3 * ctrlEnd.y + end.y;
		b = 2 * (start.y - 2 * ctrlStart.y + ctrlEnd.y);
		c = (-start.y) + ctrlStart.y;
		
		delta = b*b - 4 * a * c;
		
		t1 = -1;
		t2 = -1;
		if (delta >= 0) {
			sqrtDelta = sqrt(delta);
			t1 = ((-b) - sqrtDelta) / (2 * a);
			t2 = ((-b) + sqrtDelta) / (2 * a);
		}
		
		if (t1 >= 0 && t1 <= 1) {
			p3.x = bezierPoint(start.x, ctrlStart.x, ctrlEnd.x, end.x, t1);
			p3.y = bezierPoint(start.y, ctrlStart.y, ctrlEnd.y, end.y, t1);
		}
		if (t2 >= 0 && t2 <= 1) {
			p4.x = bezierPoint(start.x, ctrlStart.x, ctrlEnd.x, end.x, t2);
			p4.y = bezierPoint(start.y, ctrlStart.y, ctrlEnd.y, end.y, t2);
		}
		
		
		//Get mid point
		p5.x = bezierPoint(start.x, ctrlStart.x, ctrlEnd.x, end.x, 0.5);
		p5.y = bezierPoint(start.y, ctrlStart.y, ctrlEnd.y, end.y, 0.5);
  }
  
  void draw() {
    stroke(peppermint);
		strokeWeight(1);
    line(start.x, start.y, ctrlStart.x, ctrlStart.y);
    line(ctrlEnd.x, ctrlEnd.y, end.x, end.y);
  
    stroke(red);
		strokeWeight(3);
    noFill();
    bezier(
      start.x, start.y,
      ctrlStart.x, ctrlStart.y, 
      ctrlEnd.x, ctrlEnd.y, 
      end.x, end.y
		);
		
		start.draw();
    ctrlStart.draw();
    ctrlEnd.draw();
    end.draw();
		p1.draw();
		p2.draw();
		p3.draw();
		p4.draw();
		p5.draw();
		noFill();
  }
  
  Point[] getPoints() {
    return new Point[] {
      start, ctrlStart, ctrlEnd, end};
  }
}


class Point {
  float x, y;
  float drawSize = 16;
  color drawColor = light_blue;

  Point(float a, float b, color c) {
    x = a;
    y = b;
		drawColor = c;
  }
  
  void draw() {
    noStroke();
    fill(this.drawColor);
		ellipse(x, y, drawSize, drawSize);
  }
    
  boolean hotSpotContains(float x, float y) {
    return dist(x, y, this.x, this.y) < drawSize;
  }
  
  void updatePoints (Point newPoint) {
    x = newPoint.x;
    y = newPoint.y;
  }
}


//=========================================
// Helper functions
//------------------

Point getPointUnderMouse() {
  Point[] points = bez.getPoints();
  for (int j = 0; j < points.length; j++) {
    if (points[j].hotSpotContains(mouseX, mouseY))
      return points[j];
  }
  return null;
}

Point averagePoint (Point[] points) {
  float xSum = 0;
  float ySum = 0;
  for (int i = 0; i < points.length; i++) {
    xSum += points[i].x;
    ySum += points[i].y;
  }
  return new Point(xSum/points.length, ySum/points.length, peppermint);
}
2 Likes

I think that’s an interesting idea! It’s almost like a binary search, but with distance-from-mouse.

Your idea also kind of relates to how the de Casteljau algorithm draws a curve via recursion.

Well… I thought about it today and there are some extreme cases that are real trouble makers.

Take the image below:

If we try to figure out the distance from the yellow dot to the Bezier curve the subdivision is really important and starting with a too low subdivision might lead to the algorithm think the minimum is in the wrong area…

I tried to cut the shape in half and use a 3 degree bezier curve to get each half but sadly it does not fit. It would have been great since a 3 degree bezier curve is represented by a 2nd degree polynom so then, when trying to figure out the smallest distance, one would have taken the squared and tried to found the roots of the derivative which would have been of degree 3 (which we know the formulas).

Now, I think that with the interest points it is possible to approximate nicely the curve with just a few points and lines in between. Then we would just need to compute the distance from to point to each line a get the minimum.
The question would be what is a good approximation depending on the user scenario…

Hello all!

Curve attractors used to be the "Hello, world" of grasshopper sketches, it’s interesting to see how simplified it was to do when it was embedded in Rhino versus making it from scratch here. I’ll be using processing-python mode to do this because it’s much simpler to explain.

After setting up your grid of rectangles and ellipses, you need a way of drawing a curve from a small set of points. The way of drawing it should include a way to access the interpolated points for later use. One way of doing that is, let’s say you start with a list of four points as follows:

    n = 4
    curvePoints = [PVector(1.0*i*width/n,random(height)) for i in range(n+1)]

Then you can define a number for resolution where you find all interpolation points in between these points:

    resolution = 50
    interpolations = [lerpVectors(curvePoints,
                                  1.0*i/(resolution-1)) for i in range(resolution)]

lerpVectors() is essentially a function that takes in a list of points to interpolate in between and an interpolation amount (shout to @jeremydouglass for showing me this technique here)

def lerpVectors(vecs, amt):
    if(len(vecs)==1): return vecs[0]
    
    spacing = 1.0/(len(vecs)-1);
    lhs = floor(amt / spacing);
    rhs = ceil(amt / spacing);
    
    try:
        return PVector.lerp(vecs[lhs], vecs[rhs], amt%spacing/spacing);
    except:
        return PVector.lerp(vecs[constrain(lhs, 0, len(vecs)-2)], vecs[constrain(rhs, 1, len(vecs)-1)], amt);

linearoutput

This function as it is will return linear interpolations, hence just a linear output. There are other kinds of interpolations (check this article out). I will use instead a custom cosine interpolation to get some simple curves, but you can play with that and read more from the article.

def cosineLerpVec(v1,v2,amt):
    x = lerp(v1.x,v2.x,amt)
    y = cosineLerp(v1.y,v2.y,amt)
    return PVector(x,y)
def cosineLerp(y1,y2,mu):
    mu2 = (1-cos(mu*PI))/2
    return y1*(1-mu2)+y2*mu2

def lerpVectors(vecs, amt):
    if(len(vecs)==1): return vecs[0]
    
    spacing = 1.0/(len(vecs)-1);
    lhs = floor(amt / spacing);
    rhs = ceil(amt / spacing);
    
    try:
        return cosineLerpVec(vecs[lhs], vecs[rhs], amt%spacing/spacing);
    except:
        return cosineLerpVec(vecs[constrain(lhs, 0, len(vecs)-2)], vecs[constrain(rhs, 1, len(vecs)-1)], amt);

cosineoutput

Now the attractor part becomes simple. What we need to do is measure distances of each point in the grid with a point on the curve. The point in the grid will take the minimum distance to the curve. (ellipsePositions is a list of X by Y grid points):

    # Get distances
    distances = []
    for i in ellipsePositions:
        d = []
        
        # Get all distances to curve points
        for j in interpolations:
            d.append(PVector.dist(i,j))
        
        # Get minimum distance (closest point to curve)
        distances.append(min(d))
    
    # Normalize distances for later scaling
    distNormed = [ map(i,min(distances),max(distances),0.0,1.0)
                   for i in distances ]

The result can now be easily displayed if we call distances and ellipse positions together:

    for i,j in zip(distNormed,ellipsePositions):
        diameter = i*spaceX
        with pushStyle():
            fill(0)
            ellipse(j.x,j.y,diameter,diameter)
        rect(j.x,j.y,spaceX,spaceX)

finaloutput

Code in the end.

Summary
def setup():
    size(400,400)
    background(255)
    noFill()
    rectMode(CENTER)
    
    X = 25
    Y = 25
    
    spaceX = 1.0*width/X
    spaceY = 1.0*height/Y
    
    ellipsePositions = [PVector(i*spaceX,j*spaceY) for i in range(X+1) for j in range(Y+1)]
    n = 4
    curvePoints = [PVector(1.0*i*width/n,random(height)) for i in range(n+1)]
    resolution = 50
    interpolations = [lerpVectors(curvePoints,
                                  1.0*i/(resolution-1)) for i in range(resolution)]
    # Get distances
    distances = []
    for i in ellipsePositions:
        d = []
        
        # Get all distances to curve points
        for j in interpolations:
            d.append(PVector.dist(i,j))
        
        # Get minimum distance (closest point to curve)
        distances.append(min(d))
    
    # Normalize distances for later scaling
    distNormed = [ map(i,min(distances),max(distances),0.0,1.0)
                   for i in distances ]
    # Display curve
    with beginShape():
        for i in interpolations:
            vertex(i.x,i.y)
    
    # Display ellipses
    for i,j in zip(distNormed,ellipsePositions):
        diameter = i*spaceX
        with pushStyle():
            fill(0)
            ellipse(j.x,j.y,diameter,diameter)
        rect(j.x,j.y,spaceX,spaceX)
    
def cosineLerpVec(v1,v2,amt):
    x = lerp(v1.x,v2.x,amt)
    y = cosineLerp(v1.y,v2.y,amt)
    return PVector(x,y)
def cosineLerp(y1,y2,mu):
    mu2 = (1-cos(mu*PI))/2
    return y1*(1-mu2)+y2*mu2

def lerpVectors(vecs, amt):
    if(len(vecs)==1): return vecs[0]
    
    spacing = 1.0/(len(vecs)-1);
    lhs = floor(amt / spacing);
    rhs = ceil(amt / spacing);
    
    try:
        return cosineLerpVec(vecs[lhs], vecs[rhs], amt%spacing/spacing);
    except:
        return cosineLerpVec(vecs[constrain(lhs, 0, len(vecs)-2)], vecs[constrain(rhs, 1, len(vecs)-1)], amt);

I tried to break down thought process on this example as much as possible. Let me know if you need a clarification on anything that’s not clear. You could go off on a lot of tangents here: animate it, superimpose it, change display (doesn’t have to be ellipses diameters) etc. I’m interested to see what you do with this.

3 Likes

sadly, there doesn’t seem to be a button for that format. But, there is a plugin out there that i will have the admins look at. - Discourse Math - plugin - Discourse Meta

1 Like

@WakeMeAtThree thats great! The explanation is really helpful. I can’t wait to play around with this.

I tried to convert your program to java but it’s not quite there, I don’t think the minimum distances are getting stored correctly. Any suggestions?

int X = 25;
int Y = 25;
int n = 4;

PVector[] ellipsePositions = new PVector[X*Y];
PVector[] curvePoints = new PVector[n];
PVector[] interpolations;

void setup(){
    size(400,400);
    background(255);
    noFill();
    rectMode(CENTER);
    
    float spaceX = 1.0*width/X;
    float spaceY = 1.0*height/Y;
    
    for(int i = 0; i < X; i++){
      for(int j = 0; j < Y; j++){
        ellipsePositions[(i*Y)+j] = new PVector(i*spaceX,j*spaceY);
      }
    }
    
    
    
    for(int p = 0; p < n; p++){
      curvePoints [p] = new PVector(1.0*p*width/float(n),random(height));
    }
    
    int resolution = 50;
    interpolations = new PVector[resolution];
    
    for(int r = 0; r < resolution; r++){
    interpolations[r] = lerpVectors(curvePoints, 1.0*r/(resolution));
    }
    
    // Get distances
    float[] distances = new float[ellipsePositions.length];
    
    for(int pt = 0; pt < ellipsePositions.length; pt++){
      float[] d = new float[interpolations.length];
      
      // Get all distances to curve points
      for(int j = 0; j < interpolations.length; j++){
          
          d[j] = PVector.dist(ellipsePositions[pt],interpolations[j]);
      }
      // Get minimum distance (closest point to curve)
      distances[pt] = min(d);
    }
    
    // Normalize distances for later scaling
    float[] distNormed = new float[distances.length];
    for(int dn = 0; dn < distances.length; dn++){
      distNormed[dn] = map(dn,min(distances),max(distances),0.0,1.0);
    }
    // Display curve
    beginShape();
    for (PVector i : interpolations){
        vertex(i.x,i.y);
    }
    endShape();
    
    // Display ellipses
    //for i,j in zip(distNormed,ellipsePositions):
    for(int j = 0; j < ellipsePositions.length; j++){
        float diameter = distNormed[j]*spaceX;
        fill(0);
        noStroke();
        ellipse(ellipsePositions[j].x, ellipsePositions[j].y,diameter,diameter);
        noFill();
        stroke(0);
        rect(ellipsePositions[j].x, ellipsePositions[j].y,spaceX,spaceX);
    }
}


PVector cosineLerpVec(PVector v1, PVector v2, float amt){
    float x = lerp(v1.x,v2.x,amt);
    float y = cosineLerp(v1.y,v2.y,amt);
    return new PVector(x,y);
}


float cosineLerp(float y1, float y2, float mu){
    float mu2 = (1-cos(mu*PI))/2;
    return y1*(1-mu2)+y2*mu2;
}


PVector lerpVectors(PVector[] vecs, float amt){ //amt is lerp position from resolution
    if(vecs.length==1){
      return vecs[0];
    }
    
    float spacing = 1.0/(vecs.length-1); //dist between two points
    int lhs = floor(amt / spacing); //find position start?
    int rhs = ceil(amt / spacing); // find position end?
    
    try{
        return cosineLerpVec(vecs[lhs], vecs[rhs], amt%spacing/spacing);
    }
    catch(Exception  e){
        e.printStackTrace();
        return cosineLerpVec(vecs[constrain(lhs, 0, vecs.length-2)], vecs[constrain(rhs, 1, vecs.length-1)], amt);
    }
}

@jakejake Take a good long look at this part of your code.

    // Normalize distances for later scaling
    float[] distNormed = new float[distances.length];
    for(int dn = 0; dn < distances.length; dn++){
      distNormed[dn] = map(dn,min(distances),max(distances),0.0,1.0);
    }
2 Likes

Ahh Thanks!

Here is the updated code

int X = 25;
int Y = 25;
int n = 4;

PVector[] ellipsePositions = new PVector[X*Y];
PVector[] curvePoints = new PVector[n];
PVector[] interpolations;

void setup(){
    size(400,400);
    background(255);
    noFill();
    rectMode(CENTER);
    
    float spaceX = 1.0*width/X;
    float spaceY = 1.0*height/Y;
    
    for(int i = 0; i < X; i++){
      for(int j = 0; j < Y; j++){
        ellipsePositions[(i*Y)+j] = new PVector(i*spaceX,j*spaceY);
      }
    }
    
    for(int p = 0; p < n; p++){
      curvePoints [p] = new PVector(1.0*p*width/float(n),random(height));
    }
    
    int resolution = 50;
    interpolations = new PVector[resolution];
    
    for(int r = 0; r < resolution; r++){
    interpolations[r] = lerpVectors(curvePoints, 1.0*r/(resolution));
    }
    
    // Get distances
    float[] distances = new float[ellipsePositions.length];
    
    for(int pt = 0; pt < ellipsePositions.length; pt++){
      float[] d = new float[interpolations.length];
      
      // Get all distances to curve points
      for(int j = 0; j < interpolations.length; j++){
          d[j] = PVector.dist(ellipsePositions[pt],interpolations[j]);
      }
      // Get minimum distance (closest point to curve)
      distances[pt] = min(d);
    }
    
    // Normalize distances for later scaling
    float[] distNormed = new float[distances.length];
    for(int dn = 0; dn < distances.length; dn++){
      distNormed[dn] = map(distances[dn],min(distances),max(distances),0.0,1.0);
    }
    // Display curve
    beginShape();
    for (PVector i : interpolations){
        vertex(i.x,i.y);
    }
    endShape();
    
    // Display ellipses
    for(int j = 0; j < ellipsePositions.length; j++){
        float diameter = distNormed[j]*spaceX;
        //println(distNormed[j]);
        fill(0);
        noStroke();
        ellipse(ellipsePositions[j].x, ellipsePositions[j].y,diameter,diameter);
        noFill();
        stroke(0);
        rect(ellipsePositions[j].x, ellipsePositions[j].y,spaceX,spaceX);
    }
}


PVector cosineLerpVec(PVector v1, PVector v2, float amt){
    float x = lerp(v1.x,v2.x,amt);
    float y = cosineLerp(v1.y,v2.y,amt);
    return new PVector(x,y);
}


float cosineLerp(float y1, float y2, float mu){
    float mu2 = (1-cos(mu*PI))/2;
    return y1*(1-mu2)+y2*mu2;
}


PVector lerpVectors(PVector[] vecs, float amt){ //amt is lerp position from resolution
    if(vecs.length==1){
      return vecs[0];
    }
    
    float spacing = 1.0/(vecs.length-1); //dist between two points
    int lhs = floor(amt / spacing); //find position start?
    int rhs = ceil(amt / spacing); // find position end?
    
    try{
        return cosineLerpVec(vecs[lhs], vecs[rhs], amt%spacing/spacing);
    }
    catch(Exception  e){
        return cosineLerpVec(vecs[constrain(lhs, 0, vecs.length-2)], vecs[constrain(rhs, 1, vecs.length-1)], amt);
    }
}
2 Likes