How to implement HTML5 Canvas Radial Gradient.

Most 2D graphics libraries have their own radial gradient and most radial gradients are similar to the radial gradient of Open VG[4]. Open VG as has two properties (the focal point and the circle). However, the HTML5 canvas radial gradient is different. It has the start circle and the end circle. Quartz 2D[2] has the same HTML5 canvas radial gradient because HTML5 canvas specification[1] had been suggested by Apple[3]. Some browsers do not comply with the specification[1], so they render differently. I think 2D vector graphics libraries of some browsers do not have such a thing like Quartz 2D radial gradient and they did not fully understand the specification because it is not a common radial gradient.
Our team had a very close look to the specification and deduced it. Here in this post, I will explain you the geometrical meaning of the specification and the way to render it.

1. Radial Gradient Conflict

Below picture compares the canvas elements rendered by different browsers. CanvasGL Pad, Firefox, Safari, and Opera draw radial gradients correctly while IE9, Chrome and QT do not. ‘CanvasGL Pad’ is the name of our implementation.

Pic 1. HTML5 Radial Gradients that each browser draws.

The stops[6] of the radial gradient consist of rainbow colors, from red (start circle) to violet (end circle). In Pic1, ‘S’ represents the start circle, and ‘E’ represents the end circle. Click the above link and see how your browser draws HTML5 canvas radial gradient.

CanvasGL Pad, Safari, FireFox, and Opera show radial gradients that comply with the HTML5 canvas radial gradient specification. Most of the pictures drawn by Chrome and QT are not complying the specification. Unfortunately, IE9 draws the pictures correctly except for #7; this is the picture that does not comply with the specification.

2. Radial Gradient Formula

The following paragraph is quoted from HTML5 canvas radial gradient specification.

Radial gradients must be rendered by following these steps:

  1. If x0 = x1 and y0 = y1 and r0 = r1, then the radial gradient must paint nothing. Abort these steps.
  1. Let x(ω) = (x1-x0)ω + x0
    Let y(ω) = (y1-y0)ω + y0
    Let r(ω) = (r1-r0)ω + r0Let the color at ω be the color at that position on the gradient (with the colors coming from the interpolation and extrapolation described above).
  1. For all values of ω where r(ω) > 0, starting with the value of ω nearest to positive infinite and ending with the value of ω nearest to negative infinite, draw a circumference of the circle with radius r(ω) at position (x(ω), y(ω)), with the color at ω, but only painting on the parts of the canvas that have not yet been painted on by earlier circles in this step for this rendering of the gradient.

This effectively creates a cone, touched by the two circles defined in the creation of the gradient, with the part of the cone before the start circle (0.0) using the color of the first offset, the part of the cone after the end circle (1.0) using the color of the last offset, and areas outside the cone untouched by the gradient (transparent black).

At first, most developers may think of the following approach.
1. Find maximum ω with r(ω) > 0.
2. Draw a hairline circle with color consistent with ω.
3. Draw a hairline circle  decreasing ω by small steps to negative infinite. If a pixel was rendered already, skip it.

However, it is difficult to measure such a small step (how long is it?) and the range is between minimum ω and maximum ω.

I will represent the way to determine a color at every pixel. This approach has a more clear range (pixels in the viewport) and better performance than the above simple approach.

Let’s derive the radial gradient formula that indicates what color should be rendered in a given pixel.

x0, y0, r0 represent the center point and the radius of the start circle as well as x1, y1, r1.

Step 2 of the specification says that ω decides the color of the radial gradient.
x(ω) = (x1-x0)ω + x0   (1)
y(ω) = (y1-y0)ω + y0   (2)
r(ω) = (r1-r0)ω + r0   (3)

ω decides the color in a given p(px, py) that results from the following equation (that is, p should be compliant with the following equation).

(x(ω) – px)^2 + (y(ω) – py)^2 = r(ω)^2   (4)

Let’s substitute (1), (2), and (3) into (4).
((x1-x0)ω + x0 – px)^2 + ((y1-y0)ω + y0 – py)^2 = ((r1-r0)ω + r0)^2   (5)

Let’s define the following formula in order to make (5) simple.
Let’s define  px’ = px – x0   (6)
Let’s define  py’ = py – y0   (7)
Let’s define  dx = x1 – x0   (8)
Let’s define  dy = y1 – y0   (9)
Let’s define  dr = r1 – r0   (10)

Then, (5) can be (11) by substituting (6), (7), (8), (9), and (10).
(dx^2 + dy^2 – dr^2)ω^2 – 2(px’dx + py’dy + r0dr)ω + px’^2 + py’^2 – r0^2 = 0   (11)

