4

I am working on a relatively simple app that will generate differently colored version of the same .SVG image (modified by HSL values).

Right now I'm implementing hue changes. I am using a generated list of colors. Before drawing the color variations a base color is selected. In this case I used a dead simple .SVG of a green square (hsl(137,100%,82%)).

This is what my code looks like:

for(let i = 0; i < nColors; i++){
                    ctx.filter = 'hue-rotate('+(palette[i].h-hStart)+'deg)';  
                    ctx.drawImage(img, i*100, 0, 100, 100);      
                    ctx.filter = "none";            
                }

where:

  • nColors is the amount of colors in the array

  • palette is an array of objects with properties h, s and l - cointains the colors

  • hStart is the base hue of my image (in this case 137)

I'm calculating the hue difference between the current color and the base color and rotating the canvas drawing hue by that number, then drawing the squares side by side. Unfortunately, here are my results.

enter image description here

The list at the top contains the actual colors I want to impose on my .SVG, the squares at the bottom are my canvas.

As you can see, the color diverts more and more with each iteration. I've checked the exact colors in Photoshop (I know Photoshop uses HSB but I converted the values) and the S&L differences are really big and somewhat regular (the first one is correct).

  1. 100,82
  2. 100,82
  3. 100,89
  4. 83,100
  5. 52,100
  6. 53,100
  7. 60,100
  8. 62,100

Now, I did read somewhere that different browsers may render colors differently so I checked the colors with getPixelData and the results matched my Photoshop readings, therefore I believe that the issue indeed lies in the hue-rotate filter.

I could achieve the same results by reading all the pixel data and changing it "manually", but in the end I'd like to paint each new image to an invisible, large canvas and export high resolution .PNGs - it would be rather CPU intensive and take a long time.

Is it actually a bug/feature of hue-rotate or am I making a mistake somewhere? Is there any way to fix it? Is there any other way to achieve the same results while keeping it relatively simple and sticking to vectors?

EDIT: here's a fiddle

George Kagan
  • 5,913
  • 8
  • 46
  • 50
