In a previous post, I presented some code which would create a smoothly dithered linear gradient in HTML5 canvas. These dithered gradients look very smooth to the eye and do not suffer from the “banding” or “stepped” effect caused by integer rounding (which is especially apparent when using subtly changing dark gradients spanning a wide area). With a little bit of algebra I’ve worked out how to compute dithered radial gradients as well.
But first, an important note: you have to really want this smooth gradient for your application. The gradient is computed pixelbypixel, then dithering requires another run through the data, involving repeated array access and several multiplications per pixel in order to arrive at the final picture. In my testing, it all happens quite fast, but it is surely a heavier computation than a standard gradient. So practical uses may be limited. But for me it was a interesting coding exercise and a solution to a specific problem, and I’ll post it here in case anyone has a use for it! What motivated me to compute dithered gradients was my recent uses of JavaScript to create generative art, where I’m willing to wait a few more milliseconds for a smooth gradient background.
Click here or on the screenshot below to see a much larger dithered radial gradient generated in real time, next to a standard canvas gradient for the sake of comparison. The banding effect in the standard gradient is more apparent on certain monitors and depends on brightness settings. Look below for the source code and my comments on the code.
Download
Download the full commented source code here: DitheredRadialGradient.zip
About the code
Similar to my dithered linear gradients, this gradient is created by first computing a floatbased gradient, and then using these float vales to calculate a gradient with integer color values. But while the integers are being calculated, FloydSteinberg dithering is applied to diffuse the error and make for a smooth appearance.
But how do we calculate a radial gradient? I won’t derive the entire expression here, but let me describe the algebra for you.
First we must understand how a radial gradient is defined. The standard definition requires two circles: one with center coordinates \((x_0, y_0)\) and radius \(r_0\), where the color will be some starting RGB values, and another circle with center \((x_1, y_1)\) and radius \(r_1\), where the color will have ending RGB values. The gradient is formed by linearly interpolating the center, radius, and colors from their beginning to ending values, and drawing circles accordingly.
So if we have a specific pixel located at the point \((x,y)\), what color should it be?
Consider the following figure, where we consider a point \((x,y)\) in our canvas, and try to determine what circle it lies on:
The point \(P\) is between the starting and ending center points, and in general its coordinates will take the form
\[
(x_0 + t(x_1x_0), y_0 + t(y_1y_0)),
\]
where \(t\) is the interpolating parameter whose value lies between 0 and 1. The point \((x,y)\) will be on the circle defined by the interpolation value \(t\) when the distance \(d\) shown above is the matching interpolated radius value, that is, when
\[
d = r_0 + t(r_1r_0).
\]
So this gives us an equation to set up to find what \(t\) value should be used to calculate the proper color value at this point. We use the distance formula and set the interpolated radius value to equal the distance between \((x,y)\) and \(P\) and obtain:
\[
r_0 + t(r_1r_0) = \sqrt{(x_0 + t(x_1x_0) – x)^2 + (y_0 + t(y_1y_0) – y)^2}.
\]
Squaring both sides and multiplying out some expressions, we will obtain a quadratic equation in \(t\), which we can solve with the quadratic formula. In general, such an equation will have zero, one, or two solutions which depends on the value of the discriminant in the quadratic formula. If you draw yourself some pictures you can convince yourself that for some gradients, a point \((x,y)\) will lie on two associated circles, but we will just select the later circle (with the greater \(t\) value) to determine color. And if there are no solutions, we will just select the beginning color. If the \(t\) value works out to be larger than one, we will just use the ending color values.
Once we’ve determined the proper \(t\) value, it is an easy matter to calculate color: we simply use this interpolation parameter to interpolate red, green, and blue values between the starting and ending values.
I’ll leave you to pore over the code and work out the algebra for yourself if you are interested in more details!
Again, I can’t promise you that this will be useful for ordinary web development tasks, but use the code if you absolutely must have an ultrasmooth radial gradient.
Acknowledgment
The mathematics was typset in this blog post using the open source JavaScript display engine MathJax.
you can optimize this section
//While converting floats to integer valued color values, apply FloydSteinberg dither.
var i0, i1, i2, i3, c0 = 7 / 16, c1 = 3 / 16, c2 = 5 / 16, c3 = 1 / 16;
for (i = 0; i < len / 4; i++) {
i0 = i + 1;
i1 = i – 1 + rectW;
i2 = i + rectW;
i3 = i + 1 + rectW;
nearestValue = ~~(rBuffer[i]);
quantError = rBuffer[i] – nearestValue;
rBuffer[i0] += c0 * quantError;
rBuffer[i1] += c1 * quantError;
rBuffer[i2] += c2 * quantError;
rBuffer[i3] += c3 * quantError;
nearestValue = ~~(gBuffer[i]);
quantError = gBuffer[i] – nearestValue;
gBuffer[i0] += c0 * quantError;
gBuffer[i1] += c1 * quantError;
gBuffer[i2] += c2 * quantError;
gBuffer[i3] += c3 * quantError;
nearestValue = ~~(bBuffer[i]);
quantError = bBuffer[i] – nearestValue;
bBuffer[i0] += c0 * quantError;
bBuffer[i1] += c1 * quantError;
bBuffer[i2] += c2 * quantError;
bBuffer[i3] += c3 * quantError;
}
March 14, 2013 @ 7:48 pm
Aah! You’re right! I should have done that. I’ll update when I have the chance. Thanks!
March 15, 2013 @ 1:16 am
And possibly:
i0 = i + 1;
i2 = i+rectW;
i1 = i2 – 1
i3 = i2 + 1
And pull out (len / 4) although the browser *might* optimise that!
ðŸ˜‰
But a really smooth looking gradient anyway!
March 15, 2013 @ 2:32 pm
Hi Dan,
great idea, thanks for sharing this! I am trying to I have had a lot of fun taking the code for a spin and modifying it until almost unrecognizable (cleanly separated drawing a float image (e.g. gradient) from rendering (dithering) such a float image to the canvas, experimented with other error diffusion filter grids (e.g. Sierra), optimizations (minor speedup), dedicated drawing functions for less general gradients with no intermediate color stops (speedup by factor of 3), concentric start and end circle, and a start point instead of a start circle. I might share this (for free), but am a little hesitant because of the licensing. I have seen your comment on http://rectangleworld.com/blog/about . The way it is, people can *use* your work (because you have published it), but only in private. You have not explicitly given anyone the right to *distribute* your work, for example as part of a web site. Any distribution would be a violation of copyright.
On the one hand, you “hope that people … give me some credit and perhaps a link to my blog” – this sounds like you want to allow distribution, but desire attribution. Why not be clear about this, and explicitly allow distribution as long as attribution is given, such as the CCBY licenses do?
https://en.wikipedia.org/wiki/Creative_Commons_license#Seven_regularly_used_licenses
You may even want to require that your work remains free even if modified, for example as required by CCBYSA.
https://en.wikipedia.org/wiki/Definition_of_Free_Cultural_Works#.22Free_cultural_works.22_approved_licenses
On the other hand you “prefer … people contact me about using specific pieces”. It is not quite clear if this refers to all your work or only some specific works of yours. You are of course free to put different licenses on different works of yours. You can also allow noncommercial usage for free, and require commercial users to obtain a separate license, as requird by CCBYNCSA.
April 1, 2016 @ 11:02 am
Thank you for your very thoughtful and thorough comment! Yes, I certainly need to clarify permissions better. I will need to put different licenses on different pieces. I have seen people taking my more artistic work and passing it off as their own, and this usually bothers me. But code like this radial gradient I intend for people to use as they wish. So please modify and distribute as you like, acknowledgment not necessary. But I’m always interested in seeing what people do so keep in touch!
I’m afraid I don’t have time at the moment to clarify licenses across the site, but I will update when I have a chance. Thanks for helping me think through this.
April 5, 2016 @ 1:38 pm
Please put a link here if you do release your work, Id really like to see it.
Thanks.
April 26, 2016 @ 2:17 pm
