8

I want to change the background color of this image while keeping the form, the effects and the contour of the image.

<canvas id="canvas01" width="1200" height="800"></canvas>
<script>
    function drawImage(imageObj,x, y, width, height){
        var canvas = document.getElementById('canvas01');
        var context = canvas.getContext('2d');
        context.drawImage(imageObj, x, y, width, height);
    }
    var image = new Image();
    image.onload = function(){
        drawImage(this, 400, 100, 320, 450);
    };
    image.src ="images/658FFBC6.png";
</script>
Francisco C
  • 8,871
  • 4
  • 31
  • 41
M.Chaymae
  • 137
  • 1
  • 6
  • see this fiddle:http://jsfiddle.net/pHwmL/1/ . related to the thread https://stackoverflow.com/questions/10069171/canvas-change-colors-of-an-image-using-html5-css-js – Suchit kumar Jul 19 '17 at 10:08
  • Are you talking about changing the background color your the image, which in your case is transparent, or the global color the image? Like make your green T-shirt in red. – Allan Raquin Jul 19 '17 at 10:19
  • i already tried this before, the thing is, the canvas itself contains a lot of images, so i just want to tint one of the image in the canvas and not the whole canvas. thank you Suchit Kumar.. – M.Chaymae Jul 19 '17 at 10:42
  • i'm talking about changing the color of the T-shirt (Like make it red or blue), but i have to keep the effect and the shape of the Tshirt. Thank you Allan Raquin – M.Chaymae Jul 19 '17 at 10:44

3 Answers3

15

Luma preservation

At the risk of looking similar to the existing answer, I would like to point out a small but important difference using a slightly different approach.

The key is to preserve the luma component in an image (ie. shadow details, wrinkles etc. in this case) so two steps are needed to control the look using blending modes via globalCompositeOperation (or alternatively, a manual approach using conversion between RGB and the HSL color-space if older browsers must be supported):

  • "saturation": will alter the chroma (intensity, saturation) from the next drawn element and apply it to the existing content on the canvas, but preserve luma and hue.
  • "hue": will grab the chroma and luma from the source but alter the hue, or color if you will, based on the next drawn element.

As these are blending modes (ignoring the alpha channel) we will also need to clip the result using composition as a last step.

The color blending mode can be used too but it will alter luma which may or may not be desirable. The difference can be subtle in many cases, but also very obvious depending on target chroma and hue where luma/shadow definition is lost.

So, to achieve a good quality result preserving both luma and chroma, these are more or less the main steps (assumes an empty canvas):

// step 1: draw in original image
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(img, 0, 0);

// step 2: adjust saturation (chroma, intensity)
ctx.globalCompositeOperation = "saturation";
ctx.fillStyle = "hsl(0," + sat + "%, 50%)";  // hue doesn't matter here
ctx.fillRect(0, 0);

// step 3: adjust hue, preserve luma and chroma
ctx.globalCompositeOperation = "hue";
ctx.fillStyle = "hsl(" + hue + ",1%, 50%)";  // sat must be > 0, otherwise won't matter
ctx.fillRect(0, 0, c.width, c.height);

// step 4: in our case, we need to clip as we filled the entire area
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(img, 0, 0);

// step 5: reset comp mode to default
ctx.globalCompositeOperation = "source-over";

50% lightness (L) will keep the original luma value.

Live Example

Click the checkbox to see the effect on the result. Then test with different chroma and hue settings.

var ctx = c.getContext("2d");
var img = new Image(); img.onload = demo; img.src = "//i.stack.imgur.com/Kk1qd.png";
function demo() {c.width = this.width>>1; c.height = this.height>>1; render()}

