32

My question is, what is the best way to tint an image that is drawn using the drawImage method. The target useage for this is advanced 2d particle-effects (game development) where particles change colors over time etc. I am not asking how to tint the whole canvas, only the current image i am about to draw.

I have concluded that the globalAlpha parameter affects the current image that is drawn.

//works with drawImage()
canvas2d.globalAlpha = 0.5;

But how do i tint each image with an arbitrary color value ? It would be awesome if there was some kind of globalFillStyle or globalColor or that kind of thing...

EDIT:

Here is a screenshot of the application i am working with: http://twitpic.com/1j2aeg/full alt text http://web20.twitpic.com/img/92485672-1d59e2f85d099210d4dafb5211bf770f.4bd804ef-scaled.png

djdolber
  • 3,966
  • 2
  • 18
  • 9

6 Answers6

34

You have compositing operations, and one of them is destination-atop. If you composite an image onto a solid color with the 'context.globalCompositeOperation = "destination-atop"', it will have the alpha of the foreground image, and the color of the background image. I used this to make a fully tinted copy of an image, and then drew that fully tinted copy on top of the original at an opacity equal to the amount that I want to tint.

Here is the full code:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <title>HTML5 Canvas Test</title>
        <script type="text/javascript">
var x; //drawing context
var width;
var height;
var fg;
var buffer

window.onload = function() {
    var drawingCanvas = document.getElementById('myDrawing');
    // Check the element is in the DOM and the browser supports canvas
    if(drawingCanvas && drawingCanvas.getContext) {
        // Initaliase a 2-dimensional drawing context
        x = drawingCanvas.getContext('2d');
        width = x.canvas.width;
        height = x.canvas.height;

        // grey box grid for transparency testing
        x.fillStyle = '#666666';
        x.fillRect(0,0,width,height);
        x.fillStyle = '#AAAAAA';
        var i,j;
        for (i=0; i<100; i++){
            for (j=0; j<100; j++){
                if ((i+j)%2==0){
                    x.fillRect(20*i,20*j,20,20);
                }
            }
        }

        fg = new Image();
        fg.src = 'http://uncc.ath.cx/LayerCake/images/16/3.png';

        // create offscreen buffer, 
        buffer = document.createElement('canvas');
        buffer.width = fg.width;
        buffer.height = fg.height;
        bx = buffer.getContext('2d');

        // fill offscreen buffer with the tint color
        bx.fillStyle = '#FF0000'
        bx.fillRect(0,0,buffer.width,buffer.height);

        // destination atop makes a result with an alpha channel identical to fg, but with all pixels retaining their original color *as far as I can tell*
        bx.globalCompositeOperation = "destination-atop";
        bx.drawImage(fg,0,0);


        // to tint the image, draw it first
        x.drawImage(fg,0,0);

        //then set the global alpha to the amound that you want to tint it, and draw the buffer directly on top of it.
        x.globalAlpha = 0.5;
        x.drawImage(buffer,0,0);
    }
}
        </script>
    </head>
    </body>
        <canvas id="myDrawing" width="770" height="400">
            <p>Your browser doesn't support canvas.</p>
        </canvas>
    </body>
</html>
Nathan
  • 6,095
  • 10
  • 45
  • 54
  • 1
    Does anyone know if this has 'game-like' performance? I guess it's better to create the buffer once and then reuse it? – Dirk Boer Nov 26 '17 at 23:00
5

Unfortunately, there is not simply one value to change similar to openGL or DirectX libraries I've used in the past. However, it's not too much work to create a new buffer canvas and use the available globalCompositeOperation when drawing an image.

// Create a buffer element to draw based on the Image img
const buffer = document.createElement('canvas');
buffer.width = img.width;
buffer.height = img.height;
const btx = buffer.getContext('2d');
    
// First draw your image to the buffer
btx.drawImage(img, 0, 0);
    
// Now we'll multiply a rectangle of your chosen color
btx.fillStyle = '#FF7700';
btx.globalCompositeOperation = 'multiply';
btx.fillRect(0, 0, buffer.width, buffer.height);
    
// Finally, fix masking issues you'll probably incur and optional globalAlpha
btx.globalAlpha = 0.5;
btx.globalCompositeOperation = 'destination-in';
btx.drawImage(img, 0, 0);

