Bitmap, Sprite y SpriteSheet

20/03/2016 CreateJS

Imágenes con Bitmap

En el diseño de aplicaciones gráficas es obligación implementar herramientas enfocadas en la manipulación y presentación de imágenes, por esta razón ya es hora de presentar a Bitmap, una clase que nos permitirá cargar imágenes propias sin dificultades.

Bitmap es una clase muy simple, cuya única función consiste en presentar una imagen que tengamos guardada en nuestro directorio de ficheros. Para inicializar un objeto Bitmap, bastará con:

var bitmap = new createjs.Bitmap("path/image.png");

De este modo, podremos cargar una imagen. Seguidamente podremos agregarlo al DisplayList y tratarlo como cualquier otro objeto que ya conocemos.

                
                    canvas = document.getElementById('canvas');
                    stage = new createjs.Stage(canvas);
                    bitmap = new createjs.Bitmap('images/tutorials/createjs/butterfly');
                    stage.addChild(bitmap);
                
            

Sprite y Spritesheet

Con Bitmap conseguimos imágenes estáticas; en este apartado presentaré a las clases Sprite y SpriteSheet, clases con las cuales podremos hacer uso de una secuencia de imágenes o 'frames' para crear animaciones.

La clase Sprite es una de la más interesantes de CreateJS y es con la que verdaderamente enriqueceremos nuestras aplicaciones, sin embargo, no podremos instanciar un Sprite sin antes haber creado una instancia de SpriteSheet.

la clase SpriteSheet es una clase lógica, y solo contendrá información sobre un SpriteSheet de nuestro directorio de ficheros, es decir, contendrá información sobre cuántas animaciones hay, cuántos frames tiene, qué velocidad, si tiene animaciones loopeadas, etc.

De modo que, lo primero que deberemos hacer es crear un SpriteSheet. En este ejemplo usaremos la siguiente imagen:

Vemos que este SpriteSheet posee dos filas, cada una corresponde con una animación distinta. La primera fila es una animación de 'idle' (stand by - estar parado), y la segunda fila corresponde con una animación de caminar. Deberemos observar con detenimiento esa imagen para sacar toda la información necesaria.

Esta información la podemos escribir de dos formas: como un objeto Javascript o como un JSON. En este ejemplo, lo escribiremos todo en Javascript. Más adelante conoceremos una herramienta que automáticamente obtiene esta información en formato JSON ;)

Construyendo un SpriteSheet

Con la imagen de arriba procederemos a construir un SpriteSheet. El constructor de esta clase es:

var spritesheet = new createjs.SpriteSheet(data) // donde data es un objeto con toda la información

La información que debe tener el objeto 'data' será la siguiente:

  • images: Array con los path de las imágenes
  • frames: Objeto con información sobre cada frame (sub-imagen del spritesheet). Esta información se puede suministrar de dos formas:
    1. Objeto: Si todos los frames son iguales
      {width, height, count, regX, regY, spacing, margin}
    2. Array: Si hay que definir cada frame individualmente
      [x, y, width, height, index, regX, regY] // De estas propiedades, sólo x, y, width y height son obligatorias
  • Animations: Objeto opcional. Se puede especificar de varias formas
    1. Animación de 1 frame:
      animations: { walk: 2 } // una sola animación, llamada 'walk' compuesto de un solo frame, el frame número 2
    2. Animaciones de frames consecutivos:
                                      animations: {
                                          run: [0, 8], // animación 'run' desde el frame 0 hasta el 8
                                          jump: [9, 12, "run", 2] // animación 'jump'. Cuando acabe su ejecución pasará a ejecutar la animación 'run'. El ultimo valor es la velocidad
                                      }
                                  
    3. Animaciones de frames no consecutivos:
                                      animations: {
                                          walk: {
                                              frames: [1,2,3,3,2,1]
                                          },
                                          shoot: {
                                              frames: [1,4,5,6],
                                              next: "walk", // opcional. Especificar la 'siguiente' animación para cuando acabe 'walk'
                                              speed: 0.5 // opcional. Especificar la velocidad o framerate
                                          }
                                      }
                                  
  • Framerate: Número opcional. Para especificar la velocidad general de frames per sec. Esta propiedad sólo funcionará si actualizamos el Stage con el evento 'tick' de la clase Ticker.
    stage.update(event);