function render() {
  var hue = +rHue.value, sat = +rSat.value, l = +rL.value;
  
  ctx.clearRect(0, 0, c.width, c.height);
  ctx.globalCompositeOperation = "source-over";
  ctx.drawImage(img, 0, 0, c.width, c.height);

  if (!!cColor.checked) {
    // use color blending mode
    ctx.globalCompositeOperation = "color";
    ctx.fillStyle = "hsl(" + hue + "," + sat + "%, 50%)";
    ctx.fillRect(0, 0, c.width, c.height);
  }
  else {
    // adjust "lightness"
    ctx.globalCompositeOperation = l < 100 ? "color-burn" : "color-dodge";
    // for common slider, to produce a valid value for both directions
    l = l >= 100 ? l - 100 : 100 - (100 - l);
    ctx.fillStyle = "hsl(0, 50%, " + l + "%)";
    ctx.fillRect(0, 0, c.width, c.height);
    
    // adjust saturation
    ctx.globalCompositeOperation = "saturation";
    ctx.fillStyle = "hsl(0," + sat + "%, 50%)";
    ctx.fillRect(0, 0, c.width, c.height);

    // adjust hue
    ctx.globalCompositeOperation = "hue";
    ctx.fillStyle = "hsl(" + hue + ",1%, 50%)";
    ctx.fillRect(0, 0, c.width, c.height);
  }
  
  // clip
  ctx.globalCompositeOperation = "destination-in";
  ctx.drawImage(img, 0, 0, c.width, c.height);

  // reset comp. mode to default
  ctx.globalCompositeOperation = "source-over";
}

rHue.oninput = rSat.oninput = rL.oninput = cColor.onchange = render;
body {font:16px sans-serif}
<div>
  <label>Hue: <input type=range id=rHue max=359 value=0></label>
  <label>Saturation: <input type=range id=rSat value=100></label>
  <label>Lightness: <input type=range id=rL max=200 value=100></label>
  <label>Use "color" instead: <input type=checkbox id=cColor></label>
</div>
<canvas id=c></canvas>
Community
  • 1
  • 1
  • Yes it works but I have another issue with some colors for example if I choose the HSL code of a color for example (white hsl(0, 0%, 100%)) i don't get the white color but a way different color. How can i deal with this ? Thank you K3N – M.Chaymae Jul 20 '17 at 08:13
  • @M.Chaymae yeah, it's a problem with the HSL model. If there is no chroma data (ie. sat=0) it won't work. However, you can get around using lightness (L) combined with color-dodge blending mode for white t's, and color-burn when you need black t's. I updated the answer. –  Jul 20 '17 at 15:49
  • i have another question, your code worked perfectly however i found a problem when i try to draw multiple image on the same canvas it looks like this https://ibb.co/dCgpjk. How can i remove those gray rectangles in your code. Once again Thank you so much for your help ! – M.Chaymae Aug 02 '17 at 16:12
  • @M.Chaymae you can use the clipping step (the last step in the code above, see code below the comment `// clip`). It is assumed of course that the image being used is having a alpha-channel (transparent background). –  Aug 02 '17 at 16:54
  • I did as u told me but I have a problem ... I have multiple image on the same canvas so one of these images appears and the others no but this time without the gray rectangle .. I don't know it doesnt appear like the image but without the gray rectangles – M.Chaymae Aug 02 '17 at 18:07
  • @M.Chaymae are you remembering to set back composite mode to "source-over" after doing the clip? (I will add this to the answer) You could also use an off-screen canvas for each individual element and merge all to the main canvas. –  Aug 02 '17 at 19:01
  • @M.Chaymae I suggest you open a new question with the additional steps you're using. Blending modes are a bit different than composition as it only works on the front layer, not alpha channel. But we need to see the additional steps to determine where the problems are. It's a bit out of scope for the current question. –  Aug 03 '17 at 10:50
  • I did as you told and still no answers I don't know what to do? – M.Chaymae Aug 05 '17 at 17:36
6

Global composite operations

The 2D context property ctx.globalCompositeOperation is very useful for a wide range of image processing tasks. For more on globalCompositeOperation at MDN

You can convert the image into a canvas, that way you can edit it.

