Recherche…


Dessiner rapidement de nombreuses images traduites, mises à l'échelle et pivotées

Il existe de nombreuses situations où vous souhaitez dessiner une image qui est pivotée, mise à l'échelle et traduite. La rotation devrait avoir lieu autour du centre de l'image. C'est le moyen le plus rapide de le faire sur la toile 2D. Ces fonctions sont bien adaptées aux jeux en 2D où l'on s'attend à rendre quelques centaines voire plus de 1000 images toutes les 60 secondes. (dépend du matériel)

// assumes that the canvas context is in ctx and in scope
function drawImageRST(image, x, y, scale, rotation){
    ctx.setTransform(scale, 0, 0, scale, x, y); // set the scale and translation
    ctx.rotate(rotation);                       // add the rotation
    ctx.drawImage(image, -image.width / 2, -image.height / 2); // draw the image offset by half its width and height
}

Une variante peut également inclure la valeur alpha qui est utile pour les systèmes de particules.

function drawImageRST_Alpha(image, x, y, scale, rotation, alpha){
    ctx.setTransform(scale, 0, 0, scale, x, y); // set the scale and translation
    ctx.rotate(rotation);                       // add the rotation
    ctx.globalAlpha = alpha;
    ctx.drawImage(image, -image.width / 2, -image.height / 2); // draw the image offset by half its width and height
}

Il est important de noter que les deux fonctions laissent le contexte du canevas dans un état aléatoire. Bien que les fonctions ne seront pas affectées les autres rendant mon être. Lorsque vous avez fini de rendre les images, vous devrez peut-être restaurer la transformation par défaut

ctx.setTransform(1, 0, 0, 1, 0, 0); // set the context transform back to the default 

Si vous utilisez la version alpha (deuxième exemple) et la version standard, vous devrez vous assurer que l'état alpha global est restauré

ctx.globalAlpha = 1;

Un exemple d'utilisation des fonctions ci-dessus pour rendre certaines particules et quelques images

// assume particles to contain an array of particles
for(var i = 0; i < particles.length; i++){
    var p = particles[i];
    drawImageRST_Alpha(p.image, p.x, p.y, p.scale, p.rot, p.alpha);
    // no need to rest the alpha in the loop
}
// you need to reset the alpha as it can be any value 
ctx.globalAlpha = 1;

drawImageRST(myImage, 100, 100, 1, 0.5);  // draw an image at 100,100
// no need to reset the transform 
drawImageRST(myImage, 200, 200, 1, -0.5); // draw an image at 200,200 
ctx.setTransform(1,0,0,1,0,0);            // reset the transform

Faire pivoter une image ou un chemin autour de son centre

entrer la description de l'image ici

Les étapes 1 à 5 ci-dessous permettent de déplacer n'importe quelle image ou forme de chemin n'importe où sur la toile et de la faire pivoter à n'importe quel angle sans modifier les coordonnées du point d'origine de l'image / du chemin.

  1. Déplacer l'origine du canevas [0,0] vers le centre de la forme

    context.translate( shapeCenterX, shapeCenterY );
    
  2. Faire pivoter la toile de l'angle souhaité (en radians)

    context.rotate( radianAngle );
    
  3. Déplacer l'origine du canevas vers le coin supérieur gauche

     context.translate( -shapeCenterX, -shapeCenterY );
    
  4. Dessinez l'image ou la forme du chemin en utilisant ses coordonnées d'origine.

     context.fillRect( shapeX, shapeY, shapeWidth, shapeHeight );
    
  5. Toujours nettoyer! Rétablit l'état de transformation à l'endroit où il était avant # 1

  • Étape n ° 5, option n ° 1: Annulez chaque transformation dans l'ordre inverse

       // undo #3
       context.translate( shapeCenterX, shapeCenterY );
       // undo #2
       context.rotate( -radianAngle );
       // undo #1
       context.translate( -shapeCenterX, shapeCenterY );
    
  • Étape 5, Option 2: Si le canevas était dans un état non transformé (valeur par défaut) avant de commencer l'étape 1, vous pouvez annuler les effets des étapes 1 à 3 en réinitialisant toutes les transformations à leur état par défaut.

       // set transformation to the default state (==no transformation applied)
       context.setTransform(1,0,0,1,0,0)
    

