AffineTransform: Rotación de un gráfico o imagen en java

De ChuWiki

Vamos a hacer un pequeño programa que abra una ventana en la que se verá un foto y tendrá dos botones para girar la imagen a izquierda o a derecha. Aprovecharemos el ejemplo para dar algunas bases del pintado de gráficos sobre un componente java y para introducir la clase AffineTransform.


Bases del pintado de gráficos sobre un componente Java[editar]

Si queremos que un componente de java pinte nuestros gráficos, debemos heredar de él y sobreescribir el método paint(Graphics g). Dentro de este método, usando los métodos del Graphics que nos pasan, dibujaremos lo que queramos. Graphics nos proporciona métodos drawLine(...), drawCircle(...), drawImage(...), etc. Esos métodos dibujan sobre el componente.

Cuando queramos que cambie el dibujo del componente, debemos llamar al método repaint(). Esto provocará que java llame en cuanto pueda al método paint(Graphics) en el que tendremos la oportunidad de dibujar otra cosa.

El componente habitual del que se hereda para dibujar es java.awt.Canvas. Sin embargo, a mi al menos, me ha dado problemas extraños y es más limitado que cualquiera de los componentes de javax.swing. Por ello, suelo usar y heredar de JComponent para el dibujo.

El Graphics que recibimos en el método paint(Graphics) es una clase Graphics por compatibilidad con versiones antiguas de java. Sin embargo, en los javax.swing la clase que recibimos, aunque se declare como Graphics en el parámetro, es en realidad una clase hija llamada Graphics2D. Esta clase tiene muchos más métodos de dibujado y muchas más posibilidades. Por ello, podemos hacer con confianza un "cast" del Graphics que recibimos a Graphics2D.

Dibujo y girado del gráfico o imagen[editar]

Como hemos comentado, heredamos de JComponent y sobreescribimos el método paint(Grahics g). Esta clase recibirá en el constructor el nombre del fichero imagen que queremos dibujar, lo cargará y se lo guardará en un atributo de la clase que pondremos a ese efecto.

Como queremos que la imagen gire, pondremos también un atributo "rotacion", con el ángulo de giro deseado en radianes, así como los métodos setRotacion() y getRotacion() para poder modificar este valor a gusto.

En el método paint(Graphics) haremos en primer lugar un "cast" de Graphics a Graphics2D. De esta forma tendremos disponibles los métodos que permiten dibujar una imagen girada. El método de Graphics2D que permite dibujar la imagen girada es el método drawImage() que admite como parámetro, además de la imagen, una clase AffineTranform. Veamos esta clase con un poco más de detalle.


La clase AffineTransform[editar]

La clase AffineTransform es una clase que se encarga de "deformar" los gráficos, aplicando en ellos una transformación. La transformación a realizar se determina por medio de los valores de una matriz de 3x3. Podemos instanciar esta clase pasándole en el constructor esta matriz de valores. Sin embargo, hay que saber muchas matemáticas para saber qué valores pasar en función de la transformación que queremos hacer. Afortunadamente, para facilitarnos la vida, la clase AffineTransform tiene varios métodos estáticos que nos devuelven una instancia de la clase para las transformaciones más habiutales, como rotaciones, escalados, traslaciones, etc.

Usaremos, para obtener nuestra tranformación, el método estático getRotateInstance() de la clase AffineTransform, al que pasamos el ángulo deseado de giro en radianes y el punto que hacen de centro de giro. En nuestro caso, pasaremos el atributo "rotacion" y el punto medio de la imagen width/2 y height/2.

AffineTransform tx = AffineTransform.getRotateInstance(rotacion,
               icono.getIconWidth()/2, icono.getIconHeight()/2);


Dibujo de la imagen[editar]

Para dibujar la imagen, símplemente usaremos el método drawImage() de Graphics2D que admite la imagen y la instancia de la clase AffineTransform que acabamos de obtener.


El componente con la imagen[editar]

La siguiente es la clase completa del componente que dibuja la imagen rotada según el ángulo que se le diga con setRotacion().

package com.chuidiang.ejemplos.girar_grafico;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;