function imageToCanvas(image){
    const c = document.createElement("canvas");
    c.width = image.width;
    c.height = image.height;
    c.ctx = c.getContext("2d"); // attach context to the canvas for eaasy reference
    c.ctx.drawImage(image,0,0);
    return c;
}

You can use the globalCompositeOperation = "color" to colour the image

function colorImage(image,color){ // image is a canvas image
     image.ctx.fillStyle = color;
     image.ctx.globalCompositeOperation = "color";
     image.ctx.fillRect(0,0,image.width,image.height);
     image.ctx.globalCompositeOperation = "source-over";
     return image;
}

Unfortunately this also overwrites the alpha pixels so you need to use the original image as a mask to restore the alpha pixels.

function maskImage(dest,source){
     dest.ctx.globalCompositeOperation = "destination-in";
     dest.ctx.drawImage(source,0,0);
     dest.ctx.globalCompositeOperation = "source-over";
     return dest;
}

And then you have a coloured image

Example.

In he example I colour the image in a range of colours and added a function to restore the canvas copy of the image back to the original. If you get the image from the page as an element then use naturalWidth and naturalHeight as the width and height properties may not match the image resolution.

const ctx = canvas.getContext("2d");
const image = new Image;
var colCopy;
image.src = "https://i.stack.imgur.com/Kk1qd.png";
image.onload = () => {
  colCopy = imageToCanvas(image);
  const scale = canvas.height / image.naturalHeight; 
  ctx.scale(scale, scale);
  ctx.drawImage(colCopy, 0, 0);
  for (var i = 32; i < 360; i += 32) {
    restoreImage(colCopy, image);
    colorImage(colCopy, "hsl(" + i + ",100%,50%)");
    maskImage(colCopy, image);
    ctx.drawImage(colCopy, 150 * i / 16, 0);
  }
}



function imageToCanvas(image) {
  const c = document.createElement("canvas");
  c.width = image.naturalWidth;
  c.height = image.naturalHeight;
  c.ctx = c.getContext("2d"); // attach context to the canvas for easy reference
  c.ctx.drawImage(image, 0, 0);
  return c;
}

function restoreImage(dest, source) {
  dest.ctx.clearRect(0, 0, dest.width, dest.height);
  dest.ctx.drawImage(source, 0, 0);
  return dest;
}

function colorImage(dest, color) { // image is a canvas image
  dest.ctx.fillStyle = color;
  dest.ctx.globalCompositeOperation = "color";
  dest.ctx.fillRect(0, 0, dest.width, dest.height);
  dest.ctx.globalCompositeOperation = "source-over";
  return dest;
}

function maskImage(dest, source) {
  dest.ctx.globalCompositeOperation = "destination-in";
  dest.ctx.drawImage(source, 0, 0);
  dest.ctx.globalCompositeOperation = "source-over";
  return dest;
}
canvas {
  border: 2px solid black;
}
<canvas id="canvas" width=600></canvas>

The image can get a little washed out in some situations, you can convert the image to a higher contrast black and white image using composite operations similar to shown above, and use the high contrast image as the template to colour.

Using Filters

Most of the common browsers now support canvas filters which has a hue shift filter. You can use that to shift the hue to the value you want, though first you will need to know what the image original hue is. (see below example on how to find HUE)

See Canvas filters at MDN for compatibility and how to use canvas filters.

The following function will preserve the saturation and just shift the hue.

// dest canvas to hold the resulting image
// source the original image
// hue The hue to set the dest image to
// sourceHue the hue reference point of the original image.
function colorImage(dest,source, hue , sourceHue) { // image is a canvas image
  dest.ctx.clearRect(0,0,dest.width, dest.height);
  dest.ctx.filter="hue-rotate("+((hue - sourceHue) | 0)+"deg)";
  dest.ctx.drawImage(source,0, 0, dest.width, dest.height);
  return dest;
}

Filters example.

The following uses ctx.filter = "hue-rotate(30deg)" to rotate the hue. I have not included any code to find the image original hue so manually set it by eye to 120.

