28 votes

Comment teinter une image avec HTML5 Canvas ?

Ma question est la suivante : quelle est la meilleure façon de teinter une image dessinée à l'aide de la méthode drawImage ? L'utilisation visée est celle d'effets de particules 2d avancés (développement de jeux) où les particules changent de couleur au fil du temps, etc. Je ne demande pas comment teinter l'ensemble de la toile, mais seulement l'image que je suis sur le point de dessiner.

J'en ai conclu que le paramètre globalAlpha affecte l'image courante qui est dessinée.

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

Mais comment puis-je teinter chaque image avec une valeur de couleur arbitraire ? Ce serait génial s'il y avait une sorte de globalFillStyle ou globalColor ou ce genre de chose...

EDIT :

Voici une capture d'écran de l'application sur laquelle je travaille : http://twitpic.com/1j2aeg/full alt text http://web20.twitpic.com/img/92485672-1d59e2f85d099210d4dafb5211bf770f.4bd804ef-scaled.png

32voto

Nathan Points 2414

Vous avez des opérations de composition, et l'une d'entre elles est destination-atop. Si vous composez une image sur une couleur solide avec 'context.globalCompositeOperation = "destination-atop"', elle aura l'alpha de l'image de premier plan et la couleur de l'image d'arrière-plan. Je l'ai utilisé pour faire une copie entièrement teintée d'une image, puis j'ai dessiné cette copie entièrement teintée par-dessus l'original avec une opacité égale à la quantité de teinte que je voulais.

Voici le code complet :

<!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>

5voto

JL235 Points 659

Il existe une méthode ici que vous pouvez utiliser pour teinter des images, et qui est plus précis que le dessin de rectangles colorés et plus rapide que le travail sur une base pixel par pixel. Vous trouverez une explication complète dans cet article de blog, y compris le code JS, mais voici un résumé de son fonctionnement.

Tout d'abord, vous parcourez l'image à teinter pixel par pixel, en lisant les données et en divisant chaque pixel en 4 composants distincts : rouge, vert, bleu et noir. Vous écrivez chaque composant dans un canevas séparé. Ainsi, vous avez maintenant 4 versions (rouge, vert, bleu et noir) de l'image originale.

Lorsque vous voulez dessiner une image teintée, vous créez (ou trouvez) un canevas hors écran et dessinez ces composants sur celui-ci. Le noir est dessiné en premier, puis vous devez définir l'opération globalCompositeOperation du canevas sur "plus léger" pour que les composants suivants soient ajoutés au canevas. Le noir est également non transparent.

Les trois composantes suivantes sont dessinées (les images rouge, bleue et verte), mais leur valeur alpha est basée sur la part de leur composante dans la couleur du dessin. Ainsi, si la couleur est le blanc, les trois images sont dessinées avec un alpha de 1. Si la couleur est le vert, alors seule l'image verte est dessinée et les deux autres sont ignorées. Si la couleur est l'orange, l'alpha est complet sur le rouge, le vert est partiellement transparent et le bleu est ignoré.

Vous avez maintenant une version teintée de votre image rendue sur la toile de rechange, et il vous suffit de la dessiner là où vous en avez besoin sur votre toile.

Une fois encore, le code pour ce faire se trouve dans l'article du blog.

4voto

ConMan Points 126

Lorsque j'ai créé un test de particules, j'ai simplement mis en cache des images basées sur la rotation (comme 35 rotations), la teinte de la couleur et l'alpha et j'ai créé un wrapper pour qu'elles soient créées automatiquement. Cela a bien fonctionné. Oui, il devrait y avoir une sorte d'opération de teinte, mais lorsqu'il s'agit d'un rendu logiciel, le mieux est de tout mettre en cache, comme dans Flash. Exemple de particule que j'ai fait pour le plaisir

<!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>

où vector.js est juste un objet vectoriel naïf :

// 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});
*/

3voto

SmujMaiku Points 186

Malheureusement, il n'y a pas qu'une seule valeur à modifier, comme dans les bibliothèques openGL ou DirectX que j'ai utilisées dans le passé. Cependant, il n'y a pas trop de travail à faire pour créer un nouveau canevas de tampon et utiliser les valeurs disponibles dans la bibliothèque globalCompositeOperation lors du dessin d'une 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);

Vous pouvez maintenant utiliser buffer comme premier paramètre canvas2d.drawImage . Utilisation de multiplier vous obtiendrez une teinte littérale mais teinte y couleur peut également être à votre goût. En outre, cette méthode est suffisamment rapide pour être intégrée dans une fonction et réutilisée.

2voto

djdolber Points 1281

Cette question reste posée. La solution que certains semblent suggérer consiste à dessiner l'image à teinter sur un autre canevas et, à partir de là, à saisir l'objet ImageData pour pouvoir la modifier pixel par pixel. Le problème est que cette solution n'est pas vraiment acceptable dans le contexte du développement d'un jeu, car je devrai dessiner chaque particule deux fois au lieu d'une. Une solution que je suis sur le point d'essayer est de dessiner chaque particule une fois sur un canevas et de saisir l'objet ImageData, avant le démarrage de l'application proprement dite, puis de travailler avec l'objet ImageData au lieu de l'objet Image proprement dit, mais cela pourrait s'avérer assez coûteux de créer de nouvelles copies puisque je devrai conserver un objet ImageData original non modifié pour chaque graphique.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X