Exemple de démonstration de code:

// canvas references & canvas styling
var canvas=document.createElement("canvas");
canvas.style.border='1px solid red';
document.body.appendChild(canvas);
canvas.width=378;
canvas.height=256;
var ctx=canvas.getContext("2d");
ctx.fillStyle='green';
ctx.globalAlpha=0.35;        

// define a rectangle to rotate
var rect={ x:100, y:100, width:175, height:50 };

// draw the rectangle unrotated
ctx.fillRect( rect.x, rect.y, rect.width, rect.height );

// draw the rectangle rotated by 45 degrees (==PI/4 radians)
ctx.translate( rect.x+rect.width/2, rect.y+rect.height/2 );
ctx.rotate( Math.PI/4 );
ctx.translate( -rect.x-rect.width/2, -rect.y-rect.height/2 );
ctx.fillRect( rect.x, rect.y, rect.width, rect.height );

Introduction aux transformations

Les transformations modifient la position de départ d'un point donné en déplaçant, en tournant et en mettant à l'échelle ce point.

  • Traduction: Déplace un point de distanceX et de distanceY .
  • Rotation: fait pivoter un point par un radian angle autour de son point de rotation. Le point de rotation par défaut dans Canevas HTML est l'origine supérieure gauche [x = 0, y = 0] du canevas. Mais vous pouvez repositionner le point de rotation en utilisant des traductions.
  • Mise à l'échelle: scalingFactorY à l'échelle la position d'un point par un scalingFactorX et un scalingFactorY depuis son point de mise à l'échelle. Le point de mise à l'échelle par défaut dans Canvas HTML est l'origine supérieure gauche [x = 0, y = 0] du canevas. Mais vous pouvez repositionner le point de mise à l'échelle à l'aide de traductions.

Vous pouvez également effectuer des transformations moins courantes, telles que le cisaillement (skewing), en définissant directement la matrice de transformation du canevas à l'aide de context.transform .

Traduire (== déplacer) un point avec context.translate(75,25)

entrer la description de l'image ici

Faire pivoter un point avec context.rotate(Math.PI/8)

entrer la description de l'image ici

Mettre à l'échelle un point avec context.scale(2,2)

entrer la description de l'image ici

Canvas réalise en réalité des transformations en modifiant le système de coordonnées complet de la toile.

  • context.translate déplace l'origine du canevas [0,0] du coin supérieur gauche vers un nouvel emplacement.
  • context.rotate fera pivoter tout le système de coordonnées du canevas autour de l'origine.
  • context.scale mettra à l'échelle le système de coordonnées du canevas entier autour de l'origine. Considérez ceci comme augmentant la taille de chaque x, y sur le canevas: every x*=scaleX et every y*=scaleY .

Les transformations de la toile sont persistantes. Tous les nouveaux dessins continueront à être transformés jusqu'à ce que vous réinitialisiez la transformation du canevas à son état par défaut (== totalement non transformé). Vous pouvez revenir à la valeur par défaut avec:

// reset context transformations to the default (untransformed) state
context.setTransform(1,0,0,1,0,0);

Une matrice de transformation pour suivre les formes traduites, pivotées et mises à l'échelle

Canvas vous permet de context.translate , context.rotate et context.scale afin de dessiner votre forme dans la position et la taille souhaitées.

Canvas lui-même utilise une matrice de transformation pour suivre efficacement les transformations.

  • Vous pouvez changer la matrice de Canvas avec context.transform
  • Vous pouvez changer la matrice de Canvas avec des commandes individuelles de translate, rotate & scale
  • Vous pouvez complètement écraser la matrice de Canvas avec context.setTransform ,
  • Mais vous ne pouvez pas lire la matrice de transformation interne de Canvas - elle est en écriture seule.

Pourquoi utiliser une matrice de transformation?

Une matrice de transformation vous permet d'agréger de nombreuses traductions, rotations et échelles individuelles en une seule matrice facilement réappliquée.

Lors d'animations complexes, vous pouvez appliquer des dizaines (ou des centaines) de transformations à une forme. En utilisant une matrice de transformation, vous pouvez (presque) réappliquer instantanément ces dizaines de transformations avec une seule ligne de code.