const ctx = canvas.getContext("2d");
const image = new Image;
var colCopy;
const sourceHue = 120;
image.src = "https://i.stack.imgur.com/Kk1qd.png";
image.onload = () => {
  colCopy = imageToCanvas(image);
  
  const scale = canvas.height / image.naturalHeight; 
  ctx.scale(scale, scale);
  ctx.drawImage(colCopy, 0, 0);
  for (var i = 32; i < 360; i += 32) {
    colorImage(colCopy,image,i,sourceHue);
    ctx.drawImage(colCopy, 150 * i / 16, 0);
  }
}



function imageToCanvas(image) {
  const c = document.createElement("canvas");
  c.width = image.naturalWidth;
  c.height = image.naturalHeight;
  c.ctx = c.getContext("2d"); // attach context to the canvas for easy reference
  c.ctx.drawImage(image, 0, 0);
  return c;
}

function colorImage(dest,source, hueRotate , sourceHue) { // image is a canvas image
  dest.ctx.clearRect(0,0,dest.width, dest.height);
  dest.ctx.filter="hue-rotate("+((hueRotate - sourceHue) | 0)+"deg)";
  dest.ctx.drawImage(source,0, 0, dest.width, dest.height);
  return dest;
}
canvas {
  border: 2px solid black;
}
<canvas id="canvas" width=600></canvas>

RGB to Hue

There are plenty of answers to help find the hue of a pixel here on SO. Here is a particularly detailed one RGB to HSL conversion.

Filters example White.

The following uses ctx.filter = "grayscale(100%)" to remove saturation and then ctx.filter = "brightness(amount%)" to change the brightness. This gives a range of gray colours from black to white. You can also do the same with the colour, by reducing the grayscale amount.

const ctx = canvas.getContext("2d");
const image = new Image;
var colCopy;
const sourceHue = 120;
image.src = "https://i.stack.imgur.com/Kk1qd.png";
image.onload = () => {
  colCopy = imageToCanvas(image);
  
  const scale = canvas.height / image.naturalHeight; 
  ctx.scale(scale, scale);
  ctx.drawImage(colCopy, 0, 0);
  for (var i = 40; i < 240; i += 20) {
    grayImage(colCopy,image,i);
    ctx.drawImage(colCopy, 150 * ((i-40) / 12), 0);
  }
}



function imageToCanvas(image) {
  const c = document.createElement("canvas");
  c.width = image.naturalWidth;
  c.height = image.naturalHeight;
  c.ctx = c.getContext("2d"); // attach context to the canvas for easy reference
  c.ctx.drawImage(image, 0, 0);
  return c;
}

function grayImage(dest,source, brightness) { // image is a canvas image
  dest.ctx.clearRect(0,0,dest.width, dest.height);
  dest.ctx.filter = "grayscale(100%)";
  dest.ctx.drawImage(source,0, 0, dest.width, dest.height);
  dest.ctx.filter = "brightness(" + brightness +"%)";
  dest.ctx.drawImage(dest,0, 0, dest.width, dest.height);

  return dest;
}
canvas {
  border: 2px solid black;
}
<canvas id="canvas" width=600></canvas>
halfer
  • 18,701
  • 13
  • 79
  • 158
Blindman67
  • 41,565
  • 7
  • 47
  • 102
  • do you have any idea howa can use this code for multiple images ? I want to add more images to my canvas and color them all in once – M.Chaymae Aug 05 '17 at 17:55
0

You can combine filters on a single line of code before performing your draw operation, like this:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const image = document.getElementById('source');

ctx.filter = 'hue-rotate(120deg) grayscale(10%) brightness(150%)';
ctx.drawImage(image, 10, 10, 180, 120);
<canvas id="canvas"></canvas>
<div style="display:none;">
  <img id="source"
       src="https://interactive-examples.mdn.mozilla.net/media/examples/gecko-320-213.jpg">
</div>
Mark Miller
  • 985
  • 8
  • 13