user1960364
user1960364

Reputation: 2001

HTML5 Canvas - Drawing Linear Gradients on a Circle (Color Wheel)

I'm trying to draw a circle with, not radial gradients, but linear gradients that go around the circle... Basically, I'm trying to create a color wheel and it has to be dynamic as the colors will be customizable... However, I'm completely baffled on how to approach this matter...

I thought I could draw my own circle and color it, then loop the process with a larger radius to fill it out. But that proved to not only be extremely ineffecient but very buggy too...

Here was my first attempt: http://jsfiddle.net/gyFqX/1/ I stuck with that method but changed it to fill a 2x2 square for each point on the circle. It worked alright for blending up to 3 colors, but then you begin to notice it's distortion.

Anyway, I've continued working on it a bit and this is what I have now: http://jsfiddle.net/f3SQ2/

var ctx = $('#canvas')[0].getContext('2d'),
    points = [],
    thickness = 80;

for( var n = 0; n < thickness; n++ )
    rasterCircle( 200, 200, (50 + n) );

function fillPixels() {
    var size = points.length,
        colors = [ 
            hexToRgb( '#ff0000' ), // Red
            hexToRgb( '#ff00ff' ), // Magenta
            hexToRgb( '#0000ff' ), // Blue
            hexToRgb( '#00ffff' ), // Teal
            hexToRgb( '#00ff00' ), // Green
            hexToRgb( '#ffff00' ), // Yellow            
            hexToRgb( '#ff0000' ), // Red
        ],
        colorSpan = colors.length - 1;

    if ( colors.length > 0 ) {
        var lastPadding = size % colorSpan,
            stepSize = size / colorSpan,
            steps = null, 
            cursor = 0;

        for ( var index = 0; index < colorSpan; index++ ) {
            steps = Math.floor( ( index == colorSpan - 1 ) ? stepSize + lastPadding : stepSize );
            createGradient( colors[ index ], colors[ index + 1 ], steps, cursor );
            cursor += steps;
        }
    }

    function createGradient( start, end, steps, cursor ) {
        for ( var i = 0; i < steps; i++ ) {
            var r = Math.floor( start.r + ( i * ( end.r - start.r ) / steps ) ),
                g = Math.floor( start.g + ( i * ( end.g - start.g ) / steps ) ),
                b = Math.floor( start.b + ( i * ( end.b - start.b ) / steps ) );

            ctx.fillStyle = "rgba("+r+","+g+","+b+",1)";
            ctx.fillRect( points[cursor][0], points[cursor][1], 2, 2 );
            cursor++;
        }
    }

    points = [];
}

function setPixel( x, y ) {
    points.push( [ x, y ] );
}

function rasterCircle(x0, y0, radius) {
    var f = 1 - radius,
        ddF_x = 1,
        ddF_y = -2 * radius,
        x = 0,
        y = radius;

    setPixel(x0, y0 + radius);
    while(x < y) {
        if(f >= 0) {
            y--;
            ddF_y += 2;
            f += ddF_y;
        }
        x++;
        ddF_x += 2;
        f += ddF_x;    
        setPixel(x0 - x, y0 - y);
    }

    var temp = [];
    f = 1 - radius,
    ddF_x = 1,
    ddF_y = -2 * radius,
    x = 0,
    y = radius;
    while(x < y) {
        if(f >= 0) {
            y--;
            ddF_y += 2;
            f += ddF_y;
        }
        x++;
        ddF_x += 2;
        f += ddF_x;    
        temp.push( [x0 - y, y0 - x] );
    }
    temp.push( [x0 - radius, y0] );

    for(var i = temp.length - 1; i > 0; i--)
        setPixel( temp[i][0], temp[i][1] );

    fillPixels();
}

What I'm trying to accomplish is something like this: http://img252.imageshack.us/img252/3826/spectrum.jpg

The 'brightness' (white to black fade) is not an issue as I know it can be accomplished by using a radial gradient after the color spectrum has been drawn. However, I'd appreciate some help in figuring out how to draw the spectrum itself.

I was even thinking I could draw a linear one and then bend (transform) it, but there aren't any native functions to do that and tackling something such as that is above my skill level. :-/

Upvotes: 1

Views: 3408

Answers (2)

Shmiddty
Shmiddty

Reputation: 13967

Check this out: http://jsfiddle.net/f3SQ2/5/

var can = $('#canvas')[0],
    ctx = can.getContext('2d'),
    radius = 120,
    thickness = 80,
    p = {
        x: can.width,
        y: can.height
    },
    start = Math.PI,
    end = start + Math.PI / 2,
    step = Math.PI / 180,
    ang = 0,
    grad,
    r = 0,
    g = 0,
    b = 0,
    pct = 0;

