Pattern Attractors

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