(11) indicates that we can decide ω using only 3 geometrical values.
1. px’, py’ : Position p based on the start circle.
2. dx, dy, dr : The difference of two circles.
3. r0 : Radius of the start circle.

Let’s solve the equation (11). Discriminant  D is (px’dx + py’dy + r0dr)^2 – (dx^2 + dy^2 – dr^2)(px’^2 + py’^2 – r0^2). This equation is valid only if D >= 0. There are two roots: one positive and the other negative.
ω = (px’dx + py’dy + r0dr +- sqrt(D)) / (dx^2 + dy^2 – dr^2)   (11)

Which is valid, the positive root or the negative root? Step 3 of the above specification suggests the answer is the positive root. Now, we will discuss about the geometrical meaning of the positive root and the negative root.

3. Geometric Analysis

The equation (11) gives us 2 solutions: one is a positive root and the other one is a negative root. I will explain what they mean using a conical surface.

I represent Pic2 because we can use ‘CanvasGL 3D’ in Pic2 as a geometrical standard. ‘CanvasGL 3D’ in Pic2 shows how to render a conical surface in a 3D space.
Pic2. CanvasGL 3D is easy to understand a 3D geometry of the radial gradient.

In Pic3, I mark ‘+’ at the pixel that is determined by a positive root of the equation (11), and ‘-’ otherwise.


Pic3. two roots of equation(11) on the radial gradient.

You can fill the surfaces between the two outer circles (or draw the radial gradient) with the radial gradient as below.

  1. Imagine a 3D conical surface clipped by the two planes of the two circles.
  2. Fill the conical surface with colors defined by ω. The range of ω is [0, 1].
  3. Project the conical surface on the canvas in a way the end circle is located nearer than the start circle.

The last sentence described above is the geometrical analysis of the step 3 of the specification: “starting with the value of ω nearest to positive infinity and ending with the value of ω nearest to negative infinity”.

Pic 3 is rendered using both positive and negative roots. When we project the conical surface in a way that we see the end circle nearer than the start circle, the positive root always renders the outer surface and the negative root renders the inner surface. I will call +surface the surface rendered by the positive root and -surface the surface rendered by the negative root.
In all the pixels we should choose either a positive root or a negative root in order to decide the color. In most of the pixels we should choose positive roots because +surface covers over most of -surface.

Let’s dive into a more detail explanation of why +surface represents the outer surface when projecting the conical surface.
Pic4. How two roots(ω1 and ω2) relate with the point p.

In Pic4, ω1 and ω2 are the two roots of the point p derived from equation (11). The blue axis represents ω. ω2 is nearer to ωE or 1 (end circle) than ω1. ω2 is positive root and decides the color of the outer circular curve which you can see on the upper left part in Pic4.

I drew a clipped conical surface to better represent a 3D image (see the left image of Pic 5). However, step 3 of the HTML5 Canvas radial gradient specification tells to render an infinite conical surface as shown in the right image of Pic 5. You can easily understand the specification by extending a radial gradient in Quartz 2D.[3]
Pic5. Figure 8-8 Extending a radial gradient in Quartz 2D gradient page.

We can extend the radial gradient extrapolating ω infinitely from two circles and thus creating a conical surface. Then we fill in the two circles respectively. This is only valid when r(ω) >= 0.

In case #1 of Pic2, most pixels are determined by negative root. We see the inner surface of conical surface. As I mentioned, the inner surface is rendered by negative root. If we render using positive root, all pixels will be red because they are decided by ω that is r(ω) < 0. Same phenomenon occurs in case #3.

Case #3 is peculiar. The picture rendered by ‘CanvasGL 3D’ is very different from the one rendered by ‘Safari’. It is because ‘CanvasGL 3D’ does not have an outer positive surface that covers the inner negative surface as ‘Safari’ does. ‘CanvasGL 3D’ has chosen negative roots to draw the picture in areas that positive root could not draw.

Case #7 is when (dx^2 + dy^2 – dr^2) = 0 in (11). Chrome may not handle the case where the coefficient of ω^2 is zero.

4. Conclusion

We dived into how to analyze the HTML5 radial gradient specification geometrically and how to render it.
I hope this article helped all browsers comply with the HTML5 Canvas Radial Gradient Specification.

Appendix A. GLSL Implementation

I will present only GLSL fragment shader code. I ignored the vertex shader because it only sends point p to the fragement shader as varying. You can implement the radial gradient using Direct3D or software rasterizer with the formula I derived.

First, below is the ‘CanvasGL Pad’ code in Pic2.