You can now use buffer as your first parameter canvas2d.drawImage. Using multiply you'll get literal tint but hue and color may also be to your liking. Also, this is fast enough to wrap in a function for reuse.

SmujMaiku
  • 603
  • 6
  • 10
5

There is a method here you can use to tint images, and it's more accurate then drawing coloured rectangles and faster then working on a pixel-by-pixel basis. A full explanation is in that blog post, including the JS code, but here is a summary of how it works.

First you go through the image you are tinting pixel by pixel, reading out the data and splitting each pixel up into 4 separate components: red, green, blue and black. You write each component to a separate canvas. So now you have 4 (red, green, blue and black) versions of the original image.

When you want to draw a tinted image, you create (or find) an off-screen canvas and draw these components to it. The black is drawn first, and then you need set the globalCompositeOperation of the canvas to 'lighter' so the next components are added to the canvas. The black is also non-transparent.

The next three components are drawn (the red, blue and green images), but their alpha value is based on how much their component makes up the drawing colour. So if the colour is white, then all three are drawn with 1 alpha. If the colour is green, then only the green image is drawn and the other two are skipped. If the colour is orange then you have full alpha on the red, draw green partially transparent and skip the blue.

Now you have a tinted version of your image rendered onto the spare canvas, and you just draw it to where ever you need it on your canvas.

Again the code to do this is in the blog post.

Maximillian Laumeister
  • 19,884
  • 8
  • 59
  • 78
JL235
  • 2,455
  • 2
  • 19
  • 14
  • 3
    Archived version of that link: https://web.archive.org/web/20171014203801/http://www.playmycode.com/blog/2011/06/realtime-image-tinting-on-html5-canvas/ – Slbox Aug 05 '18 at 04:28
4

When I created a particle test I just cached images based on rotation (like 35 rotations), color tint, and alpha and created a wrapper so that they were created automatically. Worked well. Yes there should be some kind of tint operation, but when dealing with software rendering your best bet much like in flash is to cache everything. Particle Example I made for fun

<!DOCTYPE HTML>
<html lang="en">
<head>
<title>Particle Test</title>
<script language="javascript" src="../Vector.js"></script>
<script type="text/javascript">

function Particle(x, y)
{
    this.position = new Vector(x, y);
    this.velocity = new Vector(0.0, 0.0);
    this.force = new Vector(0.0, 0.0);
    this.mass = 1;
    this.alpha = 0;
}

// Canvas
var canvas = null;
var context2D = null;

// Blue Particle Texture
var blueParticleTexture = new Image();
var blueParticleTextureLoaded = false;

var blueParticleTextureAlpha = new Array();

var mousePosition = new Vector();
var mouseDownPosition = new Vector();

// Particles
var particles = new Array();

var center = new Vector(250, 250);

var imageData;

function Initialize()
{
    canvas = document.getElementById('canvas');
    context2D = canvas.getContext('2d');

    for (var createEntity = 0; createEntity < 150; ++createEntity)
    {
        var randomAngle = Math.random() * Math.PI * 2;
        var particle = new Particle(Math.cos(randomAngle) * 250 + 250, Math.sin(randomAngle) * 250 + 250);
        particle.velocity = center.Subtract(particle.position).Normal().Normalize().Multiply(Math.random() * 5 + 2);
        particle.mass = Math.random() * 3 + 0.5;
        particles.push(particle);
    }

    blueParticleTexture.onload = function()
    {
        context2D.drawImage(blueParticleTexture, 0, 0);
        imageData = context2D.getImageData(0, 0, 5, 5);
        var imageDataPixels = imageData.data;
        for (var i = 0; i <= 255; ++i)
        {
            var newImageData = context2D.createImageData(5, 5);
            var pixels = newImageData.data;
            for (var j = 0, n = pixels.length; j < n; j += 4)
            {
                pixels[j] = imageDataPixels[j];
                pixels[j + 1] = imageDataPixels[j + 1];
                pixels[j + 2] = imageDataPixels[j + 2];
                pixels[j + 3] = Math.floor(imageDataPixels[j + 3] * i / 255);
            }
            blueParticleTextureAlpha.push(newImageData);
        }
        blueParticleTextureLoaded = true;
    }
    blueParticleTexture.src = 'blueparticle.png';

    setInterval(Update, 50);
}

