Aprende a crear juegos en HTML5 Canvas

domingo, 15 de diciembre de 2013

Rotación de imágenes

Hay tres técnicas básicas que acompañan a una imagen: Moverla de lugar (Que es lo que hemos estado viendo desde un comienzo), Escalarla (Para hacerla más grande, más chica e invertirla, que podemos hacer con los valores tercero y cuarto de la función  "drawImage") y por último Rotarla. Sin embargo, para conseguir esta última tarea, tenemos que olvidar todo lo que sabemos de las dos primeras.

Rotar una imagen tiene un pequeño grado de complejidad, ya que no existe una función como tal que rote una imagen. Lo que se puede hacer, es rotar el contexto del lienzo, y mediante esta técnica, dibujar una imagen rotada. Esto se hace mediante 6 pasos básicos:

  1. Guardar el formato actual del contexto mediante ctx.save().
  2. Trasladar al centro del lugar donde queremos dibujar nuestra imagen con ctx.translate(x,y).
  3. (Opcional) Escalar el contexto para dibujar la imagen escalada mediante ctx.scale(x,y).
  4. Rotar el contexto para dibujar la imagen rotada con ctx.rotate(rads).
  5. Dibujar la imagen, restando la mitad del ancho y del alto para dibujarla centrada, con ctx.drawImage(img,x,y);
  6. Restaurar nuestro contexto a su forma original con ctx.restore().

Hay tres puntos que quiero resaltar en estos datos. Primero que nada, la rotación se hace en radianes, por lo que si tenemos el ángulo en grados, debemos multiplicarlo por "Math.PI/180".

Después, observen que subrayo "al centro" en la traslación. Esto es porque la imagen rota desde el punto 0,0 donde está el contexto, y si lo hacemos de otra forma, nos resultará en un efecto no deseado; pueden probarlo si gustan para que comprendan a lo que me refiero. Este punto de centrar la imagen puede ser complicado con los rectángulos, pero en los círculos es tan fácil como dar su posición, ya que esta viene desde el centro.

Por último, noten que en el punto 5 indico que hay que restar la mitad del ancho y alto de la imagen. Esto es claro, por efecto del punto anterior, para que la imagen se dibuje de forma centrada.

Ahora que hemos comprendido esto, ¡Llevemos los conocimientos a la práctica! Usando el código de la ocasión pasada, comenzaremos modificando la función "Circle", agregándole dos nuevas variables: scale y rotation.
    function Circle(x,y,radius){
        this.x=(x==null)?0:x;
        this.y=(y==null)?0:y;
        this.radius=(radius==null)?0:radius;
        this.scale=1;
        this.rotation=0;
    }
Creamos también en "Circle" la función "drawImage", que se encargará de cubrir los seis pasos indicados arriba, para dibujar nuestra imagen rotada:
    Circle.prototype.drawImage=function(ctx,img){
        if(img.width){
            ctx.save();
            ctx.translate(this.x,this.y);
            ctx.scale(this.scale,this.scale);
            ctx.rotate(this.rotation*Math.PI/180);
            ctx.drawImage(img,-img.width/2,-img.height/2);
            ctx.restore();
        }
        else
            this.stroke(ctx);
    }
Importamos la imagen que deseamos usar para la rotación:
    var iShip=new Image();
    iShip.src='assets/ship.png';
Y la dibujamos:
        ctx.strokeStyle='#0f0';
        player.drawImage(ctx,iShip);
Para rotar la imagen, usaremos las tecla izquierda y derecha en la función "act":
        // Set Rotation
        if(pressing[KEY_LEFT]){
            player.rotation-=10;
        }
        if(pressing[KEY_RIGHT]){
            player.rotation+=10;
        }
De esta forma, podremos dibujar imágenes que rotan.

Frecuentemente me preguntan "¿Y cómo hago para las colisiones entre objetos que rotan?" A lo que yo les respondo "¡Por eso usamos círculos!". Para explicarme mejor, agrega la siguiente línea en la función "paint":
        if(pressing[KEY_SPACE])
            player.stroke(ctx);
Cuando presiones la tecla espacio, se dibujará el área de colisión de nuestro personaje. Verás que independientemente de su rotación, su área de colisión está siempre a la misma distancia ¡Por eso usamos círculos! Este efecto no es posible con colisiones rectangulares.

Con esto queda cubierto el tema de rotación de imágenes.

Código final:

[Canvas not supported by your browser]
(function(){
    'use strict';
    window.addEventListener('load',init,false);
    var KEY_SPACE=32;
    var KEY_LEFT=37;
    var KEY_RIGHT=39;
    
    var canvas=null,ctx=null;
    var lastPress=null;
    var pressing=[];
    var player=new Circle(150,100,5);
    var iShip=new Image();
    iShip.src='assets/ship.png';

    function init(){
        canvas=document.getElementById('canvas');
        ctx=canvas.getContext('2d');
        canvas.width=300;
        canvas.height=200;
        
        run();
        repaint();
    }

    function run(){
        setTimeout(run,50);
        act(0.05);
    }

    function repaint(){
        requestAnimationFrame(repaint);
        paint(ctx);
    }

    function act(deltaTime){
        // Set Rotation
        if(pressing[KEY_LEFT]){
            player.rotation-=10;
        }
        if(pressing[KEY_RIGHT]){
            player.rotation+=10;
        }
    }

    function paint(ctx){
        ctx.fillStyle='#000';
        ctx.fillRect(0,0,canvas.width,canvas.height);
        
        ctx.strokeStyle='#0f0';
        player.drawImage(ctx,iShip);
        
        if(pressing[KEY_SPACE])
            player.stroke(ctx);
        
        ctx.fillStyle='#fff';
        ctx.fillText('Rotation: '+player.rotation,0,20);
    }

    document.addEventListener('keydown',function(evt){
        lastPress=evt.keyCode;
        pressing[evt.keyCode]=true;
    },false);

    document.addEventListener('keyup',function(evt){
        pressing[evt.keyCode]=false;
    },false);

    function Circle(x,y,radius){
        this.x=(x==null)?0:x;
        this.y=(y==null)?0:y;
        this.radius=(radius==null)?0:radius;
        this.scale=1;
        this.rotation=0;
    }
        
    Circle.prototype.distance=function(circle){
        if(circle!=null){
            var dx=this.x-circle.x;
            var dy=this.y-circle.y;
            return (Math.sqrt(dx*dx+dy*dy)-(this.radius+circle.radius));
        }
    }

    Circle.prototype.stroke=function(ctx){
        ctx.beginPath();
        ctx.arc(this.x,this.y,this.radius,0,Math.PI*2,true);
        ctx.stroke();
    }

    Circle.prototype.drawImage=function(ctx,img){
        if(img.width){
            ctx.save();
            ctx.translate(this.x,this.y);
            ctx.scale(this.scale,this.scale);
            ctx.rotate(this.rotation*Math.PI/180);
            ctx.drawImage(img,-img.width/2,-img.height/2);
            ctx.restore();
        }
        else
            this.stroke(ctx);
    }

    window.requestAnimationFrame=(function(){
        return window.requestAnimationFrame || 
            window.webkitRequestAnimationFrame || 
            window.mozRequestAnimationFrame || 
            function(callback){window.setTimeout(callback,17);};
    })();
})();

11 comentarios:

  1. en caso contrario como se puede evitar una rotación

    ResponderBorrar
  2. muchas gracias otra pregunta si lo quiero es que no rote la pantalla tengo una aplicación pero al mover la pantalla de un dispositivo rota

    ResponderBorrar
    Respuestas
    1. ¿Lo que intentas es bloquear la rotación del navegador en un dispositivo móvil? Actualmente la W3C está trabajando en un estándar para hacer eso desde Javascript o CSS, pero hasta ahora no es posible.

      Lo que puedes hacer, es bloquear la rotación de tu celular en las opciones de tu teléfono, o si lo deseas, puedes mostrar un mensaje a tus usuarios indicando que roten su dispositivo cuando la rotación no es la indicada para tu aplicación.

      Borrar
  3. en caso que quiera que la imagen pudiera moverse de izquierda a derecha, arriba abajo, con las teclas de dirección y hacerla girar con otra tecla, como podría hacerlo?.
    Gracias

    ResponderBorrar
    Respuestas
    1. Precisamente usar lo aprendido antes para desplazarse con las cuatro teclas como dices, y asignar a la rotación las otras dos teclas que deseas usar para la rotación. ¿Cuales tienes pensadas para esta tarea?

      Borrar
  4. Hola que tal, como pordría rotar un rectangulo y ver donde colisiona. Supongo que en un contexto 2d no se puede hacer. Un saludo!

    ResponderBorrar
    Respuestas
    1. Puedes rotar una imagen rectangular visualmente, pero rotar sus colisiones requiere que aprendas sobre colisiones poligonales, que es un tema que aun no aprendemos aquí en el blog. Pero un truco común, es usar colisiones circulares en figuras que rotan, de edtá forma su área de colisión es siempre la misma respecto a su centro, y son pocos los casos donde se requiere de algo más preciso que ello.

      ¿Los rectángulos que tu usas requieren de un nivel de precisión mayor?

      Borrar
  5. gracias por responder! son dos rectangulos haciendo una cruz y queria hacer la colision con otro rectangulo. Sobre las colisiones poligonales eso ya es con alguna libreria verdad?

    ResponderBorrar
    Respuestas
    1. Parece que tu caso si requeriría colisiones poligonales. No es necesario una librería, aunque muchas ya implementan ese método. Igual, hay mucha información en Internet que te puede ayudar a implementar dicha función, solo es cuestión de encontrar una que te permita comprender la forma en que esta trabaja y cómo implementarla. ¡Suerte!

      Borrar
  6. Como puedo hacerlo si necesito que la imagen rote hacia donde esta el mouse?

    ResponderBorrar