Gráficos II

Gráficos

Resumen: Clase Curve (curva de Bézier), clase Rectangle, clase MorphShape.

Clase Curve.

La clase Curve ubicada en el paquede org.newdawn.slick.geom permite crear una curva de Bézier.
Una curva de Bézier está formada por un punto inicial, por varios puntos de control y por un punto final. De acuerdo a la cantidad de puntos que contiene la curva recibe un nombre, por ejemplo, una curva con 2 puntos se la llama curva de Bézier lineal, una curva con 3 puntos se llama curve de Bézier cuadrática, y así.
La curva debe pasar por el punto inicial y por el punto final. Los puntos de control siven para guiar a la curva pero no necesariamente la curva pasa por ellos.
Existen animaciones sobre cómo se va dibujando una curva de Bézier:



Slick2D brinda la posibidad de crear una curva de Bézier cúbica, es decir, con dos puntos de control.

La clase Curve provee dos constructores:

  • new Curve(Vector2f comienzo, Vector2f primerControl, Vector2f segControl, Vector2f fin).
  • new Curve(Vector2f comienzo, Vector2f primerControl, Vector2f segControl, Vector2f fin, int numSeg).

Ambos contructores permiten crear curvas de Bézier a partir del punto inicial, punto final y los dos puntos de control.
El segundo constructor además permite especificar el número de segmentos por el cual va a estar compuesto la curva, al igual que clase Circle.

Para dibujar una curva de Bézier use el método draw() de la clase Graphics.

La clase Curve contiene solamente 3 métodos públicos:

  • boolean closed(): Retorna siempre false debido a que Slick2D considera que una curva nunca es cerrada.
  • Vector2f pointAt(float t): Devuelve un Vector con las coordenadas del punto que corresponde al parámetro t. Es importante saber que a la curva de Bézier se la puede representar matemáticamente como una ecuación paramétrica y es por ello que este método tiene sentido.
  • Shape transform(Transform transform): Este método permite transformar la curva mediante un objeto Transform. Como ya dije anteriormente, más adelante volveremos sobre la clase Transform.

Clase Rectangle.

La clase Rectangle como su nombre lo indica permite crear un rectángulo. Provee solamente un constructor:

  • new Rectangle(float x, float y, float ancho, float alto).

El constructor recibe como parámetro la esquina superior izquierda (x, y) y también el ancho y alto del rectángulo.

Algunos métodos:

  • boolean contains(float x, float y): Verifica si el punto está o no dentro del rectángulo.
  • float getHeight().
  • float getWidth().
  • void setWidth(float width).
  • void setHeight(float height).
  • void grow(float crecHor, float crecVert): Este método hace crecer al rectángulo tanto como indican los parámetros. Por ejemplo, si el parámetro crecHor (crecimiento horizontal) es igual a 40 entonces el rectángulo crecerá 40 para el lado izquierdo y 40 para el lado derecho, aumentando en 80 el ancho original. Si se usan parámetros negativos entonces el rectángulo se hará más angosto. Aunque no siempre será el caso. Por ejemplo, vea el siguiente código:

rect = new Rectangle(300, 300, 80, 40);
rect.grow(-200, 0);

Observe que el ancho del rectángulo es 80 y le estoy diciendo que crezca -200 de cada lado (es decir, decrecer 200). El rectángulo va a decrecer tanto que luego empezará a crecer terminando con una ancho de 400 - 80 = 320. Eso es porque el lado derecho original se desplazo 200 para la izquierda y el lado izquierdo se desplazo 200 para la derecha, convirtiéndose así el lado derecho en el lado izquiero y viceversa, generando de esta manera un crecimiento en el rectángulo.
  • void scaleGrow(float escalaHor, float escalaVert): Similar al anterior pero se basa en crecimiento relativo al actual, definiendo parámetros de escalas. El regimen de la escala no me parece muy intuitivo así que no vale la pena explicarlo.
  • boolean intersects(Shape shape): Devuelve true si el rectángulo interseca con la figura pasada como parámetro.

Clase MorphShape.