function Update()
{
    // Clear the screen
    context2D.clearRect(0, 0, canvas.width, canvas.height);

    for (var i = 0; i < particles.length; ++i)
    {
        var particle = particles[i];

        var v = center.Subtract(particle.position).Normalize().Multiply(0.5);
        particle.force = v;
        particle.velocity.ThisAdd(particle.force.Divide(particle.mass));
        particle.velocity.ThisMultiply(0.98);
        particle.position.ThisAdd(particle.velocity);
        particle.force = new Vector();
        //if (particle.alpha + 5 < 255) particle.alpha += 5;
        if (particle.position.Subtract(center).LengthSquared() < 20 * 20)
        {
            var randomAngle = Math.random() * Math.PI * 2;
            particle.position = new Vector(Math.cos(randomAngle) * 250 + 250, Math.sin(randomAngle) * 250 + 250);
            particle.velocity = center.Subtract(particle.position).Normal().Normalize().Multiply(Math.random() * 5 + 2);
            //particle.alpha = 0;
        }
    }

    if (blueParticleTextureLoaded)
    {
        for (var i = 0; i < particles.length; ++i)
        {
            var particle = particles[i];
            var intensity = Math.min(1, Math.max(0, 1 - Math.abs(particle.position.Subtract(center).Length() - 125) / 125));
            context2D.putImageData(blueParticleTextureAlpha[Math.floor(intensity * 255)], particle.position.X - 2.5, particle.position.Y - 2.5, 0, 0, blueParticleTexture.width, blueParticleTexture.height);
            //context2D.drawImage(blueParticleTexture, particle.position.X - 2.5, particle.position.Y - 2.5);
        }
    }
}

</script>

<body onload="Initialize()" style="background-color:black">
    <canvas id="canvas" width="500" height="500" style="border:2px solid gray;"/>
        <h1>Canvas is not supported in this browser.</h1>
    </canvas>
    <p>No directions</p>
</body>
</html>

where vector.js is just a naive vector object:

// Vector class

// TODO: EXamples
// v0 = v1 * 100 + v3 * 200; 
// v0 = v1.MultiplY(100).Add(v2.MultiplY(200));

// TODO: In the future maYbe implement:
// VectorEval("%1 = %2 * %3 + %4 * %5", v0, v1, 100, v2, 200);

