Calculate X,Y pixel position on gradient based on hex color using Javascript

Spread the love

Question Description

I have a raindow HSV gradient canvas that when you click it, an element is added at that location with its background as the color of the clicked pixel.

What I’d like is to have it work in reverse as well. For example if you had a hex color, I’d like to find that pixel on the canvas and create an element at that position.

My first thought was to somehow use a matrix/quadrant system. My next thought was that since I’m using HSV, I could use my HSV gradient location points to figure out the location. The problem is that my points aren’t equidistant from each other which makes it harder. On top of that, I have a white gradient and black gradient covering the main color gradient and I need that to be accounted for.

So my question is, how can I find the position of the color pixel or at least it’s closest match by just using a hex code?

Here is my code thus far:
http://codepen.io/shelbywhite/pen/EyqPWY?editors=1000

HTML:

CSS:

.container {
    background: grey;
    height: 350px;
    width: 400px;
}

.circle {
    background: transparent;
    box-shadow: 0 0 8px rgba(0,0,0,0.2);
    border-radius: 50%;
    border: 2px solid #fff;
    height: 20px;
    margin: -12px;
    width: 20px;
    position: absolute;
}

.colorSpectrum {
    display: block;  
    height: 100%;
    transform: translateZ(0);
    width: 100%;
}

Javascript:

$(function() {

    var closest = function(num, arr) {
        var curr = arr[0];
        var diff = Math.abs(num - curr);

        for (var val = 0; val < arr.length; val++) {
            var newdiff = Math.abs(num - arr[val]);
            if (newdiff < diff) {
                diff = newdiff;
                curr = arr[val];
            }
        }

        return curr;
    };


    var container = $('.container');
    var containerWidth = container.width();
    var containerHeight = container.height();

    var verticalGradientsHeight = Math.round(containerHeight * .34);
    console.log('verticalGradientsHeight', verticalGradientsHeight);
    var round = function(value, decimals) {
        return Number(Math.round(value+'e'+decimals)+'e-'+decimals);
    };


    // Draws the color spectrum onto the canvas
    var drawColorSpectrum = function() {

        // Cache canvas element
        var canvasElement = $('.colorSpectrum');

        // Cache javascript element
        var canvas = canvasElement[0];

        // Get canvas context
        var ctx = canvas.getContext('2d');

        // Cache page height
        var canvasWidth = containerWidth;

        // Cache page height
        var canvasHeight = containerHeight - 72;

        // Bottom gradient start position
        var blackStartYPos = canvasHeight - verticalGradientsHeight;

        // Bottom gradient end position
        var blackEndYPos = canvasHeight;

        // Create white gradient element
        var white = ctx.createLinearGradient(0, 0, 0, verticalGradientsHeight);

        // Create black gradient element
        var black = ctx.createLinearGradient(0, blackStartYPos, 0, blackEndYPos);

        // Create new instance of image
        var img = new Image();


        // Cache container
        _colorSpectrumContainer = canvasElement.parent();

        // Set global var
        spectrumCanvas = canvasElement;

        // Set width of canvas
        canvas.width = canvasWidth;

        // Set height of canvas
        canvas.height = canvasHeight;

        // Image load listener
        img.onload = function() {

            // Draw intial image
            ctx.drawImage(this, 0, 0, canvasWidth, canvasHeight);

            // Draw white to transparent gradient
            white.addColorStop(0, "hsla(0,0%,100%,1)");
            white.addColorStop(0.05, "hsla(0,0%,100%,1)");
            white.addColorStop(0.20, "hsla(0,0%,100%,0.89)");
            white.addColorStop(0.38, "hsla(0,0%,100%,0.69)");
            white.addColorStop(0.63, "hsla(0,0%,100%,0.35)");
            white.addColorStop(0.78, "hsla(0,0%,100%,0.18)");
            white.addColorStop(0.91, "hsla(0,0%,100%,0.06)");
            white.addColorStop(1, "hsla(0,0%,100%,0)");
            ctx.fillStyle = white;
            ctx.fillRect(0, 0, canvasWidth, verticalGradientsHeight);

            // Draw black to transparent gradient
            black.addColorStop(0, "hsla(0,0%,0%,0)");
            black.addColorStop(0.20, "hsla(0,0%,0%,0.01)");
            black.addColorStop(0.28, "hsla(0,0%,0%,0.04)");
            black.addColorStop(0.35, "hsla(0,0%,0%,0.09)");
            black.addColorStop(0.51, "hsla(0,0%,0%,0.26)");
            black.addColorStop(0.83, "hsla(0,0%,0%,0.69)");
            black.addColorStop(1, "hsla(0,0%,0%,1)");

            ctx.fillStyle = black;
            ctx.fillRect(0, blackStartYPos, canvasWidth, verticalGradientsHeight);
        }

        // Set image source
        img.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAABCAYAAACbv+HiAAAA0ElEQVR4AYWSh2oDMAwFz6u7//+d2YmXalGBIBM47nnPIIEtmd8FGBTgDbPxDmbn49pX+cZX+Nz4mkZ2SECEAXTCAprlalntBC5whdUJnOfKEy5DjZYtB+o0D3XUMk0tkaZZEn2VuyiJQQQywS/P4c25ucTrfF3ndsoVdjmy3NMiuptR1eHfNcBFM2orW1ZXru00JZiBDrIII5AG5AlloX5TcG6/ywuuv0zAbyL4TWRZmIvU5TNBTjCPIIu5N3YgO7Wxtbot3q4+2LgTyFnZ/QHzBZD1KDpyqQAAAABJRU5ErkJggg==";
    };


    // 
    var hexToRgb = function(hex) {

        hex = hex.replace('#','');

        r = parseInt(hex.substring(0, 2), 16);
        g = parseInt(hex.substring(2, 4), 16);
        b = parseInt(hex.substring(4, 6), 16);

        return [r, g, b];
    };


    // 
    var rgbToHsb = function(r, g, b) {

        var rr, gg, bb,
            r = r / 255,
            g = g / 255,
            b = b / 255,
            h, s,
            v = Math.max(r, g, b),
            diff = v - Math.min(r, g, b),
            diffc = function(c){
                return (v - c) / 6 / diff + 1 / 2;
            };

        if (diff == 0) {
            h = s = 0;
        } else {
            s = diff / v;
            rr = diffc(r);
            gg = diffc(g);
            bb = diffc(b);

            if (r === v) {
                h = bb - gg;
            }else if (g === v) {
                h = (1 / 3) + rr - bb;
            }else if (b === v) {
                h = (2 / 3) + gg - rr;
            }
            if (h < 0) {
                h += 1;
            }else if (h > 1) {
                h -= 1;
            }
        }

        return {
            h: Math.round(h * 360),
            s: Math.round(s * 100),
            b: Math.round(v * 100)
        };
    };


    // Find hue in stop range
    var findHueInStopRange = function(hue) {

        // Array of hue stops with HSV, RGB, and HEX info
        var stops = [{
            h: 0,
            l: 0,
            s: 100,
            b: 100
        }, {
            h: 60,
            l: 21,
            s: 100,
            b: 100
        }, {
            h: 120,
            l: 40,
            s: 85,
            b: 85
        }, {
            h: 180,
            l: 56,
            s: 85,
            b: 85
        }, {
            h: 237,
            l: 72,
            s: 86,
            b: 96
        }, {
            h: 300,
            l: 89,
            s: 86,
            b: 96
        }, {
            h: 359,
            l: 100,
            s: 100,
            b: 100
        }];

        // Total number of stops
        var stopsLength = stops.length;

        // Loop through stops
        for (var i = 0; i < stopsLength; i += 1) {

            // Temp set
            var currentStop = stops[i];

            // Temp set
//             var nextStop = stops[i + 1];
            var nextStop = (i + 1 > stopsLength - 1) ? currentStop : stops[i + 1];

            // Location is a percentage
            var huePos;

            // Temp set
            var xPos = false;

            console.log('hue', currentStop.h, '>>', hue, '<<', nextStop.h);
            // Find which range of hue stops the current color is 
            // Hue is between current and next hue stop
            if (hue >= currentStop.h && hue <= nextStop.h) {

                // hue is current stop
                if (hue === currentStop.h) {

                    // Set as location
                    huePos = currentStop.l;

                    // hue is next stop
                } else if (hue === nextStop.h) {

                    // Set as location
                    huePos = nextStop.l;

                // Hue is somewhere between stops
                } else {

                    // Get percentage location between hue stops
                    var relativeHuePos = (hue - currentStop.h) / (nextStop.h - currentStop.h);

                    // Normalized to fit custom gradient stop locations
                    huePos = relativeHuePos * (nextStop.l - currentStop.l) + currentStop.l;
                }

                // A location was found
                if (huePos) {

                    // Convert from percentage to pixel position
                    xPos = Math.round(containerWidth * (huePos / 100));

                    return xPos;

                } else {

                    continue;
                }
            }
        }
    };


    // Find saturation in stop range
    var findSaturationInStopRange = function (saturation) {

        // Array of hue stops with HSV, RGB, and HEX info
        var stops = [{
            l: 0,
            s: 0
        }, {
            l: 0.05,
            s: 6
        }, {
            l: 0.20,
            s: 18
        }, {
            l: 0.38,
            s: 35
        }, {
            l: 0.63,
            s: 69
        }, {
            l: 0.78,
            s: 89,
        }, {
            l: 0.91,
            s: 100,
        }, {
            l: 1,
            s: 100,
        }];

        // Total number of stops
        var stopsLength = stops.length;

        // Loop through stops
        for (var i = 0; i < stopsLength; i += 1) {

            // Temp set
            var currentStop = stops[i];

            // Temp set
            var nextStop = (i + 1 > stopsLength - 1) ? currentStop : stops[i + 1];

            // Location is a percentage
            var satPos;

            // Temp set
            var yPos = false;

            // Convert location to percentage
            var currentStopLocation = currentStop.l * 100;

            // Convert location to percentage
            var nextStopLocation = nextStop.l * 100;


            // Find which range of hue stops the current color is 
            // Hue is between current and next hue stop
            if (saturation >= currentStop.s && saturation <= nextStop.s) {

                // hue is current stop
                if (saturation === currentStop.s) {

                    // Set as location
                    satPos = currentStopLocation;

                    // hue is next stop
                } else if (saturation === nextStop.s) {

                    // Set as location
                    satPos = nextStopLocation;

                // Hue is somewhere between stops
                } else {

                    // Get percentage location between gradient stops
                    var ratioBetweenSaturation = (saturation - currentStop.s) / (nextStop.s - currentStop.s);

                    // Normalized to fit custom gradient stop locations
                    satPos = ratioBetweenSaturation * (nextStopLocation - currentStopLocation) + currentStopLocation;
                }

                console.log('ratioBetweenSaturation', ratioBetweenSaturation);
                console.log('satPos', satPos);
                console.log('saturation', saturation, '>=', currentStop.s, saturation, '<=', nextStop.s);

                // A location was found
                if (satPos !== false) {

                    // Convert from percentage to pixel position
                    yPos = Math.round(verticalGradientsHeight * (satPos / 100));

                    return yPos;

                } else {

                    continue;
                }
            }
        }
    };


    // Find brightness in stop range
    var findBrightnessInStopRange = function (brightness) {

        // Array of hue stops with HSV, RGB, and HEX info
        var stops = [{
            l: 0,
            b: 100
        }, {
            l: 0.20,
            b: 88
        }, {
            l: 0.28,
            b: 69
        }, {
            l: 0.35,
            b: 26
        }, {
            l: 0.51,
            b: 9
        }, {
            l: 0.83,
            b: 4,
        }, {
            l: 1,
            b: 0,
        }];

        // Total number of stops
        var stopsLength = stops.length;

        // Loop through stops
        for (var i = 0; i < stopsLength; i += 1) {

            // Temp set
            var currentStop = stops[i];

            // Temp set
            var nextStop = (i + 1 > stopsLength - 1) ? currentStop : stops[i + 1];

            // Location is a percentage
            var brightPos;

            // Temp set
            var yPos = false;

            // Convert location to percentage
            var currentStopLocation = currentStop.l * 100;

            // Convert location to percentage
            var nextStopLocation = nextStop.l * 100;

            console.log('brightness', brightness, '>=', currentStop.b, brightness, '<=', nextStop.b);


            // Find which range of hue stops the current color is 
            // Hue is between current and next hue stop
            if (brightness <= currentStop.b && brightness >= nextStop.b) {

                // hue is current stop
                if (brightness === currentStop.b) {

                    // Set as location
                    brightPos = currentStopLocation;

                    // hue is next stop
                } else if (brightness === nextStop.b) {

                    // Set as location
                    brightPos = nextStopLocation;

                // Hue is somewhere between stops
                } else {

                    // Get percentage location between gradient stops
                    var ratioBetweenBrightness = (brightness - currentStop.b) / (nextStop.b - currentStop.b);

                    // Normalized to fit custom gradient stop locations
                    brightPos = ratioBetweenBrightness * (nextStopLocation - currentStopLocation) + currentStopLocation;
                }

                console.log('ratioBetweenBrightness', ratioBetweenBrightness);
                console.log('brightPos', brightPos);
                console.log('brightness', brightness, '>=', currentStop.b, brightness, '<=', nextStop.b);

                // A location was found
                if (brightPos !== false) {

                    // Convert from percentage to pixel position
                    yPos = Math.round(verticalGradientsHeight * (brightPos / 100));

                    return yPos;

                } else {

                    continue;
                }
            }
        }
    };


    // Get coordinates from hue, brightness, saturation
    var getColorCoordinates = function (hex) {

        // Convert hex to rgb
        var rgb = hexToRgb(hex);
        console.log('rgb', rgb);
        // Convert rgb to hsb
        var hsb = rgbToHsb(rgb[0], rgb[1], rgb[2]);
        console.log('hsb', hsb);

        // Set x position to position of hue
        var xPos = findHueInStopRange(hsb.h);
        var yPos = 0;


        //  if 100, get (containerHeight - verticalGradientHeight) + whatever position is set with bottom gradient

        // 

        // Saturation and brightness are both maxed
        if (hsb.s === 100 && hsb.b === 100) {

            // Set y position at center of container
            yPos = containerHeight * 0.5;

        } else {
                console.log('using nothing', hsb.s, hsb.b);

            //
            if (hsb.s < 100) {

                // Saturation y position (upper quadrant)
                yPos = findSaturationInStopRange(hsb.s);
                console.log('using saturation', yPos);

            } else if (hsb.b < 100) {

                // Brightness y position (lower quadrant)
                yPos = findBrightnessInStopRange(hsb.b);
                console.log('using brightness', yPos);
            }
        }

        return { x: xPos, y: yPos };
    }


    // Get hue location
    var position = false;

    // Temp set
    var hex = '42ad40';

    // Draw gradient
    drawColorSpectrum();

    // Find x position
    position = getColorCoordinates(hex); //91ff26

    console.log('location', position);

    // Draw line
    $('.circle').css({
        top: position.y + 'px',
        left: position.x + 'px',
        background: '#' + hex
    });
});