La clase MorphShape ubicada en el paquete org.newdawn.slick.geom provee servicios para animar las figuras. Es realmente muy útil y recomiendo que le prestes mucha atención pues esta clase te permitirá crear animaciones de manera muy sencilla. Aquí es dónde Slick2D empieza a ser muy útil.
Antes de seguir vamos a ver las "partes" de una animación. En primer lugar, tenemos que fijar la parte base o lo inicial de la animación. Luego tenemos que fijar puntos de control. En algún momento, la animación se corresponderá con cada uno de esos puntos de control. Por último tenemos que unir todos esos puntos de control, darle una velocidad de movimiento y listo. Lo dicho antes se resume en la siguiente lista:

1- Crear figura base.
2- Crear figuras de control, pueden ser tantas como se necesite.
3- Unir todos esos puntos de control y ejecutar la animación.

Puede parecer un tanto complicado pero Slick2D nos ofrece la posibilidad de hacerlo de manera muy sencilla.

La clase MorphShape nos provee un sólo constructor:

  • new MorphShape(Shape base).

El constructor recibe la figura de base, es decir, lo inicial de la animación. Luego, tenemos que ir agregando los puntos de control, usando el método addShape(Shape figura). Acá hay algo que notar, al momento de agregar una figura se debe tener en cuenta que sea la misma figura que la figura base. De otra manera Slick2D lanzará un RuntimeException. Cuando digo que sea la misma figura me refiero a que tengan la misma cantidad de vértices, pues esto es lo que le interesa a Slick2D. No importa la posición ni el tamaño de la figura, importa como ya dije, la cantidad de vértices. Para obtener la cantidad de vértices de una figura puede usar el método getPointCount() de la clase Shape. Recuerde que la clase Shape es la clase padre de todas las figuras que vamos viendo.
A continuación se muestra un código de cómo crear un MophShape.

@Override
public void init(GameContainer container) throws SlickException {
// Círculo de base.
Circle circulo1 = new Circle(100, 100, 40);

// Primer punto de control.
Circle circulo2 = new Circle(100, 100, 20);

// Segundo punto de control.
Circle circulo3 = new Circle(100, 100, 10);

// Creación del MorphShape (debe ser un atributo).
morphShape1 = new MorphShape(circulo1);

// Agrego los puntos de control.
morphShape1.addShape(circulo2);
morphShape1.addShape(circulo3);
}

Observe que creamos 3 círculos, de los cuales, uno es el base y los otros 2 son puntos de control. Fíjese que los círculos están ubicados en la misma posición (100, 100) y que sólo cambian los radios. Por lo tanto, la animación mostrará a un círculo cambiando de tamaño pero siempre en la misma posición. Note además que si ahora desea agregar como punto de control a un Rectángulo no podrá sin embargo sí podrá agregar una Elipse. ¿Por qué? Por los números de vértices. Un rectángulo posee 4 vértices mientras que un círculo o una elipse poseen muchos vértices (exactamente 53 vértices).
Un último dato sobre esto, le pido que recuerde el concepto de números de segmentos para dibujar un círculo. Bien, usted puede pensar que con 3 segmentos entonces corresponde 3 vértices pues esto no es así. Slick2D realiza algunos cálculos y terminan siendo 4 vértices. Como conclusión podemos decir que no se confíe con determinar la cantidad de segmentos para calcular el número de vértices en un círculo o elipse, ya que esto varía según Slick2D considere necesario para dibujar.
Según lo anterior, es correcto agregar un círculo de 3 segmentos y un rectángulo al mismo morphShape.

Hasta ahora hemos creado el MorphShape con la figura base y le hemos agregado puntos de control, nos falta conectar todos esos puntos y darle movimiento. En esta etapa Slick2D nos ayuda mucho. La clase MorphShape provee dos métodos:

void setMorphTime(float tiempo)
void updateMorphTime(float delta)

Antes de explicar estos métodos es preciso saber que la figura base representa el tiempo cero. El primer punto de control representa el tiempo 1. El segundo punto de control representa el tiempo 2, y así. En nuestro ejemplo, sería:

circulo1 cuya posición es (100, 100) y radio = 40 representa el tiempo 0.

circulo2 cuya posición es (100, 100) y radio = 20 representa el tiempo 1.

circulo3 cuya posición es (100, 100) y radio = 10 representa el tiempo 2.

Cuando la animación se inicia, el tiempo es cero. Por eso, lo que el morphShape muestra es la figura base (circulo1) quedándose ahí sin ningún movimiento.
Si invocamos al método setMorphTime() así:

morphShape1.setMorphTime(1) // Note el parámetro igual a tiempo=1.

implicaría que el morphShape muestre el primer punto de control pero sin ninguna continuidad, es decir, cambia directamente del circulo1 al circulo2. Lo que deseamos hacer es que este cambio sea gradual.
Para ello podemos usar el método updateMorphTime(). Este método recibe como parámetro el valor diferencial del tiempo, es decir, el tiempo transcurrido desde la última vez que se lo llamó. Entonces podemos usar el parámetro delta del método update() para ir llamando a ese método. Recordar que el parámetro delta tiene como valor el tiempo desde el cual se llamó la última vez al método update() expresado en milisegundos. Lo que debemos hacer ahora es crear una escala para calcular la velocidad de movimiento de nuestra animación. Observe la siguiente figura:
La escala que propuse fue que la animación debe tardar 500 ms para llegar al primer punto de control (t = 1). Para llegar al segundo punto de control (t = 2) debe tardar otros 500 ms, en total 1000 ms.
Entonces por regla de 3 simple tenemos que si t = 1 equivale a 500 ms entonces el valor de delta equivaldrá a t = delta / 500. Lo podés visualizar mejor en:

500 ms ---- t = 1

delta ms ---- t = delta * 1 / 500

Bien, entonces lo que nos queda es llamar al método updateMorphTime() haciendo el cambio de escala.

@Override
public void update(GameContainer container, int delta) throws SlickException {
   morphShape1.updateMorphTime(normalizar(delta));
}

public float normalizar(float v) {
   return v / 500;
}

Por último pintamos el morphShape en el método render().

@Override
public void render(GameContainer container, Graphics g) throws SlickException {
   g.fill(morphShape1);
}

Traslado de una figura con MorphShape.

Ahora haremos que nuestra figura se traslade. Para ello es necesario cambiar la posición. El siguiente código agrega dos líneas al ejemplo anterior para crear un nuevo morphShape que traslade una figura:

@Override
public void init(GameContainer container) throws SlickException {
Circle circulo1 = new Circle(100, 100, 40);
Circle circulo2 = new Circle(100, 100, 20);
Circle circulo3 = new Circle(100, 100, 10);

morphShape1 = new MorphShape(circulo1);
morphShape1.addShape(circulo2);
morphShape1.addShape(circulo3);

// morphShape2 debe ser un atributo.
morphShape2 = new MorphShape(new Circle(240, 100, 30));
morphShape2.addShape(new Circle(240, 300, 30));
}

Fíjese que hemos cambiado la posición en y, dejando la posición en x y el radio igual. Por lo que el círculo se trasladará en forma vertical (de arriba a abajo y de abajo hacia arriba).
A continuación se muestra el código completo:

import org.newdawn.slick.AppGameContainer;
import org.newdawn.slick.BasicGame;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.geom.Circle;
import org.newdawn.slick.geom.MorphShape;

public class MiJuego extends BasicGame {

private AppGameContainer contenedor;
private MorphShape morphShape1, morphShape2;

public MiJuego() throws SlickException {
   super("Gráficos");
   contenedor = new AppGameContainer(this);
   contenedor.setShowFPS(false);
   contenedor.setDisplayMode(600, 500, false);
   contenedor.start();
}

@Override
public void init(GameContainer container) throws SlickException {
   Circle circulo1 = new Circle(100, 100, 40);
   Circle circulo2 = new Circle(100, 100, 20);
   Circle circulo3 = new Circle(100, 100, 10);

   morphShape1 = new MorphShape(circulo1);
   morphShape1.addShape(circulo2);
   morphShape1.addShape(circulo3);
   morphShape2 = new MorphShape(new Circle(240, 100, 30));
   morphShape2.addShape(new Circle(240, 300, 30));
}

@Override
public void update(GameContainer container, int delta) throws SlickException {
   morphShape1.updateMorphTime(normalizar(delta));
   morphShape2.updateMorphTime(normalizar(delta));
}

public float normalizar(float v) {
   return v / 500f;
}

@Override
public void render(GameContainer container, Graphics g) throws SlickException {
   g.fill(morphShape1);
   g.fill(morphShape2);
}
}


No hay comentarios:

Publicar un comentario