Quelques exemples d'utilisation:

  • Tester si la souris est dans une forme que vous avez traduite, tournée et mise à l'échelle

    Il y a un context.isPointInPath intégré qui teste si un point (par exemple la souris) se trouve dans une forme de chemin, mais ce test intégré est très lent comparé au test utilisant une matrice.

    Tester efficacement si la souris est dans une forme implique de prendre la position de la souris signalée par le navigateur et de la transformer de la même manière que la forme a été transformée. Vous pouvez ensuite appliquer le test de frappe comme si la forme n'était pas transformée.

  • Redessinez une forme qui a été largement traduite, pivotée et mise à l'échelle.

    Au lieu de réappliquer des transformations individuelles avec plusieurs .translate, .rotate, .scale vous pouvez appliquer toutes les transformations agrégées sur une seule ligne de code.

  • Formes de test de collision traduites, pivotées et mises à l'échelle

    Vous pouvez utiliser la géométrie et la trigonométrie pour calculer les points constituant les formes transformées, mais il est plus rapide d'utiliser une matrice de transformation pour calculer ces points.

Une matrice de transformation "classe"

Ce code reflète les commandes de transformation context.translate , context.rotate et context.scale natives. Contrairement à la matrice de canevas native, cette matrice est lisible et réutilisable.

Méthodes:

  • translate , rotate , scale les commandes de transformation du contexte et vous permettre d'alimenter les transformations dans la matrice. La matrice contient efficacement les transformations agrégées.

  • setContextTransform prend un contexte et définit la matrice de ce contexte sur cette matrice de transformation. Cela réapplique efficacement toutes les transformations stockées dans cette matrice au contexte.

  • resetContextTransform réinitialise la transformation du contexte à son état par défaut (== non transformé).

  • getTransformedPoint prend un point de coordonnées non transformé et le convertit en un point transformé.

  • getScreenPoint prend un point de coordonnées transformé et le convertit en un point non transformé.

  • getMatrix renvoie les transformations agrégées sous la forme d'un tableau matriciel.

Code:

var TransformationMatrix=( function(){
    // private
    var self;
    var m=[1,0,0,1,0,0];
    var reset=function(){ var m=[1,0,0,1,0,0]; }
    var multiply=function(mat){
        var m0=m[0]*mat[0]+m[2]*mat[1];
        var m1=m[1]*mat[0]+m[3]*mat[1];
        var m2=m[0]*mat[2]+m[2]*mat[3];
        var m3=m[1]*mat[2]+m[3]*mat[3];
        var m4=m[0]*mat[4]+m[2]*mat[5]+m[4];
        var m5=m[1]*mat[4]+m[3]*mat[5]+m[5];
        m=[m0,m1,m2,m3,m4,m5];
    }
    var screenPoint=function(transformedX,transformedY){
        // invert
        var d =1/(m[0]*m[3]-m[1]*m[2]);
        im=[ m[3]*d, -m[1]*d, -m[2]*d, m[0]*d, d*(m[2]*m[5]-m[3]*m[4]), d*(m[1]*m[4]-m[0]*m[5]) ];
        // point
        return({
            x:transformedX*im[0]+transformedY*im[2]+im[4],
            y:transformedX*im[1]+transformedY*im[3]+im[5]
        });
    }
    var transformedPoint=function(screenX,screenY){
        return({
            x:screenX*m[0] + screenY*m[2] + m[4],
            y:screenX*m[1] + screenY*m[3] + m[5]
        });    
    }
    // public
    function TransformationMatrix(){
        self=this;
    }
    // shared methods
    TransformationMatrix.prototype.translate=function(x,y){
        var mat=[ 1, 0, 0, 1, x, y ];
        multiply(mat);
    };
    TransformationMatrix.prototype.rotate=function(rAngle){
        var c = Math.cos(rAngle);
        var s = Math.sin(rAngle);
        var mat=[ c, s, -s, c, 0, 0 ];    
        multiply(mat);
    };
    TransformationMatrix.prototype.scale=function(x,y){
        var mat=[ x, 0, 0, y, 0, 0 ];        
        multiply(mat);
    };
    TransformationMatrix.prototype.skew=function(radianX,radianY){
        var mat=[ 1, Math.tan(radianY), Math.tan(radianX), 1, 0, 0 ];
        multiply(mat);
    };
    TransformationMatrix.prototype.reset=function(){
        reset();
    }
    TransformationMatrix.prototype.setContextTransform=function(ctx){
        ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
    }
    TransformationMatrix.prototype.resetContextTransform=function(ctx){
        ctx.setTransform(1,0,0,1,0,0);
    }
    TransformationMatrix.prototype.getTransformedPoint=function(screenX,screenY){
        return(transformedPoint(screenX,screenY));
    }
    TransformationMatrix.prototype.getScreenPoint=function(transformedX,transformedY){
        return(screenPoint(transformedX,transformedY));
    }
    TransformationMatrix.prototype.getMatrix=function(){
        var clone=[m[0],m[1],m[2],m[3],m[4],m[5]];
        return(clone);
    }
    // return public
    return(TransformationMatrix);
})();

