Making Useless Blobs

Recently for a personal project I found a need to have some random blobs on a webpage. My first thought was just to whip some up in Adobe Illustrator but I thought it would nice to be able to write some code that could just create some organic shapes for me. I was able to find an npm package called blobs but I thought it'd be interesting to try and solve the problem myself. Essentially the goal was to write an algorithm that would accept some parameters and spit out an SVG compliant shape. To give you and idea of what we're trying to create here's an example:

Creating a polygon

In order to eventually create a blob my first thought was to start by making some random polygon with an arbitrary number of vertices as one our parameters. My reasoning was that if I could create some set of vertices that represented a polygon then later I would be able to connect the points together with some smoothing between them to create our final organic shape.

Determining angles

The simplest approach that I found in order to create some arbitrary polygon was to first start by generating a set of angles at which each of the vertices would fall relative to the center of our shape.

For instance, say we want our polygon to have 5 vertices we can take 2 PI and divide that by 5. Now we can start from zero and increment that value as we go along to create a set of angles. With this set of angles we now know where we want the vertices or our polygon to fall relative to the center.

const UNIT_CIRCLE = 2 * Math.PI; function createAnglesList(vertices) { const standardAngle = UNIT_CIRCLE / steps; let cumulativeSum = 0; const angles = [...new Array(steps)].map((_,i) => { return i * standardAngle; }); return angles; }

The issue with the above approach though is that it creates a set of equidistant angles every time. What we need to do is introduce a modifier that ensures the angles returned are different even if the same values for vertices is passed in. To do this we can extend the function slightly to allow a fractional value that adds some irregularity to the angles returned.