Teniendo esto presente, procederemos a crear el objeto Javascript para instanciar un SpriteSheet, y, una vez tengamos el SpriteSheet instanciado, crearemos un Sprite!

                
                    var canvas, stage, spriteSheet, data, sprite;
                    $(document).ready(function(){
                        createjs.Ticker.setFPS(60);
                        createjs.Ticker.on("tick", onTick, this);
                        canvas = document.getElementById('canvas');
                        stage = new createjs.Stage(canvas);
                        data = {
                            framerate: 16,
                            images: ['images/tutorials/createjs/spritesheet'],
                            frames: {width: 64, height: 64},
                            animations: {
                                idle: [0, 10],
                                run: [11, 20]
                            }
                        };
                        spriteSheet = new createjs.SpriteSheet(data); // Creando un SpriteSheet

                        // Ahora podemos crear un Sprite
                        sprite = new createjs.Sprite(spriteSheet, "idle"); //animación por defecto 'idle'
                        stage.addChild(sprite);
                    }

                    var onTick = function(e){
                        if(!e.paused){
                            stage.update(e); //recibe el evento 'tick'. Así podemos usar la propiedad 'framerate' del SpriteSheet
                        }
                    };
                
            

Y ahí lo tenemos! Un Sprite animado con un SpriteSheet ejecutando la animación 'idle', que es la que hemos especificado por defecto. El siguiente paso será hacer que descance y alterne entre sus dos animaciones, run y idle.

Combinando Animaciones

Nuestro SpriteSheet goza de dos animaciones, en este apartado añadiremos algunas líneas de código para hacer que el personaje se mueva o se detenga según la interacción del usuario. Usaremos las teclas de dirección del teclado para alternar entre animaciones (andar o quedarse quieto).

Lo primero que deberemos hacer será detectar cuándo se pulsa alguna de las teclas derecha o izquierda con la función document.addEventListener().

document.addEventListener('keydown', callback) // capturar cuando el usuario pulsa una tecla

La función de callback que deberemos pasarle a document.addEventListener() recibirá un objeto 'event' que contendrá la key code que corresponda con la tecla pulsada. Crearemos un array donde guardaremos todas las teclas pulsadas según su key code.

Esto lo meteremos en una función que llamaremos enableInputs():

                
                    var enableInputs = function(){
                        document.addEventListener('keydown', function(evt) { // Cuando se pulsa la tecla
                            pressing[evt.keyCode] = true;
                        }, false);

                        document.addEventListener('keyup', function(evt) { // Cuando se relaja la tecla
                            pressing[evt.keyCode] = false;
                        }, false);
                    }
                
            

Con las teclas controladas, deberemos informarnos de qué key code nos interesa. En este caso, sólo queremos los key code de las teclas 'dirección derecha' y 'dirección izquierda'. Aquí descubriremos que los códigos son el 37 y el 39. Los guardaremos en sus respectivas variables como constantes:

                KEY_LEFT = 37;
                KEY_RIGHT = 39;
            

Finalmente, escribiremos la lógica necesaria para hacer alternar entre animaciones según la tecla pulsada.

Para cambiar entre animaciones, usaremos la función sprite.gotoAndPlay(anim). Donde 'anim' será el nombre de la animación del SpriteSheet que deseemos reproducir.

También añadiremos un nuevo atributo booleano sprite.moving para saber si está en movimiento. De este modo controlaremos cuándo llamar a gotoAndPlay().

sprite.moving = false // Inicializamos a false, no se estará moviendo hasta que no se pulse alguna tecla

Por último, usaremos un 'truco' para hacerle 'flip' a la animación de caminar. Con CreateJS no será necesario crear nuevos dibujos para andar hacia la otra dirección, si no que bastará con 'reflejar' la animación de andar actual.

Esto lo podremos hacer con sprite.setTransform(x, y, scaleX, scaleY, rotation, skewX, skewY, regX, regY).

El método setTransform() está bastante completito y nos permitirá cambiar todas esas transformaciones. Para lograr el reflejo, deberemos poner como scaleX el valor -1.

sprite.setTransform(sprite.x, sprite.y, -1, 1, 0, 0, 0, sprite.regX, sprite.regY);

Nótese el -1 en la posición de scaleX, el resto de atributos los dejamos invariantes, tal y como se encontrasen.

Entonces, podremos escribir un código como el siguiente:

                
                    if(pressing[KEY_LEFT]){
                        if(!sprite.moving){ // Si no se estaba moviendo, llamamos a su animación 'run'
                            sprite.gotoAndPlay('run');
                            sprite.setTransform (sprite.x, sprite.y, 1, 1, 0, 0, 0, sprite.regX, sprite.regY); // setTransform con valores invariantes
                        }
                        sprite.moving = true; // Para evitar volver a llamar a 'run', puesto que ya la hemos llamado
                    }
                    if(pressing[KEY_RIGHT]){
                        if(!sprite.moving){
                            sprite.gotoAndPlay('run');
                            sprite.setTransform (sprite.x, sprite.y, -1, 1, 0, 0, 0, sprite.regX, sprite.regY); // setTransform con un -1 en scaleX para hacer el 'flip'
                        }
                        sprite.moving = true;
                    }

                    if(!pressing[KEY_LEFT] && !pressing[KEY_RIGHT]){ // Si no se presiona ninguna de estas teclas, llamamos a 'idle'
                        if(sprite.moving){
                            sprite.gotoAndPlay('idle');
                        }
                        sprite.moving = false; // Ha dejado de moverse
                    }
                
            

Podéis notar el uso de sprite.regX y sprite.regY, con la intención de que las 'transformaciones' de setTransform() tomen como punto de referencia el centro del Sprite. Para calcular el centro, la clase Sprite cuenta con un método muy util, getBounds() el cual devuelve un Rectangle, con las propiedades width y height indicando el ancho y el alto del frame.

Así pues, damos los valores correctos con:

                sprite.regX = sprite.getBounds().width / 2;
                sprite.regY = sprite.getBounds().height / 2;
            

Y este es el resultado, un Sprite animado interactivo!

Y aquí dejo el código completo:

                
                    var canvas, stage, sprite, spriteSheet, data, pressing, KEY_LEFT, KEY_RIGHT;

                    $(document).ready(function(){
                        enableInputs();
                        pressing = [];
                        KEY_LEFT = 37;
                        KEY_RIGHT = 39;

                        canvas = document.getElementById('canvas');
                        stage = new createjs.Stage(canvas);

                        data = {
                            framerate: 16,
                            images: ['images/tutorials/createjs/spritesheet'],
                            frames: {width: 64, height: 64},
                            animations: {
                                idle: [0, 10],
                                run: [11, 20]
                            }
                        };
                        spriteSheet = new createjs.SpriteSheet(data);
                        sprite = new createjs.Sprite(spriteSheet, "idle");

                        sprite.moving = false;
                        sprite.regX = sprite.getBounds().width / 2; 
                        sprite.regY = sprite.getBounds().height / 2;
                        sprite.x = canvas.width / 2;
                        sprite.y = canvas.height / 2;
                    
                        stage.addChild(sprite);

                        createjs.Ticker.setFPS(60);
                        createjs.Ticker.on("tick", onTick, this);
                    }

                    var onTick = function(e){
                        if(!e.paused){
                            if(pressing[KEY_LEFT]){
                                if(!sprite.moving){
                                    sprite.gotoAndPlay('run');
                                    sprite.setTransform (sprite.x, sprite.y, 1, 1, 0, 0, 0, sprite.regX, sprite.regY); // setTransform con valores invariantes
                                }
                                sprite.moving = true;
                            }
                            if(pressing[KEY_RIGHT]){
                                if(!sprite.moving){
                                    sprite.gotoAndPlay('run');
                                    sprite.setTransform (sprite.x, sprite.y, -1, 1, 0, 0, 0, sprite.regX, sprite.regY); // setTransform con un -1 en scaleX para hacer el 'flip'
                                }
                                sprite.moving = true;
                            }

                            if(!pressing[KEY_LEFT] && !pressing[KEY_RIGHT]){ // Si no se presiona ninguna de estas teclas, llamamos a 'idle'
                                if(sprite.moving){
                                sprite.gotoAndPlay('idle');
                                }
                                sprite.moving = false;
                            }

                            stage.update(e);
                        }
                    };                    

                    var enableInputs = function(){
                        document.addEventListener('keydown', function(evt) { // Cuando se pulsa la tecla
                            pressing[evt.keyCode] = true;
                        });
    
                        document.addEventListener('keyup', function(evt) { // Cuando se relaja la tecla
                            pressing[evt.keyCode] = false;
                        });
                    }                   
                
            

Y hasta aquí la introducción a Sprite y SpriteSheet, puede parecer mucho código, pero no lo es tanto. Existen herramientas que automatizan la generación del objeto 'data' para el SpriteSheet, además, tampoco hemos modularizado el código lo cual hace que parezca muy engorroso. De esto hablaremos en un futuro ;)

Como deberes, podeís añadir algunas líneas de código para hacer que se desplaze, actualizando sus valores de sprite.x como hemos visto en anteriores capítulos.

Si tienes alguna duda o sugerencia, no dudes en participar!