function Vector(X, Y)
{
    /*
    this.__defineGetter__("X", function() { return this.X; });
    this.__defineSetter__("X", function(value) { this.X = value });

    this.__defineGetter__("Y", function() { return this.Y; });
    this.__defineSetter__("Y", function(value) { this.Y = value });
    */

    this.Add = function(v)
    {
        return new Vector(this.X + v.X, this.Y + v.Y);
    }

    this.Subtract = function(v)
    {
        return new Vector(this.X - v.X, this.Y - v.Y);
    }

    this.Multiply = function(s)
    {
        return new Vector(this.X * s, this.Y * s);
    }

    this.Divide = function(s)
    {
        return new Vector(this.X / s, this.Y / s);
    }

    this.ThisAdd = function(v)
    {
        this.X += v.X;
        this.Y += v.Y;
        return this;
    }

    this.ThisSubtract = function(v)
    {
        this.X -= v.X;
        this.Y -= v.Y;
        return this;
    }

    this.ThisMultiply = function(s)
    {
        this.X *= s;
        this.Y *= s;
        return this;
    }

    this.ThisDivide = function(s)
    {
        this.X /= s;
        this.Y /= s;
        return this;
    }

    this.Length = function()
    {
        return Math.sqrt(this.X * this.X + this.Y * this.Y);
    }

    this.LengthSquared = function()
    {
        return this.X * this.X + this.Y * this.Y;
    }

    this.Normal = function()
    {
        return new Vector(-this.Y, this.X);
    }

    this.ThisNormal = function()
    {
        var X = this.X;
        this.X = -this.Y
        this.Y = X;
        return this;
    }

    this.Normalize = function()
    {
        var length = this.Length();
        if(length != 0)
        {
            return new Vector(this.X / length, this.Y / length);
        }
    }

    this.ThisNormalize = function()
    {
        var length = this.Length();
        if (length != 0)
        {
            this.X /= length;
            this.Y /= length;
        }
        return this;
    }

    this.Negate = function()
    {
        return new Vector(-this.X, -this.Y);
    }

    this.ThisNegate = function()
    {
        this.X = -this.X;
        this.Y = -this.Y;
        return this;
    }

    this.Compare = function(v)
    {
        return Math.abs(this.X - v.X) < 0.0001 && Math.abs(this.Y - v.Y) < 0.0001;
    }

    this.Dot = function(v)
    {
        return this.X * v.X + this.Y * v.Y;
    }

    this.Cross = function(v)
    {
        return this.X * v.Y - this.Y * v.X;
    }

    this.Projection = function(v)
    {
        return this.MultiplY(v, (this.X * v.X + this.Y * v.Y) / (v.X * v.X + v.Y * v.Y));
    }

    this.ThisProjection = function(v)
    {
        var temp = (this.X * v.X + this.Y * v.Y) / (v.X * v.X + v.Y * v.Y);
        this.X = v.X * temp;
        this.Y = v.Y * temp;
        return this;
    }

    // If X and Y aren't supplied, default them to zero
    if (X == undefined) this.X = 0; else this.X = X;
    if (Y == undefined) this.Y = 0; else this.Y = Y;
}
/*
Object.definePropertY(Vector, "X", {get : function(){ return X; },
                               set : function(value){ X = value; },
                               enumerable : true,
                               configurable : true});
Object.definePropertY(Vector, "Y", {get : function(){ return X; },
                               set : function(value){ X = value; },
                               enumerable : true,
                               configurable : true});
*/
Sirisian
  • 417
  • 6
  • 21
  • Thank you for the answer, it seems like a fair solution if you want good performance right now, but i am looking for the current best way to tint the particles on the fly without cacheing any graphics data. – djdolber Apr 22 '10 at 09:15
  • I will take a look at how you tint your graphics data and try that solution for the moment but i am hoping for a better solution that is "build in" so to speak. – djdolber Apr 22 '10 at 09:17
  • Allthough, i will use your tinting method but for realtime tinting. – djdolber Apr 22 '10 at 09:17
  • I concluded that i probably cant use your method since im dealing with semitransparent image data, it would probably be a lot of work getting transparency data to the image correctly since i assume the image data is merged with backgound color data as you draw it on to the canvas, so you lose the transparency data. – djdolber Apr 22 '10 at 22:21
  • Another broken link on so. Another one bites the dust. In all seriousness, you should find your code and copy it *into* your answer as a **JavaScript/HTML/CSS Snippet** so that people can actually use your answer half a decade (or more later), and at the very least, it should be a code **Code Sample**. cheers – dylnmc Jun 15 '17 at 16:44
  • Fixed. @dylnmc So sorry. I recently migrated hosting and never connected an old DNS redirect up. – Sirisian Jun 15 '17 at 21:41
  • Hey, no worries! I'm just glad that you fixed it. I was suspecting (since this is such an old post) that you may not be on any more or that the code had slipped into the netherworld! Thanks so much for copying the code here so that others can see it! :) Cool particle example, by the way! – dylnmc Jun 16 '17 at 13:32
2

This question still stands. The solution some seem to be suggesting is drawing the image to be tinted onto another canvas and from there grabbing the ImageData object to be able to modify it pixel by pixel, the problem with this is that it is not really acceptable in a game development context because i basically will have to draw each particle 2 times instead of 1. A solution i am about to try is to draw each particle once on a canvas and grabbing the ImageData object, before the actual application starts, and then work with the ImageData object instead of the actual Image object but it might prove kind of costly to create new copies since i will have to keep an unmodified original ImageData object for each graphic.

djdolber
  • 3,966
  • 2
  • 18
  • 9
1

I would take a look at this: http://www.arahaya.com/canvasscript3/examples/ he seems to have a ColorTransform method, I believe he is drawing a shape to do the transform but perhaps based on this you can find a way to adjust a specific image.

cjgammon
  • 41
  • 6