While playing around with Chaikin curves (which I first heard about from Ben Kovach in his talk about generative art) I saw that using them to connect some random points on a screen made some cool-looking glyphs. A randomly generated language!

Chaikin curves (developed by George Chaikin in '74) create a smooth curve from a series of points by cutting the corners off of the curve.

Given a set of points, we take each line segment and:

- Split it into three segments: the first 25% of the curve, the middle 50%, and the last 25%.
- Discard the first and last quarters of the original segment
- Save the start and end points of the middle line segment as the new points in the curve

In doing so, we add more points each time we run the algorithm, with the line segments getting shorter, resulting in a smoother curve. We can run the algorithm recursively and get progressively shorter line segments and a smoother line.

And then if we're dealing with a closed shape instead of a series of line segments, we also need to make a line segment out of the first and last points.

That's pretty much it, so let's build it.

First, we're going to code the algorithm, and then we'll use it on a canvas to make some glyphs that are pretty and unintelligible (what I strive for in my life, too).

Let's start off with some tests so that we're clear on what we're building.

This is going to be a recursive algorithm. On the first run through with this algorithm, we're still going to have a pretty jagged curve. But once we run through it four or five times, the curve's going to be as smooth as butter.

Since it's recursive, we want to return the points of the curve once we have no more steps so that we don't get caught in an infinite loop.

// chaikin.js// javascript's standard assert library - no need to install anything for thisconst { deepEqual } = require("assert")// write a simple assert method with slightly better loggingconst assert = (actual, expected, message) => {try {deepEqual(actual, expected, message)} catch (err) {console.error("ERROR! ", message)console.log("Expected: ")console.dir(err.expected)console.log("Actual: ")console.dir(err.actual)}}const chaikin = () => {/* Coming soon! */return {}}// our test pointsconst points = [[100, 100],[400, 300],[700, 200],]assert(chaikin({ points, step: 0 }).points,points,"returns input points once base case is reached")

If we run this test (with `node chaikin.js`

), we get an error, since the `chaikin`

function doesn't currently do anything. So, let's get the test passing:

const { deepEqual } = require("assert")const assert = (actual, expected, message) => {try {deepEqual(actual, expected, message)} catch (err) {console.error("ERROR! ", message)console.log("Expected: ")console.dir(err.expected)console.log("Actual: ")console.dir(err.actual)}}// NEW!const chaikin = ({ points, level }) => {if (step <= 0) {// return this in an object, in case we need to return more properties laterreturn { points }}}const points = [[100, 100],[400, 300],[700, 200],]assert(chaikin({ points, step: 0 }).points,points,"returns input points once base case is reached")

Base case is covered! Now we can start finding the points. We'll start with open curves. The test case is going to involve us doing a little bit of math by hand with our test points to ensure we're looking for the correct numbers.

Given points of `[100,100], [400, 300], [700, 200]`

, what would we expect the output to be? The algorithm says that we want one point to be one quarter through the line segment, and the other point to be three quarters through. Let's calculate the end points of the middle 50% line segment.

We know the first point and second point (`[100,100]`

and `[400, 300]`

) `x`

values are `400 - 100 = 300`

pixels apart. 25% of that is `75`

, and 75% is `225`

, so we would expect `x1`

to be at `100 + 75 = 175`

and `x2`

to be at `100 + 225 = 325`

.

For the `y`

values, we've got a difference of `300 - 100 = 200`

. 25% and 75% are `50`

and `150`

, respectively. So, `y1`

and `y2`

are going to be `100 + 50`

and `100 + 150`

.

So, the two points that come from the first line segment are `[175, 150]`

and `[325, 250]`

.

And for the second line segment (`[400, 300]`

and `[700, 200]`

), the two points will be: `[475, 270]`

and `[625, 225]`

.

Let's add those for the test case, using a `step`

of `1`

so that we don't hit the base case immediately.

deepEqual(chaikin({ points, step: 1 }).points,[[175, 150],[325, 250],[475, 275],[625, 225],],"returns points 25% and 75% through each line segment")

Okay, we've got our test case. But as it stands, it's going to be hard to write an algorithm to calculate these points with the step-by-step math. Let's find a formula to make this all easier.

To get the first points values, the 25% one, we took the difference between the `x`

values and then added `25%`

of that difference to the first point's `x`

value.

And for the second point, the `75%`

one, we took the difference between the `x`

values and then added `75%`

of that difference to the first `x`

value. In a formula:

point1's x:xP1 = x1 + 0.25 * (x2 - x1)xP1 = x1 + (0.25 * x2) - (0.25 * x1)xP1 = (0.75 * x1) + (0.25 * x2)point2's x:xP2 = x1 + (0.75) * (x2 - x1)xP2 = x1 + (0.75 * x2) - (0.75 * x1)xP2 = (0.25 * x1) + (0.75 * x2)

Okay! Let's get these formulas into the `chaikin`

algorithm. For each line segment, we need to use these formulas on both the `x`

and `y`

values to create the two new points.

const chaikin = ({ points, step }) => {if (step <= 0) {return { points }}// NEW!const result = []for (let i = 0; i < points.length - 1; i++) {// current and next point in the curveconst pointA = points[i]const pointB = points[i + 1]const [xA, yA] = pointAconst [xB, yB] = pointB// 25%-in point's x and yconst x1 = 0.25 * xB + 0.75 * xAconst y1 = 0.25 * yB + 0.75 * yA// 75%-in point's x and yconst x2 = 0.75 * xB + 0.25 * xAconst y2 = 0.75 * yB + 0.25 * yAresult.push([x1, y1])result.push([x2, y2])}// call chaikin again with:// - one fewer step remaining// - our result points that we just calculated as the new inputreturn chaikin({ points: result, step: step - 1 })}

When we run the tests again, all should be passing. At this point, we could run the `chaikin`

function with greater numbers of levels, and we'd get a smoother and smoother curve, as we'll see displayed soon. But first, let's cover the case for closed polygons.

The difference here between the open curves is that we need to make one last extra line segment that connects the last point with the first point.

Using the same input points for our test case, we'd now expect to get 2 more points, from the `[700, 500]`

and `[100,100]`

line segment.

Let's calculate these two new points' values by hand first again:

const x1 = 0.75 * 700 + 0.25 * 100 // 550const y1 = 0.75 * 200 + 0.25 * 100 // 175const x2 = 0.25 * 700 + 0.75 * 100 // 250const y2 = 0.25 * 200 + 0.75 * 100 // 125

The last two expected points are `[550, 175]`

and `[250, 125]`

. We'll also need to told when we run the function whether we should calculate these last two points with a boolean flag. I'll name it `isClosed`

. The test case will look like this:

assert(chaikin({ points, step: 1, isClosed: true }).points, // new isClosed flag is true[[175, 150],[325, 250],[475, 350],[625, 450],[550, 175],[250, 125],],"returns points 25% and 75% through each line segment for a closed polygon")

Running this test fails because we're not doing anything with the `isClosed`

value in our `chaikin`

function yet. If the value is `true`

, we just want to run the `for`

loop one more time, meaning we'll run the function `points.length`

times (`3`

, in our test case) instead of `points.length - 1`

times. Let's toss that in:

// NEW! add the isClosed argument, defaulting to falseconst chaikin = ({ points, step, isClosed = false }) => {if (step <= 0) {return { points, isClosed }}const result = []// NEW! use the isClosed variable to determine the max for loop indexconst maxIndex = isClosed ? points.length : points.length - 1for (let i = 0; i < maxIndex; i++) {const pointA = points[i]const pointB = points[i + 1]const [xA, yA] = pointAconst [xB, yB] = pointB// 25% in point's x and yconst x1 = 0.25 * xB + 0.75 * xAconst y1 = 0.25 * yB + 0.75 * yA// 75% in point's x and yconst x2 = 0.75 * xB + 0.25 * xAconst y2 = 0.75 * yB + 0.25 * yAresult.push([x1, y1])result.push([x2, y2])}// NEW! pass the isClosed value to any future callsreturn chaikin({ points: result, step: step - 1, isClosed })}

Almost there, but we've still got a problem. If you can't spot it, you can see an error message when we try to run this as is. If the curve is closed, `i`

can get up to `2`

with our test input points of length `3`

. When we get to `i`

being `2`

, we can calculate `pointA`

as `points[2]`

. But when we try to get `pointB`

, we're trying to get `points[3]`

, which doesn't exist!

Instead, we want to get `points[0]`

, the first point, for `pointB`

. `0`

just so happens to be the remainder when we divide the index of `3`

by `points.length`

. We'll use that fact in order to prevent our current error with the *modulus*, or remainder, operator. Then, instead of finding `points[3]`

, we'll look for `points[(the remainder of i+1) / points.length] == points[0 / 3] == points[0]`

.

Let's make the switch:

const chaikin = ({ points, step, isClosed = false }) => {if (step <= 0) {return { points, isClosed }}const result = []const maxIndex = isClosed ? points.length : points.length - 1for (let i = 0; i < maxIndex; i++) {const pointA = points[i]// NEW: use the modulus operator to avoid an index that's too highconst pointB = points[(i + 1) % points.length]const [xA, yA] = pointAconst [xB, yB] = pointB// 25% in point's x and yconst x1 = 0.25 * xB + 0.75 * xAconst y1 = 0.25 * yB + 0.75 * yA// 75% in point's x and yconst x2 = 0.75 * xB + 0.25 * xAconst y2 = 0.75 * yB + 0.25 * yAresult.push([x1, y1])result.push([x2, y2])}return chaikin({ points: result, step: step - 1, isClosed })}

Our `chaikin`

function is complete! We've got some test cases to give us some confidence that the algorithm is working. Let's get it onto a canvas now to see it in action.

To start using this on a canvas, we'll need to make an `html`

page. We can take our `chaikin`

implementation and put it into a `script`

tag at the end of the body.

<!-- index.html --><body><canvas></canvas><script>const chaikin = ({ points, step, isClosed = false }) => {/* exact implementation we just wrote */}var canvas = document.querySelector("canvas")var context = canvas.getContext("2d")// set the size to smaller of window width or heightvar size = Math.min(window.innerWidth, window.innerHeight)canvas.width = size - 100canvas.height = size - 100// draw a gray rectangle covering the entire canvascontext.rect(0, 0, canvas.width, canvas.height)context.fillStyle = "rgba(0,0,0, 0.1)"context.fill()</script></body>

If you open up this `html`

file and see a gray box, all is working well. You can also throw all of this code into an HTML file on Codepen to see it all working:

First, let's just draw some hard-coded points. Let's move down the canvas and ping pong between 0 and the full width as the x coordinates. Then, we'll draw both that curve and the chaikin curve with 5 steps of recursion, which should lead to a pretty smooth curve.

<body><canvas></canvas><script>const chaikin = ({ points, step, isClosed = false }) => {/* ... */}var canvas = document.querySelector("canvas")var context = canvas.getContext("2d")var size = Math.min(window.innerWidth, window.innerHeight)canvas.width = size - 100canvas.height = size - 100context.rect(0, 0, canvas.width, canvas.height)context.fillStyle = "rgba(0,0,0, 0.1)"context.fill()// NEW!// create the pointsconst points = [[0, 0],[canvas.width, canvas.height * 0.25],[0, canvas.height * 0.5],[canvas.width, canvas.height * 0.75],[0, canvas.height],]// draw the original points as a thin black linecontext.beginPath()context.lineWidth = 1context.moveTo(points[0][0], points[0][1])points.forEach(([x, y]) => {context.lineTo(x, y)})context.stroke()// draw the chaikin-ed pointsconst { points: chaikinPoints } = chaikin({ points, step: 5 })context.lineWidth = 5context.strokeStyle = "red"context.beginPath()context.moveTo(chaikinPoints[0][0], chaikinPoints[0][1])chaikinPoints.forEach(([x, y]) => {context.lineTo(x, y)})context.stroke()</script></body>

A chaikin curve on a canvas! We did it! If we want to make some random glyphs, all we'll need to do is use random points instead of the hard-coded ping-pong points I made.

To make this easier on ourselves, let's make a helper function to create a point. Given a starting `x`

and `y`

, as well as a `width`

and `height`

of the box that the point needs to be in, we'll return an `[x,y]`

point with a random coordinates inside of that box.

Then we'll create a set of 10 or so random points and draw a curve of those ten points at 4 steps.

<body><canvas></canvas><script>const chaikin = ({ points, step, isClosed = false }) => {/* ... */}var canvas = document.querySelector("canvas")var context = canvas.getContext("2d")var size = Math.min(window.innerWidth, window.innerHeight)canvas.width = size - 100canvas.height = size - 100context.lineWidth = 1context.rect(0, 0, canvas.width, canvas.height)context.fillStyle = "rgba(0,0,0, 0.1)"context.fill()// NEW!const createRandomPoint = ({ x, y, width, height }) => {const randomX = x + Math.random() * widthconst randomY = y + Math.random() * heightreturn [Math.floor(randomX), Math.floor(randomY)]}const points = []const pointsCount = 10for (let i = 0; i < pointsCount; i++) {points.push(createRandomPoint({x: 0,y: 0,width: canvas.width,height: canvas.height,}))}// draw the chaikin-ed pointsconst { points: chaikinPoints } = chaikin({ points, step: 5 })context.lineWidth = 5context.strokeStyle = "black"context.beginPath()context.moveTo(chaikinPoints[0][0], chaikinPoints[0][1])chaikinPoints.forEach(([x, y]) => {context.lineTo(x, y)})context.stroke()</script></body>

We're pretty much where we need to be already. Refreshing the page will keep creating new random glyphs. There are two more steps I want to tackle to make the glyphs more centered and more random.

First, let's work on centering the glyph. To do this, my plan is to find the bounding box of the chaikin points to get the minimum and maximum `x`

and `y`

values in the curve. Then, we can find the center of the outer box we put the chaikin curve's random points in as well as the center of the chaikin bounding box. With both centers, we can calculate how far off the center of the smaller chaikin box is, and then shift every point in the chaikin curve that many pixels so that the centers for both boxes are the same.

For that, we'll first need to create a `getBounds`

function that will return the minimum and maximum `x`

and `y`

values for a set of points.

const getBounds = points => {let minX, minY, maxX, maxYif (!Array.isArray(points) || !points.length) {throw "Expected an array with more than zero elements"}points.forEach(([x, y]) => {// set new minimum if value:// 1. doesn't exist (but isn't 0)// 2. is less than the current minimumif ((!minX && minX !== 0) || x <= minX) {minX = x}// set new maximum if value:// 1. doesn't exist (but isn't 0)// 2. is more than the current maximumif ((!maxX && maxX !== 0) || x >= maxX) {maxX = x}if ((!minY && minY !== 0) || y <= minY) {minY = y}if ((!maxY && maxY !== 0) || y >= maxY) {maxY = y}})return {min: [minX, minY],max: [maxX, maxY],}}

Okay, now let's figure out the center of both boxes and shift the chaikin box to line up with the center of the outer box.

<script>const getBounds = points => {/* ... */}const { points: chaikinPoints } = chaikin({ points, step: 5 })// use the getBounds function to determine the height, width, and center// of the chaikin curveconst { min: chaikinMin, max: chaikinMax } = getBounds(chaikinPoints)const [chaikinMinX, chaikinMinY] = chaikinMinconst [chaikinMaxX, chaikinMaxY] = chaikinMaxconst chaikinWidth = chaikinMaxX - chaikinMinXconst chaikinHeight = chaikinMaxY - chaikinMinYconst chaikinCenterX = chaikinMinX + chaikinWidth / 2const chaikinCenterY = chaikinMinY + chaikinHeight / 2// these values need to correspond to the x, y, width and height// values we used to create the random points.const outerBoxCenterX = 0 + canvas.width / 2const outerBoxCenterY = 0 + canvas.height / 2// find the difference between the outerBoxCenter and chaikinCenterconst xShiftForChaikin = outerBoxCenterX - chaikinCenterXconst yShiftForChaikin = outerBoxCenterY - chaikinCenterY// shift the chaikin curve points by the required shift to center themconst shiftedChaikinPoints = chaikinPoints.map(([x, y]) => {return [x + xShiftForChaikin, y + yShiftForChaikin]})// draw the new, shifted chaikin curvecontext.beginPath()context.moveTo(shiftedChaikinPoints[0][0], shiftedChaikinPoints[0][1])shiftedChaikinPoints.forEach(([x, y]) => {context.lineTo(x, y)})context.stroke()</script>

Whew! Okay, now when you refresh the page, you should be getting a chaikin curve glyph that looks centered in the canvas.

Last thing we'll do today is randomize the number of points in the curve, as well as whether the curve is closed. These are much easier than the centering. All we'll need to do is change the `pointsCount`

variable from a hard-coded `10`

to use two new `pointsMinimum`

and `pointsVariance`

variables.

const pointsMinimum = 5const pointsVariance = 5const pointsCount = pointsMinimum + Math.floor(Math.random() * pointsVariance)

You can play around with the minimum and variance to get the shapes that you want.

And then for randomly choosing whether the curve is closed, update the `chaikin`

call to have an `isClosed`

parameter of `Math.random() > 0.5`

. You'll also want to now return the `isClosed`

parameter where we call `chaikin`

so that we can draw the curve with the final line segment connected.

const { points: chaikinPoints, isClosed: isChaikinClosed } = chaikin({points,step: 5,isClosed: Math.random() > 0.5,})

And then we'll update the drawing portion of the code to check whether the curve is closed:

/* ... */context.beginPath()context.moveTo(chaikinPoints[0][0], chaikinPoints[0][1])chaikinPoints.forEach(([x, y]) => {context.lineTo(x, y)})// NEW!if (isChaikinClosed) {context.lineTo(chaikinPoints[0][0], chaikinPoints[0][1])}context.stroke()

Check out the Codepen below for the code we wrote.

From here, you'd be ready to explore what else chaikin curves could do. Some options:

- arrange a number of glyphs in a nice grid
- randomly place 100 chaikin curves on the screen
- lower the opacity of the curve and draw hundreds on the screen
- create a curve from a starting curve of 100 random points

If you make something cool, send me a screenshot!

Until next time ✌️

©2020 Matthew Knudsen