ngr900
  • 99
  • 1
  • 6
  • 1
    Can you add the image to your post? (instead of linking to it) – Prashanth Chandra Nov 15 '16 at 00:28
  • I'm sorry, I need 10 reputation for that and I just realized that I've used the wrong account - a brand new one. – ngr900 Nov 15 '16 at 00:32
  • Are you running your filter in the linear RGB colour space (the default) when you're expecting it to be run in the rgb colour space? If that's not the problem is, please create a [mcve] so we can run something. – Robert Longson Nov 15 '16 at 03:07
  • Here's a [fiddle](https://jsfiddle.net/v7dvojnL/). To be honest I am not sure what you mean by "using" a color space - I don't recall any color space settings for the canvas or its filters. Ideally, I'd like to operate on HSL exclusively but the canvas uses RGB data. Still, even considering color space differences the saturation/brigthness distortion is extreme, it's clearly visible in the last square. – ngr900 Nov 15 '16 at 03:47
  • I mean this: https://www.w3.org/TR/SVG/filters.html#FilterPrimitivesOverviewIntro and https://www.w3.org/TR/SVG/painting.html#ColorInterpolationFiltersProperty – Robert Longson Nov 15 '16 at 04:05
  • But those are SVG filter properties. I'm working on a Canvas and simply loading the SVG as an image. I mean once it's painted the canvas has no idea that it was an SVG so why would it matter? Either way, I'm not changing anything, all defaults. – ngr900 Nov 15 '16 at 04:14
  • canvas context2d `filter` is still premature, moreover with direct CSS functions. You could try to set an svg filter instead of a css one ( `ctx.filter = 'url(#theIdOfYourSVGFilter)'`). Also, I'm a bit confused by *I checked the colors with getPixelData and the results matched my Photoshop readings*. context `filter` property directly applies the filter on the drawn pixels. So the pixels data from `getImageData()` are the ones that you should see, except if you did also set a CSS filter (directly in the CSS rules), but you didn't right ? And without an [MCVE], hard to tell exactly what's wrong – Kaiido Nov 15 '16 at 04:32
  • [I added a fiddle](https://jsfiddle.net/v7dvojnL/). Forgive the getPixelData thing, I now realize that didn't make sense. I just meant that raw RGB data from the canvas extracted through getPixelData was the same as checking color on a screenshot with Photoshop. I wanted to make sure that the color my browser is displaying was the color canvas contains. I'm not applying any CSS filters. – ngr900 Nov 15 '16 at 04:43
  • Well I've got the same result using directly CSS filters and canvas one, and this on both FF and chrome => your value is converted to `HSL(200°, 83%, 61%)`. I think that when applying filters, values are first converted to rgb so saturation and lightness are not assured to be the same. But I'm not sure, and don't have really the time to dig into it and provide any answer. Check the notes : https://docs.webplatform.org/wiki/css/functions/hue-rotate + [edited your fiddle](https://jsfiddle.net/v7dvojnL/2/) so that FF doesn't throw a security error => values are a bit different with svg filter. – Kaiido Nov 15 '16 at 05:05

1 Answers1

2

This is not really a bug.

Canvas 2DContext's filter = CSSFilterFunc will produce the same result as the CSS filter: CSSFilterFunc, and the hue-rotate(angle) function does only approximate this hue-rotation : it doesn't convert all your RGBA pixels to their HSL values. So yes, you'll have wrong results.

But, you may try to approximate this using SVGFilterMatrix instead. The original hue-rotate will produce a similar result than the CSSFunc one, but we can calculate the hue rotation and apply it to a colorMatrix.
If you want to write it, here is a paper explaining how to do it : http://www.graficaobscura.com/matrix/index.html

I don't really have time right now to do it, so I'll borrow an already written js implementation of a better approximation than the default one found in this Q/A, written by pixi.js mates and will only show you how to apply it on your canvas, thanks to an SVGFilter.

Note that as correctly pointed by @RobertLongson, you also need to set the color-interpolation-filters property of the feColorMatrix element to sRGB since it defaults to linear-sRGB.

// set our SVGfilter's colorMatrix's values
document.getElementById('matrix').setAttribute('values', hueRotate(100));

var cssCtx = CSSFiltered.getContext('2d');
var svgCtx = SVGFiltered.getContext('2d');
var reqctx = requiredRes.getContext('2d');
cssCtx.fillStyle = svgCtx.fillStyle = reqctx.fillStyle = 'hsl(100, 50%, 50%)';
cssCtx.fillRect(0, 0, 100, 100);
svgCtx.fillRect(0, 0, 100, 100);
reqctx.fillRect(0, 0, 100, 100);

// CSSFunc
cssCtx.filter = "hue-rotate(100deg)";
// url func pointing to our SVG Filter
svgCtx.filter = "url(#hue-rotate)";

reqctx.fillStyle = 'hsl(200, 50%, 50%)';

cssCtx.fillRect(100, 0, 100, 100);
svgCtx.fillRect(100, 0, 100, 100);
reqctx.fillRect(100, 0, 100, 100);

var reqdata = reqctx.getImageData(150, 50, 1, 1).data;
var reqHSL = rgbToHsl(reqdata);
console.log('required result : ', 'rgba(' + reqdata.join() + '), hsl(' + reqHSL + ')');
var svgData = svgCtx.getImageData(150, 50, 1, 1).data;
var svgHSL = rgbToHsl(svgData);
console.log('SVGFiltered : ', 'rgba(' + svgData.join() + '), , hsl(' + svgHSL + ')');
// this one throws an security error in Firefox < 52 
var cssData = cssCtx.getImageData(150, 50, 1, 1).data;
var cssHSL = rgbToHsl(cssData);
console.log('CSSFiltered : ', 'rgba(' + cssData.join() + '), hsl(' + cssHSL + ')');


// hueRotate will create a colorMatrix with the hue rotation applied to it
// taken from https://pixijs.github.io/docs/filters_colormatrix_ColorMatrixFilter.js.html
// and therefore from https://stackoverflow.com/questions/8507885/shift-hue-of-an-rgb-color/8510751#8510751
function hueRotate(rotation) {
  rotation = (rotation || 0) / 180 * Math.PI;
  var cosR = Math.cos(rotation),
    sinR = Math.sin(rotation),
    sqrt = Math.sqrt;

  var w = 1 / 3,
    sqrW = sqrt(w);
  var a00 = cosR + (1.0 - cosR) * w;
  var a01 = w * (1.0 - cosR) - sqrW * sinR;
  var a02 = w * (1.0 - cosR) + sqrW * sinR;
  var a10 = w * (1.0 - cosR) + sqrW * sinR;
  var a11 = cosR + w * (1.0 - cosR);
  var a12 = w * (1.0 - cosR) - sqrW * sinR;
  var a20 = w * (1.0 - cosR) - sqrW * sinR;
  var a21 = w * (1.0 - cosR) + sqrW * sinR;
  var a22 = cosR + w * (1.0 - cosR);
  var matrix = [
    a00, a01, a02, 0, 0,
    a10, a11, a12, 0, 0,
    a20, a21, a22, 0, 0,
    0, 0, 0, 1, 0,
  ];
  return matrix.join(' ');
}

function rgbToHsl(arr) {
  var r = arr[0] / 255,
    g = arr[1] / 255,
    b = arr[2] / 255;
  var max = Math.max(r, g, b),
    min = Math.min(r, g, b);
  var h, s, l = (max + min) / 2;

  if (max == min) {
    h = s = 0;
  } else {
    var d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    switch (max) {
      case r:
        h = (g - b) / d + (g < b ? 6 : 0);
        break;
      case g:
        h = (b - r) / d + 2;
        break;
      case b:
        h = (r - g) / d + 4;
        break;
    }
    h /= 6;
  }
  return [
    Math.round(h * 360),
    Math.round(s * 100),
    Math.round(l * 100)
  ];
}
body{ margin-bottom: 100px}
<!-- this is our filter, we'll add the values by js -->
<svg height="0" width="0">
  <filter id="hue-rotate">
    <feColorMatrix in="SourceGraphic" id="matrix" type="matrix" color-interpolation-filters="sRGB" />
  </filter>
</svg>
<p>CSS Filtered :
  <br>
  <canvas id="CSSFiltered" width="200" height="100"></canvas>
</p>
<p>SVG Filtered :
  <br>
  <canvas id="SVGFiltered" width="200" height="100"></canvas>
</p>
<p>Required Result :
  <br>
  <canvas id="requiredRes" width="200" height="100"></canvas>
</p>
Community
  • 1
  • 1
Kaiido
  • 123,334
  • 13
  • 219
  • 285