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
