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.