varying vec2 p;
uniform vec3 x0y0r0;
uniform vec3 dxdydr;
uniform sampler2D uGradSampler;
void main() {
    vec2 brushTexCoord;
    vec2 p2 = p - x0y0r0.xy;
    float A = dxdydr.x * dxdydr.x + dxdydr.y * dxdydr.y - dxdydr.z * dxdydr.z;
    float B = dot(p2.xy, dxdydr.xy) + x0y0r0.z * dxdydr.z;
    float C = dot(p2, p2) - (x0y0r0.z * x0y0r0.z);
    float ω;
    if (abs(A) > 0.0000001) {
        float D = (B * B) - (A * C);
        if (D < 0.0) {
            gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
            return;
        }
        ω = (B + sqrt(D)) / A;
        float r = (dxdrdr.z * ω) + x0y0r0.z;
        if (r < 0.0) {
            ω = (B - sqrt(D)) / A;
            r = (dxdrdr.z * ω) + x0y0r0.z;
            if (r < 0.0) {
                gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
                return;
            }
        }
    } else { // A == 0
        ω = 0.5 * C / B;
        float r = (dxdrdr.z * ω) + x0y0r0.z;
        if (r < 0.0) {
            gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
            return;
        }
    }
    brushTexCoord = vec2(ω, 0.5);
    gl_FragColor = texture2D(uGradSampler, brushTexCoord);
}

Secondly, this is the ‘CanvasGL 3D’ code in Pic2.

varying vec2 p;
uniform vec3 x0y0r0;
uniform vec3 dxdydr;
uniform sampler2D uGradSampler;
void main() {
    vec2 brushTexCoord;
    vec2 p2 = p - x0y0r0.xy;
    float A = dxdydr.x * dxdydr.x + dxdydr.y * dxdydr.y - dxdydr.z * dxdydr.z;
    float B = dot(p2.xy, dxdydr.xy) + x0y0r0.z * dxdydr.z;
    float C = dot(p2, p2) - (x0y0r0.z * x0y0r0.z);
    float ω;
    if (abs(A) > 0.0000001) {
        float D = (B * B) - (A * C);
        if (D < 0.0) {
            gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
            return;
        }
        ω = (B + sqrt(D)) / A;
        if (ω < 0.0 || ω > 1.0) {
            ω = (B - sqrt(D)) / A;
            if (ω < 0.0 || ω > 1.0) {
                gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
                return;
            }
        }
    } else { // A == 0
        ω = 0.5 * C / B;
        if (ω < 0.0 || ω > 1.0) {
            gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
            return;
        }
    }
    brushTexCoord = vec2(ω, 0.5);
    gl_FragColor = texture2D(uGradSampler, brushTexCoord);
}

I rendered using the above shader after making 1024 * 1 Texture with the gradient stops information. We can implement the spread mode using GL_TEXTURE_WRAP modes.

Appendix B. The Spread Mode of Radial Gradient

Generally, the gradient in 2D Vector Graphics has a spread mode property that consists of pad, repeat, and reflect[6].  We can see how to render pad, repeat, and reflect radial gradient in Pic6. W3C does not define the spread mode of HTML5 canvas radial gradient, yet.
Pic6. pad, repeat, and reflect of the radial gradient.

The common 2D vector graphics radial gradient has two properties: the circle and the focal point (instead of the start circle). We can see it in OpenVG[4], SVG[6], Qt[5], and etc.. You can see how the common 2D vector graphics radial gradient is rendered in Pic7. I captured Pic7 using the Qt gradient demo application after building qt 4.8.
Pic7. Qt gradient demo.

When the focal point is out of the circle, the common 2D vector graphics radial gradient is rendered like Pic8.

Pic8. Qt gradient demo with the focal point out of the end circle.

If you want to know the formula, refer to OpenVG Spec.[4]

Most of 2D vector graphics libraries use the focal point radial gradient because the two circle radial gradient has two roots, but the two circle radial gradient is more intuitive to human. Both have pros. and cons..

Reference

[1] HTML5 Canvas Radial Gradient Specification
http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-createlineargradient

[2] Quartz 2D Radial Gradient http://developer.apple.com/library/mac/#documentation/graphicsimaging/conceptual
/drawingwithquartz2d/dq_shadings/dq_shadings.html

[3] Canvas element in Wiki
http://en.wikipedia.org/wiki/Canvas_element#History

[4] OpenVG Specification 1.1
http://www.khronos.org/registry/vg/specs/openvg-1.1.pdf

[5] Qt Gradients
http://doc.qt.nokia.com/4.7-snapshot/demos-gradients.html

[6] SVG1.1 13 Gradients and Patterns
http://www.w3.org/TR/SVG/pservers.html

Leave a Reply

Your email address will not be published. Required fields are marked *

14 + 12 =