Reputation: 41
Is it possible to add a color filter to a black and white image?
So for example, lets say I want to use this image as a particle in a game:particle image
Is it possible for me to draw this image with some sort of filter so it can become whatever color i'd like?
Similar to how you can color in a rectangle with whatever color you want:
ctx.fillstyle = "rgb(255, 0, 0)";
ctx.fillrect(0, 0, 100, 100);
is there any way for me to dynamically add a color filter over an image in this way?
(I only want to apply the color to the image not the whole canvas)
Upvotes: 0
Views: 2468
Reputation: 1
This question is a couple years old, but I was trying to do this same thing, but none of the posted answers here quite worked as expected, so I came up with a solution that did. Piggybacking off of Blindman67's answer, which is good, but more complicated than necessary with the 4 filter channels, and the resulting color/opacity isn't quite right if you're using a color with an alpha value less than 1.
For an equivalent, but simpler solution:
const srcUrl = 'https://i.sstatic.net/XFpL3.png';
const color = '#FF0000';
const drawColoredImage = (ctx, img, rgb, width, height) => {
// Draw original image
ctx.globalCompositeOperation = 'source-over';
ctx.drawImage(img, 0, 0, width, height);
// Create colored mask
ctx.globalCompositeOperation = 'source-in';
ctx.fillStyle = rgb;
ctx.fillRect(0, 0, width, height);
// Multiply mask by original image to get final colored image
ctx.globalCompositeOperation = 'multiply';
ctx.drawImage(img, 0, 0, width, height);
};
const canvas = document.createElement('canvas');
document.body.appendChild(canvas); // Add canvas to DOM somewhere
const ctx = canvas.getContext('2d');
// Using a CSS hex color #RRGGBB or #RRGGBBAA
// Remove alpha if present and store in separate variable
const rgb = color.substring(0, 7);
const a = color.length === 9
? parseInt(color[7] + color[8], 16)
: 255;
const alpha = a / 255; // alpha value between 0 and 1
const img = new Image;
img.src = srcUrl;
img.addEventListener('load', () => {
const width = img.width;
const height = img.height;
Object.assign(canvas, {
width,
height,
});
ctx.globalAlpha = alpha;
drawColoredImage(ctx, img, rgb, width, height);
});
Now if you're using a color with an alpha value less than 1 and you want the color/opacity to be exact, you need to make a small modification and add one more step. First draw the colored image to a new, temporary canvas with an alpha of 1, then copy that image to your target canvas with the calculated alpha:
img.addEventListener('load', () => {
const width = img.width;
const height = img.height;
Object.assign(canvas, {
width,
height,
});
// Draw colored image to a temp canvas with default alpha of 1
const canvas2 = document.createElement('canvas');
Object.assign(canvas2, {
width,
height,
});
const ctx2 = canvas2.getContext('2d');
drawColoredImage(ctx2, img, rgb, width, height);
// Copy colored image to target canvas with calculated alpha
ctx.globalAlpha = alpha;
ctx.drawImage(canvas2, 0, 0, width, height);
});
Depending on your use case, you may not notice or care about the difference between the two methods, but if you have, say a white icon png, and you want to change the color to exactly match your #RRGGBBAA, use the modified method.
Upvotes: 0
Reputation: 54026
I miss read the question and gave an answer that may not suit your needs. Thus this update demonstrates how to color BW images for particle FX.
Unfortunately the 2D API is not designed with game rendering in mind and for the best results you would implement the rendering via WebGL giving you a huge variety of FX and a massive performance gain. However WebGL requires a lot of code and a good understanding of shaders.
NOTE the image you provided is black. When coloring a black and white image the black will still be black. I will thus assume the image you want colored is white. I will be using a copy of your image that has set the Alpha range from 0 - 255 and the RGB have a circular gradient from black to white.
In the game world an image rendered to the display is called a sprite. A sprite is a rectangular image, It has 4 channels RGBA and can be drawn at any position, rotation, scale, and fade (Alpha)
An example of a basic sprite rendering solution using the 2D API.
To color individual sprites you have two options.
ctx.filter
You could use the ctx.filter property and create a custom filter to color sprites.
However this is not optimal as filters are rather slow when compared to alternative solutions.
To draw a sprite in any color you will need to split the image into its red green blue and alpha parts. Then you render each channel depending on the color you want it to display.
To avoid security limits you can split the image using the following functions
Creates a copy of the image as a canvas
function copyImage(img) { // img can be any image type
const can = Object.assign(document.createElement("canvas"), {
width: img.width,
height: img.height,
});
can.ctx = can.getContext("2d");
can.ctx.drawImage(img, 0, 0, img.width, img.height);
return can;
}
This will copy an image and remove two or three RGBA color channels. It requires the function copyImage
.
function imageFilterChannel(img, channel = "red") {
const imgf = copyImage(img);
// remove unwanted channel data
imgf.ctx.globalCompositeOperation = "multiply";
imgf.ctx.fillStyle = imageFilterChannel.filters[channel] ?? "#FFF";
imgf.ctx.fillRect(0,0, img.width, img.height);
// add alpha mask
imgf.ctx.globalCompositeOperation = "destination-in";
imgf.ctx.drawImage(img, 0, 0, img.width, img.height);
imgf.ctx.globalCompositeOperation = "source-over";
return imgf;
}
imageFilterChannel.filters = {
red: "#F00",
green: "#0F0",
blue: "#00F",
alpha: "#000",
};
This function will load an image, create the 4 separate channel images and encapsulate it in a sprite object.
It requires the function imageFilterChannel
.
NOTE there is no error checking
function createColorSprite(srcUrl) {
var W, H;
const img = new Image;
img.src = srcUrl;
const channels = [];
img.addEventListener("load",() => {
channels[0] = imageFilterChannel(img, "red");
channels[1] = imageFilterChannel(img, "green");
channels[2] = imageFilterChannel(img, "blue");
channels[3] = imageFilterChannel(img, "alpha");
API.width = W = img.width;
API.height = H = img.height;
API.ready = true;
}, {once: true});
const API = {
ready: false,
drawColored(ctx, x, y, scale, rot, color) { // color is CSS hex color #RRGGBBAA
// eg #FFFFFFFF
// get RGBA from color string
const r = parseInt(color[1] + color[2], 16);
const g = parseInt(color[3] + color[4], 16);
const b = parseInt(color[5] + color[6], 16);
const a = parseInt(color[7] + color[8], 16);
// Setup location and transformation
const ax = Math.cos(rot) * scale;
const ay = Math.sin(rot) * scale;
ctx.setTransform(ax, ay, -ay, ax, x, y);
const offX = -W / 2;
const offY = -H / 2;
// draw alpha first then RGB
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = a / 255;
ctx.drawImage(channels[3], offX, offY, W, H);
ctx.globalCompositeOperation = "lighter";
ctx.globalAlpha = r / 255;
ctx.drawImage(channels[0], offX, offY, W, H);
ctx.globalAlpha = g / 255;
ctx.drawImage(channels[1], offX, offY, W, H);
ctx.globalAlpha = b / 255;
ctx.drawImage(channels[2], offX, offY, W, H);
ctx.globalCompositeOperation = "source-over";
}
};
return API;
}
Used as follows
// to load
const image = createColorSprite("https://i.sstatic.net/RXAVJ.png");
// to render with red
if (image.ready) { image.drawColored(ctx, 100, 100, 1, 0, "#FF0000FF") }
Using the functions above the demo shows how to use them and will give you some feedback in regards to the performance. It uses some of the code from the sprite example linked above.
Demo renders 200 images.
Note that there is some room for performance gains.
function copyImage(img) { // img can be any image type
const can = Object.assign(document.createElement("canvas"), {
width: img.width,
height: img.height,
});
can.ctx = can.getContext("2d");
can.ctx.drawImage(img, 0, 0, img.width, img.height);
return can;
}
function imageFilterChannel(img, channel = "red") {
const imgf = copyImage(img);
// remove unwanted channel data
imgf.ctx.globalCompositeOperation = "multiply";
imgf.ctx.fillStyle = imageFilterChannel.filters[channel] ?? "#FFF";
imgf.ctx.fillRect(0,0, img.width, img.height);
// add alpha mask
imgf.ctx.globalCompositeOperation = "destination-in";
imgf.ctx.drawImage(img, 0, 0, img.width, img.height);
imgf.ctx.globalCompositeOperation = "source-over";
return imgf;
}
imageFilterChannel.filters = {
red: "#F00",
green: "#0F0",
blue: "#00F",
alpha: "#000",
};
function createColorSprite(srcUrl) {
var W, H;
const img = new Image;
img.src = srcUrl;
const channels = [];
img.addEventListener("load",() => {
channels[0] = imageFilterChannel(img, "red");
channels[1] = imageFilterChannel(img, "green");
channels[2] = imageFilterChannel(img, "blue");
channels[3] = imageFilterChannel(img, "alpha");
API.width = W = img.width;
API.height = H = img.height;
API.ready = true;
}, {once: true});
const API = {
ready: false,
drawColored(ctx, x, y, scale, rot, color) { // color is CSS hex color #RRGGBBAA
// eg #FFFFFFFF
// get RGBA from color string
const r = parseInt(color[1] + color[2], 16);
const g = parseInt(color[3] + color[4], 16);
const b = parseInt(color[5] + color[6], 16);
const a = parseInt(color[7] + color[8], 16);
// Setup location and transformation
const ax = Math.cos(rot) * scale;
const ay = Math.sin(rot) * scale;
ctx.setTransform(ax, ay, -ay, ax, x, y);
const offX = -W / 2;
const offY = -H / 2;
// draw alpha first then RGB
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = a / 255;
ctx.drawImage(channels[3], offX, offY, W, H);
ctx.globalCompositeOperation = "lighter";
ctx.globalAlpha = r / 255;
ctx.drawImage(channels[0], offX, offY, W, H);
ctx.globalAlpha = g / 255;
ctx.drawImage(channels[1], offX, offY, W, H);
ctx.globalAlpha = b / 255;
ctx.drawImage(channels[2], offX, offY, W, H);
ctx.globalCompositeOperation = "source-over";
}
};
return API;
}
var image = createColorSprite("https://i.sstatic.net/C7qq2.png?s=328&g=1");
var image1 = createColorSprite("https://i.sstatic.net/RXAVJ.png");
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
document.body.appendChild(canvas);
var w,h;
function resize(){ w = canvas.width = innerWidth; h = canvas.height = innerHeight;}
resize();
addEventListener("resize",resize);
function rand(min,max){return Math.random() * (max ?(max-min) : min) + (max ? min : 0) }
const randHex = () => (Math.random() * 255 | 0).toString(16).padStart(2,"0");
const randColRGB = () => "#" + randHex() + randHex() + randHex() + "FF";
function DO(count,callback){ while (count--) { callback(count) } }
const sprites = [];
DO(200,(i)=>{
sprites.push({
x : rand(w), y : rand(h),
xr : 0, yr : 0, // actual position of sprite
r : rand(Math.PI * 2),
scale: rand(0.1,0.25),
dx: rand(-2,2), dy : rand(-2,2),
dr: rand(-0.2,0.2),
color: randColRGB(),
img: i%2 ? image : image1,
});
});
function update(){
var ihM,iwM;
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,w,h);
if(image.ready && image1.ready){
for(var i = 0; i < sprites.length; i ++){
var spr = sprites[i];
iw = spr.img.width;
ih = spr.img.height;
spr.x += spr.dx;
spr.y += spr.dy;
spr.r += spr.dr;
iwM = iw * spr.scale * 2 + w;
ihM = ih * spr.scale * 2 + h;
spr.xr = ((spr.x % iwM) + iwM) % iwM - iw * spr.scale;
spr.yr = ((spr.y % ihM) + ihM) % ihM - ih * spr.scale;
spr.img.drawColored(ctx, spr.xr, spr.yr, spr.scale, spr.r, spr.color);
}
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);
Old answer
After the image is drawn set either ctx.globalCompositeOperation = "color"
, or ctx.globalCompositeOperation = "hue"
and draw over the image in the color you want.
Example
ctx.drawImage(img, 0, 0, 50, 50); // draw image. Ensure that the current
// composite op is
// "source-over" see last line
ctx.globalCompositeOperation = "color"; // set the comp op (can also use "hue")
ctx.fillStyle = "red"; // set fillstyle to color you want
ctx.fillRect(0,0,50,50); // draw red over image
ctx.globalCompositeOperation = "source-over";// restore default comp op
For more info see MDN Global Composite Operations
Upvotes: 2
Reputation: 804
You can use your image as a mask. Use ctx.globalCompositeOperation
after drawing the mask with "source-in"
when you draw the background to "cut" after mask. And "destination-in"
if your mask is drawn after the background.
let color = 'rgb(255, 0, 0)';
let image = new Image();
image.src = 'https://i.sstatic.net/XFpL3.png';
image.onload = () => {
let canvas = document.createElement('canvas');
let w = canvas.width = image.width;
let h = canvas.height = image.height;
let ctx = canvas.getContext('2d');
ctx.fillStyle = color;
ctx.drawImage(image,0,0);
ctx.globalCompositeOperation = 'source-in';
ctx.fillRect(0,0,w,h);
document.body.append(canvas);
}
Upvotes: 1
Reputation: 31987
You can try positioning a layer over the image with a partially transparent background:
.container{
position:relative;
width:fit-content;
}
.layer{
position:absolute;
height:100%;
width:100%;
background-color:rgb(0, 98, 255, 0.5);
z-index:1;
}
<div class="container">
<div class="layer"></div>
<img src="https://i.sstatic.net/XFpL3.png">
</div>
Upvotes: 0