const UNIT_CIRCLE = 2 * Math.PI; function createRandomAnglesList(steps, irregularity) { const standardAngle = UNIT_CIRCLE / steps; let cumulativeSum = 0; const angles = [...new Array(steps)].map(() => { // accepts `a` and `b` and returns value between `a-b` and `a+b` const angle = giveOrTake(standardAngle, irregularity); cumulativeSum += angle; return angle; }); // normalize to force angles to sum to 2PI cumulativeSum /= UNIT_CIRCLE; return angles.map((angle) => angle / cumulativeSum); };

Passing a 0 to the above function for the irregularity value will still allow it to return equidistant angles but otherwise it will return a set of random angles for the same inputs.

Distance from center

Now that we have a set of angles our next step is to translate these angles into a set of vertices that can represent our polygon.

Let's think of each angle we have in our set as casting a ray from the center. In order to create a random shape we need only choose a point on this ray that fits within our bounding box. Our bounding box in this case is the width and height of the resulting SVG we are going to create.

In order to do this we'll first calculate the point [x,y] that falls on our bounding box for a given angle since this is the maximum point from the center our vertex can fall on. One thing to note is that we can calculate this point to always be in the first quadrant since our bounding shape is uniform in all quadrants and all we care about is it's distance from the center.

Once we have this point we can then calculate the distance maxRadius from the center of our shape to it using the distance formula and multiply that by a random number [0,1] creating a new distance from the center. With this length we can then convert that into a point [x,y] using a bit of trigonometry. Putting this together with our previously defined createRandomAnglesList our algorithm would look something like:

function createShape(vertices, width, height, irregularity) { const angleSet = createRandomAnglesList(vertices, irregularity); const halfWidth = width / 2; const halfHeight = height / 2; return anglesSet.map((currentAngle) => { const sin = Math.sin(currentAngle); const cos = Math.cos(currentAngle); // for given angle calculate point on bounding box const [x, y] = calculateIntersectionPoint( currentAngle, halfHeight, halfWidth ); // distance formula const maxRadius = Math.sqrt(x ** 2 + y ** 2); const radius = Math.random() * maxRadius; // convert to x, y position const point = [(wRadius + radius * cos), (hRadius + radius * sin)]; return point; }); }

In the above example we also have to think in terms of two different coordinate systems. This is because in standard Cartesian space our starting point [0,0] would normally exist at the center of our image.

However, since we're going to have to plot in SVG space our [0,0] point isn't at the center but rather the upper left-hand side. This doesn't matter when calculating angles or even the intersection point of the bounding box given these can be done in Cartesian space first and then translated. The translation occurs here:

const point = [(wRadius + radius * cos), (hRadius + radius * sin)];

As this line allows us to shift our x and y values from the top left to the center and then apply our new radius from the center using trigonometry.

It can also be noted the function calculateIntersectionPoint exist to calculate the point [x,y] where our ray from the center intersects our bounding box in the first quadrant. If we assume our bounding box is a rectangle the implementation for calculateIntersectionPoint could be as follows:

function calculateIntersectionPoint(angle, heightRadius, widthRadius) { // ignore slope since bounding shape is uniform const tan = Math.abs(Math.tan(angle)); let x = widthRadius; let y = tan * x; // whether to bound to x or y axis if (y > heightRadius) { y = heightRadius; x = y / tan; } return [x, y]; };

However, we can also apply our bounding box as an ellipsis instead with an implementation like so:

function calculateIntersectionPoint(angle, heightRadius, widthRadius, type) { const tan = Math.abs(Math.tan(angle)); const ab = widthRadius * heightRadius; const bottom = Math.sqrt(heightRadius ** 2 + widthRadius ** 2 * tan ** 2); const x = ab / bottom; const y = (ab * tan) / bottom; return [x, y]; };

In both implementations of calculateIntersectionPoint we don't really care about the sign of our tan value and as such can take the absolute value to force our point to be in the first quadrant. This is because, as stated before, we use this point to calculate the max distance from the center so direction doesn't matter.

With all this put together we now have an algorithm for creating a random polygon. Our inputs allow an arbitrary number of vertices as well as a value for irregularity to create some entropy. With that here's an example of what the output might look like for both types of bounding boxes with a width/height of 200px and an input of 12 for the vertices:

Smoothing lines

With that we've now created a list of vertices that can represent our shape. But if we were to just connect each of the points with a straight line we would end up with a very jagged blob. In order to eliminate these harsh lines we have to introduce another step.

What we'll do is introduce something called Bezier interpolation.

For the purpose of brevity I won't go too in depth on Bezier interpolation as there are a multitude of other articles explaining the concept better than I can. To get a better understanding I'd recommend these articles:

But to summarize the process, what we'll do is create a function that takes in 4 points a, b, c, d. From these 4 points we will return 2 points controlB, controlC that will represent the 2 control points for b and c. All together b, c, controlB, and controlC will represent the 4 parts that make up a single cubic bezier curve.

For the points a, b, c, d the points b and c represent the 2 points that form the current line we want to draw for our blob and a and d represent the adjacent points before and after our current line respectively.

Let's say we have a list of vertices [p1,p2,p3,p4] and a function that returns the 2 control points mentioned before called calculateControl. If we iterate over our list of vertices then the calls to calculateControl would look like:

  • Current line p1 to p2: calculateControl(p4, p1, p2, p3)
  • Current line p2 to p3: calculateControl(p1, p2, p3, p4)
  • Current line p3 to p4: calculateControl(p2 ,p3, p4, p0)
  • Current line p4 to p1: calculateControl(p3, p4, p0, p1)

The calculateControl function is rather lengthy so I won't post it all here. The implementation I used is essentially a translation of the one found here into Javascript though.

In order to create all the Bezier curves for the points in our polygon the code needed would look like this:

function allControlPoints(polygonPoints) { const loopedPoints = [ polygonPoints[polygonPoints.length - 1], ...polygonPoints, polygonPoints[0], polygonPoints[1] ]; return loopedPoints.slice(1, -2).map((point, index) => { const before = loopedPoints[index]; const a = point; const b = loopedPoints[index + 2]; const after = loopedPoints[index + 3]; const controlPoints = calculateControl(before, a, b, after); return controlPoints; }); }

Here the input of polygonPoints represents the returned list of points from our createShape function. The return value for this function is a 2-D list of control points with the same length of our input list and each value being a list of our 2 control points.

From here we can finally generate a path string that can be understood as the d value for a path tag as outlined in the SVG docs. All together the code to generate our nice organic blob is:

function generatePathString(vertices, irregularity) { // create our polygon vertices const points = createShape(vertices, irregularity); // initial cursor move const starting = `M ${points[0][0]} ${points[0][1]}`; // 2D list of all cubic bezier control points const controlPoints = allControlPoints(points); // reduce all this into a single d string return [...points, points[0]].slice(1).reduce((acc, curr, index) => { const [x, y] = curr; const [[a1, a2], [b1, b2]] = controls[index]; return `${acc} C ${a1} ${a2}, ${b1} ${b2}, ${x} ${y}`; }, starting); };

Here the initial starting variable represents the move M call that must be made to move our cursor to the starting point before we can begin to draw in our SVG. Now let's take a look at the output of our algorithm for various different parameter inputs:

Conclusion

If you want to play around a bit more I made an npm package containing all this code as well as a demo you can play around with to see how the parameters affect the final result.