0

Gradients 6, 7, 8 and 10 have visible gaps just after their initial color. Is there a way to fill that gap in?

I assume that once the range is partitioned, the sum of the integer partitions is less than the original if they are not divisible by the width of the canvas.

Info

  • render1() - Interpolates values by generating a range of colors between two colors.

  • render2() - JavaScript's createLinearGradient() with n-number of even stops.

$(document).ready(function () {
    function addCanvas(ol) {
        var canvas = $('<canvas>', {
            "width": 240,
                "height": 10
        }).addClass('grd')
        .appendTo($('<li>').appendTo(ol));
      return canvas[0];
    }
    var ol = $('<ol>').appendTo($('body'));
    var c1 = '#FF0000';
    var c2 = '#00FF00';
    var canvas;
    for (var i = 1; i <= 30; i+=3) {
        canvas = addCanvas(ol);
        var colors = generateColors(c1, c2, i);
        render(canvas, colors);
    }
    canvas = addCanvas(ol);
    render2(canvas, [c1, c2]);
});

function toHex(c) {
    return (function (hex) {
        return hex.length == 1 ? "0" + hex : hex;
    })(c.toString(16));
}

function hexToRgb(hex) {
    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
    } : null;
}

function rgbToHex(rgb) {
    var r = 0,
        g = 0,
        b = 0;
    if (arguments.length === 1) {
        r = rgb.r || 0;
        g = rgb.g || 0;
        b = rgb.b || 0;
    } else if (arguments.length === 3) {
        r = arguments[0];
        g = arguments[1];
        b = arguments[2];
    }
    return "#" + toHex(r) + toHex(g) + toHex(b);
}


function render(canvas, colors) {
    var ctx = canvas.getContext("2d");
    var n = colors.length;
    var x = 0;
    var subWidth = canvas.width / n;
    for (var i = 0; i < n; i++) {
        var start = i * subWidth;
        var end = start + subWidth;
        ctx.fillStyle = colors[i];
        ctx.fillRect(x + start, 0, x + end, canvas.height);
    }
}

function render2(canvas, colors) {
    var ctx = canvas.getContext("2d");
    var x = canvas.x;
    var y = canvas.y;
    var width = canvas.width;
    var height = canvas.height;
    ctx.fillStyle = createGradient(ctx, colors, x, y, width, height)
    ctx.fillRect(0, 0, width, height);
}

function generateColors(color1, color2, n) {
    function partition(a, b, n) {
        function magnitude(a, b) {
            if (a === b) return 0;
            else if (a > b) return -1 * (a - b);
            else return b - a;
        }
        return magnitude(a, b) / n;
    }
    var c1 = hexToRgb(color1);
    var c2 = hexToRgb(color2);
    var rd = partition(c1.r, c2.r, n);
    var gd = partition(c1.g, c2.g, n);
    var bd = partition(c1.b, c2.b, n);
    var colors = [];
    var i, r, g, b, h = n/2;
    for (i = 0; i < h; i++) {
        r = Math.floor(c1.r + rd * i);
        g = Math.floor(c1.g + gd * i);
        b = Math.floor(c1.b + bd * i);
        colors.push(rgbToHex(r, g, b));
    }
    for (var j = 0; i < n; j++, i++) {
        var k = h-j-1;
        r = Math.floor(c2.r - rd * k);
        g = Math.floor(c2.g - gd * k);
        b = Math.floor(c2.b - bd * k);
        colors.push(rgbToHex(r, g, b));
    }  
    return colors;
}

function createGradient(ctx, colors, x, y, width, height) {
    var h = height / 2;
    var grd = ctx.createLinearGradient(0, h, width, h);
    var n = Math.max(colors.length - 1, 1);
    for (var i = 0; i < colors.length; i++) {
        grd.addColorStop(i / n, colors[i]);
    }
    return grd;
}
canvas.grd {
  display: inline-block;
  margin: 1px 0;
}

ol {
  padding-left: 32px; 
  list-style-type:decimal;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132

1 Answers1

2

Updated Answer: New version of render function that splits up the extra pixels and puts around half on each side:

function render(canvas, colors) {
    var ctx = canvas.getContext("2d");
    var n = colors.length;
    var r = canvas.width % n;
    var a = Math.floor(r/2), b = n - r + a;
    var subWidth = Math.floor(canvas.width / n) + 1;
    var start = 0;

    for (var i = 0; i < n; i++) {
        if (i == a) {
            subWidth -= 1;
        }
        if(i == b) {
            subWidth += 1;
        }
        ctx.fillStyle = colors[i];
        ctx.fillRect(start, 0, subWidth, canvas.height);

        start += subWidth;
    }
}

Original Answer

A few things:

I think what is causing the faint gaps is that some of the rectangles are being drawn to widths that include fractional pixels. What you can do is draw the rectangles to exact pixel widths, with some being one pixel wider than others. Here's a rewrite of your render function to do just that (I think. It's pretty late here :-))

function render(canvas, colors) {
    var ctx = canvas.getContext("2d");
    var n = colors.length;
    var r = canvas.width % n;
    var subWidth = Math.floor(canvas.width / n) + 1;
    var start = 0;

    for (var i = 0; i < n; i++) {
        if (i == r) {
            subWidth -= 1;
        }
        ctx.fillStyle = colors[i];
        ctx.fillRect(start, 0, subWidth, canvas.height);

        start += subWidth;
    }
}

This calculates the largest whole width that will fit into the width of the canvas colors.length times. Then it figures out how many extra pixels there will be left (r) and then loops through giving the first r rectangles 1 extra pixel width.

(Also, remember that the fillRect function's third parameter is width, not end. Your call is working because the rectangles are being drawn over the top of each other and the start position of each rectangle is correct.)

I'm sure there's a more elegant solution to this, but this one seems to work!

Also, while I was playing around with this I noticed that in the javascript, canvas.width was 300, not 240. This led me to search around and I found this nice explanation of how that works on SO: Canvas width and height in HTML5. I think the code is rendering into a 300 x 150 canvas and then being scaled down to 240 x 10. Ultimately though, this wasn't causing the problem.

Hope this helps!

Community
  • 1
  • 1
peterjb
  • 819
  • 7
  • 6
  • Thanks! I Updated my script and implemented an `interpolate()` function: http://jsfiddle.net/MrPolywhirl/uzjtnvn3/ – Mr. Polywhirl Oct 24 '14 at 11:20
  • I wrote this today, you can pass in an any number of colors - [JSFiddle](http://jsfiddle.net/MrPolywhirl/5bwbgm56/). – Mr. Polywhirl Oct 25 '14 at 05:08
  • OK, this is awesome, but I couldn't help but not be happy with how my update to your render function adds all the extra pixels to the left. Here's a version that puts about half on each side: http://jsfiddle.net/5bwbgm56/4/ . Also, I think the scaling problem I mentioned in my answer is really effecting this now so I changed the addCanvas function to set the width and height on it. I think the quality of the output is better this way. Again this is very cool. If you can tell me, what are you going to use it for? Or what brought on the inspiration to do this? – peterjb Oct 25 '14 at 05:40
  • Oh wow, now that you mention it, it was looking a bit jagged. Thanks for looking out for that. I was making a progress bar / health meter and decided to add a non-seamless gradient... that's all I got. – Mr. Polywhirl Oct 25 '14 at 05:55