ctx.translate(p.x, p.y);
for (ang = start; ang <= end; ang += step) {
    ctx.save();
    ctx.rotate(-ang);
    // linear gradient: black->current color->white
    grad = ctx.createLinearGradient(0, radius - thickness, 0, radius);
    grad.addColorStop(0, 'black');

    h = 360-(ang-start)/(end-start) * 360;
    s = '100%';
    l = '50%';

    grad.addColorStop(.5, 'hsl('+[h,s,l].join()+')');
    grad.addColorStop(1, 'white');
    ctx.fillStyle = grad;

    // the width of three for the rect prevents gaps in the arc
    ctx.fillRect(0, radius - thickness, 3, thickness);
    ctx.restore();
}

Edit: fixed color spectrum. Apparently we can just give it HSL values, no need for conversions or messy calculations!

Modified a few things to handle scaling better: http://jsfiddle.net/f3SQ2/6/

step = Math.PI / 360

ctx.fillRect(0, radius - thickness, radius/10, thickness);

You could for example set the gradient stops like so:

h = 360-(ang-start)/(end-start) * 360;
s = '100%';

grad.addColorStop(0, 'hsl('+[h,s,'0%'].join()+')');  //black 
grad.addColorStop(.5,'hsl('+[h,s,'50%'].join()+')'); //color
grad.addColorStop(1, 'hsl('+[h,s,'100%'].join()+')');//white

Upvotes: 4

Kris
Kris

Reputation: 7170

My first note would be that the image you linked to has all 3 components it doesn't need to change and could just be a static image.

I adapted some code from a project i'm working on: http://jsfiddle.net/f3SQ2/1/

function drawColourArc(image) {
    var data = image.data;
    var i = 0;
    var w = image.width, h = image.height;
    var result = [0, 0, 0, 1];
    var outer = 1, inner = 0.5;
    var mid = 0.75;

    for (var y = 0; y < h; y++) {
        for (var x = 0; x < w; x++) {

            var dx = (x / w) - 1, dy = (y / w) - 1;

            var angular = ((Math.atan2(dy, dx) + Math.PI) / (2 * Math.PI)) * 4;
            var radius = Math.sqrt((dx * dx) + (dy * dy));

            if (radius < inner || radius > outer) {
                data[i++] = 255;
                data[i++] = 255;
                data[i++] = 255;
                data[i++] = 0;
            }
            else {
                if (radius < mid) {
                    var saturation = 1;
                    var brightness = (radius - 0.5) * 4;
                }
                else {
                    var saturation = 1- ((radius - 0.75) * 4);
                    var brightness = 1;
                }

                result[0] = angular;
                result[1] = saturation;
                result[2] = brightness;

                result[3] = 1;

                //Inline HSBToRGB
                if (result[1] == 0) {
                    result[0] = result[1] = result[2] = result[2];
                }
                else {
                    var varH = result[0] * 6;
                    var varI = Math.floor(varH); //Or ... var_i = floor( var_h )
                    var var1 = result[2] * (1 - result[1]);
                    var var2 = result[2] * (1 - result[1] * (varH - varI));
                    var var3 = result[2] * (1 - result[1] * (1 - (varH - varI)));

                    if (varI == 0 || varI == 6) {
                        result[0] = result[2];
                        result[1] = var3;
                        result[2] = var1;
                    }
                    else if (varI == 1) {
                        result[0] = var2;
                        result[1] = result[2];
                        result[2] = var1;
                    }
                    else if (varI == 2) {
                        result[0] = var1;
                        result[1] = result[2];
                        result[2] = var3;
                    }
                    else if (varI == 3) {
                        result[0] = var1;
                        result[1] = var2;
                        result[2] = result[2];
                    }
                    else if (varI == 4) {
                        result[0] = var3;
                        result[1] = var1;
                        result[2] = result[2];
                    }
                    else {
                        result[0] = result[2];
                        result[1] = var1;
                        result[2] = var2;
                    }

                }
                //End of inline
                data[i++] = result[0] * 255;
                data[i++] = result[1] * 255;
                data[i++] = result[2] * 255;
                data[i++] = result[3] * 255;
            }
        }
    }
};

var canvas = document.getElementsByTagName("canvas")[0];
var ctx = canvas.getContext("2d");
var image = ctx.createImageData(canvas.width, canvas.height);

drawColourArc(image);
ctx.putImageData(image, 0, 0);

This does it per-pixel which is accurate but you may want to draw an outline to combat the aliasing. It could be adapted to use custom colours instead of interpolating hue.

Upvotes: 1

Related Questions