import javax.swing.ImageIcon;
import javax.swing.JComponent;

/**
 * Componente que dibuja una foto y permite rotarla.
 * @author chuidiang
 *
 */
public class Lienzo extends JComponent {
    
    /**
     * Devuelve como tamaño preferido el de la foto.
     */
    @Override
    public Dimension getPreferredSize() {
        return new Dimension(icono.getIconWidth(), icono.getIconHeight());
    }

    /** La foto */
    private ImageIcon icono = null;

    /**
     * Carga la foto y la guarda
     * @param ficheroImagen
     */
    public Lienzo(String ficheroImagen) {
        icono = new ImageIcon(ficheroImagen);
    }

    /**
     * Cuanto queremos que se rote la foto, en radianes.
     */
    private double rotacion = 0.0;

    /**
     * Dibujo de la foto rotandola.
     */
    public void paint(Graphics g) {
        Graphics2D g2d = (Graphics2D) g;
        
        // AffineTransform realiza el giro, usando como eje de giro el centro
        // de la foto (width/2, height/2) y el angulo que indica el atributo
        // rotacion.
        AffineTransform tx = AffineTransform.getRotateInstance(rotacion, 
                icono.getIconWidth()/2, icono.getIconHeight()/2);
        
        // dibujado con la AffineTransform de rotacion
        g2d.drawImage(icono.getImage(), tx, this);
    }

    /**
     * Devuelve la rotacion actual.
     * @return rotacion en radianes
     */
    public double getRotacion() {
        return rotacion;
    }

    /**
     * Se le pasa la rotación deseada.
     * @param rotacion La rotacion en radianes.
     */
    public void setRotacion(double rotacion) {
        this.rotacion = rotacion;
    }
}


La ventana principal[editar]

La ventana principal no tiene nada especial, símplemente instancia todos los componentes y los visualiza. No obstante, vamos a dar algunos detalles.

  • Se está cargando una imagen ./goku.jpg. Si copias el ejemplo, tendrás que poner un path y una imagen que tengas disponible.
  • La acción que se añade a los botones de rotar a izquierda y derecha hacen lo siguiente:
    • Obtienen de Lienzo el ángulo de rotación con getRotacion()
    • Suman o restan 0.1 radianes a dicho ángulo y se lo pasan con setRotacion().
    • Llaman al método repaint(), para provocar un repintado con el nuevo ángulo de giro.

La ventana con los botones y el componente con la foto, así como el main() del programa puede ser el siguiente:

package com.chuidiang.ejemplos.girar_grafico;

import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.WindowConstants;

/**
 * Ejemplo de giro de grafico.
 * Una ventana con una foto y dos botones para girar en sentido horario y antihorario.
 * @author chuidiang
 *
 */
public class GirarGrafico {

    /** Componente que lleva la foto */
    private Lienzo l;

    /**
     * Crea una instancia de esta clase.
     * @param args
     */
    public static void main(String[] args) {
        new GirarGrafico();
    }

    /**
     * Crea la ventana, los botones y lo pone todo en marcha.
     */
    public GirarGrafico() {
        // Construccion de la ventana.
        JFrame v = new JFrame("Girar grafico");
        
        // El componente con la foto
        l = new Lienzo("./goku.jpg");
        v.getContentPane().add(l);
        
        // el panel con los botones
        JPanel botonesRotacion = new JPanel(new FlowLayout());
        JButton botonSentidoHorario = new JButton("+0.1");
        JButton botonSentidoAntiHorario = new JButton("-0.1");
        botonesRotacion.add(botonSentidoAntiHorario);
        botonesRotacion.add(botonSentidoHorario);
        v.getContentPane().add(botonesRotacion, BorderLayout.NORTH);

        // las acciones de los botones
        botonSentidoAntiHorario.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                l.setRotacion(l.getRotacion() - 0.1);
                l.repaint();
            }

        });
        botonSentidoHorario.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                l.setRotacion(l.getRotacion() + 0.1);
                l.repaint();
            }

        });

        // visualizarlo todo.
        v.pack();
        v.setVisible(true);
        v.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    }
}