DEV Community is a community of 702,959 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

How to Draw Squircles and Superellipses

ndesmic
I like to make fun web things from scratch. Ideally build-less, framework-less, infrastructure-less and free from the annoyances of my day job.

Squircles are a shape that has become quite popular recently, particularly as a shape around icons. These are quite interesting from a math perspective and you likely won't find out-of-the-box ways to draw them and related shapes so I'd like to try and document how you might go about it.

What's a Squircle?

A "squircle" is a portmanteau of square and circle and visually takes a shape somewhere between the both of then. Note that this is different than a square with rounded corners:

A square with corner radius equal to half it's length is a close approximation but not quite a squircle. A squircle has no actual straight lines in it but rather a subtle curve.

To really understand a squircle we need to look at some more esoteric math. It's interesting, I promise!

First let's try to define was a circle is in the first place. I claim that a circle is a shape in which all points are equidistant from a center point. So if that's a circle then let's take a look at our typical definition for a circle:

``````x**2 + y**2 = r ** 2
``````

Let's graph that in case you forgot what a circle looks like:

What is the distance to any point? It's:

``````const r = Math.sqrt(x**2 + y**2);
``````

Which we get from Pythagoras's theorem. But that was a somewhat arbitrary way to decide what distance is. Another type of distance is "taxi" distance. You can think about it as the number of blocks you drive to a destination in a city full of perfect square blocks. It actually doesn't matter which route you take, it's always the same.

``````const d = Math.abs(x) + Math.abs(y);
``````

What does it look like to graph this?

So according to our definition this too is a circle of sorts we've just changed the definition of what distance is.

As it turns out we can generalize definitions of distance called the Lp-norm. If you look closely the two distances share a similar shape:

``````const d = (Math.abs(x)**p + Math.abs(y)**p)**(1/p)
``````

And yes this can go to 3+ dimensions too by adding terms under the root. The taxi distance is p = 1, and our standard Euclidean distance is p = 2. We can graph some others like p = 0.5:

Or maybe p = Infinity:

Interesting. These shapes are more generally called "superellipses" and take the form:

``````//Superellipse
Math.abs((x - cx) / rx) ** p + Math.abs((y - cy) / ry) ** p = 1
``````

What you might call different P values:

• P < 1 - Astroid (Not to be confused with "Asteroid")
• P = 1 - Diamond (Or a rotated square)
• P = 2 - Circle
• P = 4 - Squircle
• P = Infinity - Square

P = 4 is the true definition of a Squircle.

A first stab in polar coordinates

My first shot was drawing in polar coordinates as I figured since it's a circle that this would be the easiest way to represent it. `theta` will vary from 0 to 2π and using it we'll find `r`. If we take the superellipse equation and plug in our polar transforms `x = r*Math.cos(theta)` and `y = r*Math.sin(theta)` we can solve for `r`:

``````const currentR = r ** 2 / (Math.abs(r * Math.cos(theta)) ** p + Math.abs(r * Math.sin(theta)) ** p ) ** (1/p);
``````

I'm using `currentR` (the polar value of r at a given theta) to differentiate it from `r` which is the radius of the superellipse.

Then we just iterate over theta in little bits (0.01) and draw the result by converting back into cartesian coordinates:

``````//this.dom.canvas is a reference to a canvas element
//this.#r and this.#p are properties corresponding to p and r
//this.#cx and this.#cy are the center of the superellipse
const context = this.dom.canvas.getContext("2d");
context.clearRect(0, 0, this.dom.canvas.width, this.dom.canvas.height);
for(let t = 0; t < Math.PI * 2; t += 0.01){ //play with the step size
const r = this.#r ** 2 / (Math.abs(this.#r * Math.cos(t)) ** this.#p + Math.abs(this.#r * Math.sin(t)) ** this.#p ) ** (1/this.#p);
const [x,y] = polarToCartesian(r, t, this.#cx, this.#cy);
context.fillRect(x,y,1,1);
}
``````

This works pretty nicely:

However when we try with P < 1 it doesn't work as well:

This is because the steps we are taking are too large around the corners, the 'r's change too quickly. We can remedy this by making the the steps smaller, for instance if we use a step value of `0.001`:

It's mostly better but we can still see some precision loss at the very tips. We can further decrease the step size but we're going to be doing a lot of extra work (100x the work of the original) just to get those extra little bits looking nice.

Improving efficiency

So mulling some ideas on how to improve efficiency, the most obvious way is to use cartesian coordinates and only try to calculate a color for each real pixel. Since we know exactly how wide the drawing is, we can simply create one pixel for each `y` value as we traverse x one-by-one. I'm going to drop back down into `imageData` to do it:

``````for(let i = this.#cx - this.#r; i < this.#cx + this.#r; i++){
const x = i - this.#cx;
const y = Math.floor(this.#r * ((1 - Math.abs(x/this.#r) ** this.#p) ** (1/this.#p)))
writePixel(imageData, i, this.#cy - y, this.#color);
writePixel(imageData, i, this.#cy + y, this.#color);
}
context.putImageData(imageData, 0, 0);
``````

`writePixel` is a utility function so we don't have to write out the whole indexing part:

``````export function writePixel(imageData, x, y, color){
const index = (imageData.width * 4 * y) + (x * 4);
imageData.data[index] = color[0] * 255;
imageData.data[index + 1] = color[1] * 255;
imageData.data[index + 2] = color[2] * 255;
imageData.data[index + 3] = color[3] * 255;
}
``````

We iterate across X (accounting for centering) and then we use a cartesian form of the superellipse equation:

``````const y = Math.floor(this.#r * ((1 - Math.abs(x/this.#r) ** this.#p) ** (1/this.#p)))
``````

The `Math.floor` is to ensure we are using integers otherwise we couldn't index into the `imageData` array. Also, we write 2 pixels per `x`, as to undo the absolute value we need both `y` and `-y` and that forms the bottom part of the ellipse.

We run into the same issue though where the changes are too big in the middle. However, we can just iterate through the same way in `y`:

``````for (let i = this.#cx - this.#r; i < this.#cy + this.#r; i++) {
const y = i - this.#cx;
const x = Math.floor(this.#r * ((1 - Math.abs(y / this.#r) ** this.#p) ** (1 / this.#p)))
writePixel(imageData, this.#cx + x, i, this.#color);
writePixel(imageData, this.#cx - x, i, this.#color);
}
``````

If we do that then we should naturally cover the areas where y changes quickly at the cost of redrawing a few points. But it should still be better than the tiny step method.

It has some jaggedness to it, but that's not too bad as we have a full, intact shape.

You can also increase the sizes of the pixels drawn:

``````export function writePixelRect(imageData, x, y, color, width = 1, height = 1){
const offsetX = Math.floor(width / 2);
const offsetY = Math.floor(height / 2);
for(let i = 0; i < width; i++){
for(let j = 0; j < height; j++){
writePixel(imageData, x + i - offsetX, y + j - offsetY, color);
}
}
}
``````

And just call that instead of `writePixel` with the width set to something higher for example 2x2:

In the end it's a trade-off as if we do pixel-by-pixel then we lose the natural anti-aliasing that the canvas gives us.

Rectllipse? Superoval?

The previous equations were simplified to be circles but if you follow the original equation we can change the radii for x and y:

``````//polar
const r = (this.#rx * this.#ry) / (Math.abs(this.#ry * Math.cos(t)) ** this.#p + Math.abs(this.#rx * Math.sin(t)) ** this.#p ) ** (1/this.#p);
//cartesian
const y = Math.floor(this.#ry * ((1 - Math.abs(x / this.#rx) ** this.#p) ** (1/this.#p)));
const x = Math.floor(this.#rx * ((1 - Math.abs(y / this.#ry) ** this.#p) ** (1 / this.#p)));
``````

I'm not sure if this has a name but it's a cross between an oval and a rectangle:

If you use a 3:4 ratio of height to width it's pretty much the shape of an old television.

Rotation

Rotation is pretty easy in polar coordinates, we just add to theta a fixed amount:

``````//this.#angle should be in radians
const angle = normalizeAngle(t + this.#angle);
const r = (this.#rx * this.#ry) / (Math.abs(this.#ry * Math.cos(angle)) ** this.#p + Math.abs(this.#rx * Math.sin(angle)) ** this.#p ) ** (1/this.#p);
``````

It's harder in Cartesian coordinates. We're going to use our trusty rotation function:

``````//rotates around 0,0
function rotate(position, angle){
return [
Math.cos(angle) * position[0] - Math.sin(angle) * position[1],
Math.sin(angle) * position[0] + Math.cos(angle) * position[1]
];
}
``````

Then we're going to use it after getting the `y` value:

``````const y = this.#ry * ((1 - Math.abs(x / this.#rx) ** this.#p) ** (1/this.#p));
const [tx,ty] = rotate([x,y], this.#angle, this.#cx, this.#cy).map(x => Math.floor(x));
const [tx2, ty2] = rotate([x, -y], this.#angle, this.#cx, this.#cy).map(x => Math.floor(x));
``````

Since we're rotating we can no longer just flip the coordinate to get `-y` we need to rotate that as well. We also don't need to take the floor until after rotation but we need to do it on both points to avoid decimal indexes.

``````writePixelRect(imageData, tx - this.#cx, this.#cy - ty, this.#color, this.#thickness, this.#thickness);
writePixelRect(imageData, tx2 - this.#cx, this.#cy - ty2, this.#color, this.#thickness, this.#thickness);
``````

We also have to add some offsets from the center as the rotation was around point 0,0. It will rotate counterclock-wise, like school math. If you want the rotation to go clockwise then subtract instead of add `this.#angle`.

Since I've been doing a lot of tutorials on gradients I thought I'd add this enhancement to my radial gradients. Please check post: https://dev.to/ndesmic/circular-gradients-from-scratch-4hgg. It's not hard at all to add, we just change the exponents on the `getRValue` function with a new `p` property (I've also changed "clipTheta" to "clipAngle" if you were following along exactly):

``````getRValue(r, theta){
const transformedTheta = normalizeAngle(theta - this.#clipAngle);
return r / ((this.#rx * this.#ry) / (Math.abs(this.#ry * Math.cos(transformedTheta)) ** this.#p + Math.abs(this.#rx * Math.sin(transformedTheta)) ** this.#p)
** (1 / this.#p));
}
``````

I also updated the `r` properties to remove `r` since we're now in terms of `rx` and `ry`:

``````set r(val) {
const r = parseFloat(val);
this.#rx = r;
this.#ry = r;
}
set rx(val){
this.#rx = parseFloat(val);
}
set ry(val){
this.#ry = parseFloat(val);
}
``````

And they look like this (P=0.5, P=1, P=2, P=4, P=Infinity; clamped clipped).