** UPDATE **

I'm actually trying to do this in HSV not HSL. I don't have preference other than I used photoshop to generate a smooth gradient.

Also, I have added a new example with what I have created. One of the up-voted answers below hints at how to accomplish what I'm trying to do, but so far I haven't been able to successfully do it.

Updated code will be at this link, I've also updated the code above:
http://codepen.io/shelbywhite/pen/EyqPWY?editors=1000

Practice As Follows

To get the location you need the HSL values

// global 
var RGB = [0,0,0]; // holds the RGB values 0-255
var LSH = [0,0,0]; // holds the LSH values (note H is normalised to 0-255)
var rgbToLSH = function(){
    var r = RGB[0]/255;
    var g = RGB[1]/255;
    var b = RGB[2]/255;
    var min = Math.min(r,g,b);
    var max = Math.max(r,g,b);
    var lum = (min+max)/2;
    if(lum > 0.5){
        var sat = (max-min)/(max+min);
    }else{
        var sat = (max-min)/(2-max-min);
    }
    if(r >= b && r >= g){
        var hue = (g-b)/(max-min);
    }else
    if(b >= b && b >= g){
        var hue = 4.0 + (r-g)/(max-min);
    }else{
        var hue = 2.0 + (b-r)/(max-min);
    }
    hue *= 60;
    if(hue < 0) hue += 360;
    hue = (hue/360);
    lum = Math.min(1,Math.max(0,lum));
    sat = Math.min(1,Math.max(0,sat));
    hue = Math.min(1,Math.max(0,hue));
    LSH[0] = lum*255;
    LSH[1] = sat*255;
    LSH[2] = hue*255;

}

Hue will give the position on the x axis and Saturation will give you y from top to midway and Lightness will give you y axis position from midway to the bottom (going by your example);

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.