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);
```

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);
```

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)
```

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.