Démo:

Cette démo utilise la matrice de transformation "Class" ci-dessus pour:

  • Suivre (== enregistrer) la matrice de transformation d'un rectangle.

  • Redessinez le rectangle transformé sans utiliser les commandes de transformation de contexte.

  • Testez si la souris a cliqué à l'intérieur du rectangle transformé.

Code:

<!doctype html>
<html>
<head>
<style>
    body{ background-color:white; }
    #canvas{border:1px solid red; }
</style>
<script>
window.onload=(function(){

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
    var cw=canvas.width;
    var ch=canvas.height;
    function reOffset(){
        var BB=canvas.getBoundingClientRect();
        offsetX=BB.left;
        offsetY=BB.top;        
    }
    var offsetX,offsetY;
    reOffset();
    window.onscroll=function(e){ reOffset(); }
    window.onresize=function(e){ reOffset(); }

    // Transformation Matrix "Class"
    
    var TransformationMatrix=( function(){
        // private
        var self;
        var m=[1,0,0,1,0,0];
        var reset=function(){ var m=[1,0,0,1,0,0]; }
        var multiply=function(mat){
            var m0=m[0]*mat[0]+m[2]*mat[1];
            var m1=m[1]*mat[0]+m[3]*mat[1];
            var m2=m[0]*mat[2]+m[2]*mat[3];
            var m3=m[1]*mat[2]+m[3]*mat[3];
            var m4=m[0]*mat[4]+m[2]*mat[5]+m[4];
            var m5=m[1]*mat[4]+m[3]*mat[5]+m[5];
            m=[m0,m1,m2,m3,m4,m5];
        }
        var screenPoint=function(transformedX,transformedY){
            // invert
            var d =1/(m[0]*m[3]-m[1]*m[2]);
            im=[ m[3]*d, -m[1]*d, -m[2]*d, m[0]*d, d*(m[2]*m[5]-m[3]*m[4]), d*(m[1]*m[4]-m[0]*m[5]) ];
            // point
            return({
                x:transformedX*im[0]+transformedY*im[2]+im[4],
                y:transformedX*im[1]+transformedY*im[3]+im[5]
            });
        }
        var transformedPoint=function(screenX,screenY){
            return({
                x:screenX*m[0] + screenY*m[2] + m[4],
                y:screenX*m[1] + screenY*m[3] + m[5]
            });    
        }
        // public
        function TransformationMatrix(){
            self=this;
        }
        // shared methods
        TransformationMatrix.prototype.translate=function(x,y){
            var mat=[ 1, 0, 0, 1, x, y ];
            multiply(mat);
        };
        TransformationMatrix.prototype.rotate=function(rAngle){
            var c = Math.cos(rAngle);
            var s = Math.sin(rAngle);
            var mat=[ c, s, -s, c, 0, 0 ];    
            multiply(mat);
        };
        TransformationMatrix.prototype.scale=function(x,y){
            var mat=[ x, 0, 0, y, 0, 0 ];        
            multiply(mat);
        };
        TransformationMatrix.prototype.skew=function(radianX,radianY){
            var mat=[ 1, Math.tan(radianY), Math.tan(radianX), 1, 0, 0 ];
            multiply(mat);
        };
        TransformationMatrix.prototype.reset=function(){
            reset();
        }
        TransformationMatrix.prototype.setContextTransform=function(ctx){
            ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
        }
        TransformationMatrix.prototype.resetContextTransform=function(ctx){
            ctx.setTransform(1,0,0,1,0,0);
        }
        TransformationMatrix.prototype.getTransformedPoint=function(screenX,screenY){
            return(transformedPoint(screenX,screenY));
        }
        TransformationMatrix.prototype.getScreenPoint=function(transformedX,transformedY){
            return(screenPoint(transformedX,transformedY));
        }
        TransformationMatrix.prototype.getMatrix=function(){
            var clone=[m[0],m[1],m[2],m[3],m[4],m[5]];
            return(clone);
        }
        // return public
        return(TransformationMatrix);
    })();

    // DEMO starts here

    // create a rect and add a transformation matrix
    // to track it's translations, rotations & scalings
    var rect={x:30,y:30,w:50,h:35,matrix:new TransformationMatrix()};

    // draw the untransformed rect in black
    ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
    // Demo: label
    ctx.font='11px arial';
    ctx.fillText('Untransformed Rect',rect.x,rect.y-10);

    // transform the canvas & draw the transformed rect in red
    ctx.translate(100,0);
    ctx.scale(2,2);
    ctx.rotate(Math.PI/12);
    // draw the transformed rect
    ctx.strokeStyle='red';
    ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
    ctx.font='6px arial';
    // Demo: label
    ctx.fillText('Same Rect: Translated, rotated & scaled',rect.x,rect.y-6);
    // reset the context to untransformed state
    ctx.setTransform(1,0,0,1,0,0);

    // record the transformations in the matrix
    var m=rect.matrix;
    m.translate(100,0);
    m.scale(2,2);
    m.rotate(Math.PI/12);

    // use the rect's saved transformation matrix to reposition, 
    //     resize & redraw the rect
    ctx.strokeStyle='blue';
    drawTransformedRect(rect);

    // Demo: instructions
    ctx.font='14px arial';
    ctx.fillText('Demo: click inside the blue rect',30,200);

    // redraw a rect based on it's saved transformation matrix
    function drawTransformedRect(r){
        // set the context transformation matrix using the rect's saved matrix
        m.setContextTransform(ctx);
        // draw the rect (no position or size changes needed!)
        ctx.strokeRect( r.x, r.y, r.w, r.h );
        // reset the context transformation to default (==untransformed);
        m.resetContextTransform(ctx);
    }

    // is the point in the transformed rectangle?
    function isPointInTransformedRect(r,transformedX,transformedY){
        var p=r.matrix.getScreenPoint(transformedX,transformedY);
        var x=p.x;
        var y=p.y;
        return(x>r.x && x<r.x+r.w && y>r.y && y<r.y+r.h);
    } 

    // listen for mousedown events
    canvas.onmousedown=handleMouseDown;
    function handleMouseDown(e){
        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();
        // get mouse position
        mouseX=parseInt(e.clientX-offsetX);
        mouseY=parseInt(e.clientY-offsetY);
        // is the mouse inside the transformed rect?
        if(isPointInTransformedRect(rect,mouseX,mouseY)){
            alert('You clicked in the transformed Rect');
        }
    }

    // Demo: redraw transformed rect without using
    //       context transformation commands
    function drawTransformedRect(r,color){
        var m=r.matrix;
        var tl=m.getTransformedPoint(r.x,r.y);
        var tr=m.getTransformedPoint(r.x+r.w,r.y);
        var br=m.getTransformedPoint(r.x+r.w,r.y+r.h);
        var bl=m.getTransformedPoint(r.x,r.y+r.h);
        ctx.beginPath();
        ctx.moveTo(tl.x,tl.y);
        ctx.lineTo(tr.x,tr.y);
        ctx.lineTo(br.x,br.y);
        ctx.lineTo(bl.x,bl.y);
        ctx.closePath();
        ctx.strokeStyle=color;
        ctx.stroke();
    }

}); // end window.onload
</script>
</head>
<body>
    <canvas id="canvas" width=512 height=250></canvas>
</body>
</html>


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow