0

Let's say I have a grid with images in Java. I now draw the images in the Graphics2D component g as follows:

  g.drawImage(image, 50 * cellWidth, 50 * cellHeight, cellWidth, cellHeight, Color.WHITE, null)

I'm now interested in rotating the image (while staying in the same grid row and column) 90 degrees in a given direction. Could someone help me accomplish this?

Ronald
  • 157
  • 6

1 Answers1

2

First, you need a Graphics2D context. In most cases when supplied with a Graphics it's actually an instance of Graphics2D so you can simply cast it.

Having said that though, when perform transformations, it's always useful to create a new context (this copies the state only)...

Graphics2D g2d = (Graphics2D) g.create();

Next, you want to translate the origin point. This makes it a lot easier to do things like rotation.....

g2d.translate(50 * cellWidth, 50 * cellHeight);

Then you can rotate the context around the centre point of the cell (remember, 0x0 is now our cell offset)...

g2d.rotate(Math.toRadians(90), cellWidth / 2, cellWidth / 2);

And then we can simply draw the image...

g2d.drawImage(image, 0, 0, cellWidth, cellHeight, Color.WHITE, null);

And don't forget to dispose of the copy when you're done

g2d.dispose();

You might also want to take a look at The 2D Graphics trail, as you could use a AffineTransformation instead, but it'd be accomplishing the same thing, more or less

Is there a way to actually see the rotating happening (so see the rotation "live")?

Animation is a complex subject, add in the fact that Swing is single threaded and not thread safe and you need to think carefully about it.

Have a look at Concurrency in Swing and How to Use Swing Timers for more details.

Simple animation

The following example makes use of simple Swing Timer to rotate a image when it's clicked. The example makes use of time based approach (ie the animation runs over a fixed period of time). This produces a better result then a linear/delta approach.

enter image description here

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

public class Simple {

    public static void main(String[] args) throws IOException {
        new Simple();
    }

    public Simple() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    JFrame frame = new JFrame();
                    frame.add(new TestPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                } catch (IOException ex) {
                    Logger.getLogger(Advanced.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        });
    }

    public class TestPane extends JPanel {

        private List<BufferedImage> images;

        private BufferedImage selectedImage;

        public TestPane() throws IOException {
            images = new ArrayList<>(9);
            for (int index = 0; index < 9; index++) {
                BufferedImage img = ImageIO.read(getClass().getResource("/images/p" + (index + 1) + ".png"));
                images.add(img);
            }

            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                    if (selectedImage != null) {
                        return;
                    }
                    int col = (e.getX() - 32) / 210;
                    int row = (e.getY() - 32) / 210;

                    int index = (row * 3) + col;
                    selectedImage = images.get(index);
                    startTimer();
                }                
            });
        }

        private Timer timer;
        private Instant startedAt;
        private Duration duration = Duration.ofSeconds(1);
        private double maxAngle = 1440;
        private double currentAngle = 0;

        protected void startTimer() {
            if (timer != null) {
                return;
            }
            timer = new Timer(5, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (startedAt == null) {
                        startedAt = Instant.now();
                    }
                    Duration runtime = Duration.between(startedAt, Instant.now());
                    double progress = runtime.toMillis() / (double)duration.toMillis();
                    if (progress >= 1.0) {
                        progress = 1.0;
                        selectedImage = null;
                        startedAt = null;
                        stopTimer();
                    }
                    currentAngle = maxAngle * progress;
                    repaint();;
                }
            });
            timer.start();
        }

        protected void stopTimer() {
            if (timer == null) {
                return;
            }
            timer.stop();
            timer = null;
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension((210 * 3) + 64, (210 * 3) + 64);            
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.translate(32, 32);
            int row = 0;
            int col = 0;
            for (BufferedImage img : images) {
                int x = col * 210;
                int y = row * 210;

                Graphics2D gc = (Graphics2D) g2d.create();
                gc.translate(x, y);
                if (selectedImage == img) {
                    gc.rotate(Math.toRadians(currentAngle), 210 / 2, 210 / 2);
                }

                gc.drawImage(img, 0, 0, this);

                gc.dispose();

                col++;
                if (col >= 3) {
                    col = 0;
                    row++;
                }
            }
            g2d.dispose();
        }

    }
}

nb: My images are 210x210 in size and I'm been naughty with not using the actual sizes of the images, and using fixed values instead

Advanced animation

Advanced

While the above example "works", it becomes much more complicated the more you add it. For example, if you want to have multiple images rotate. Towards that end, you will need to keep track of some kind of model for each image which contains the required information to calculate the current rotation value.

Another issue is, what happens if you want to compound the animation? That is, scale and rotate the animation at the same time.

Towards this end, I'd lean towards using concepts like "time lines" and "key frames"

The following example is based on my personal library Super Simple Swing Animation Framework. This is bit more of a playground for me then a fully fledged animation framework, but it embodies many of the core concepts which help make animating in Swing simpler and help produce a much nicer result

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import org.kaizen.animation.Animatable;
import org.kaizen.animation.AnimatableAdapter;
import org.kaizen.animation.AnimatableDuration;
import org.kaizen.animation.DefaultAnimatableDuration;
import org.kaizen.animation.curves.Curves;
import org.kaizen.animation.timeline.BlendingTimeLine;
import org.kaizen.animation.timeline.DoubleBlender;

public class Advanced {

    public static void main(String[] args) throws IOException {
        new Advanced();
    }

    public Advanced() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    JFrame frame = new JFrame();
                    frame.add(new TestPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                } catch (IOException ex) {
                    Logger.getLogger(Advanced.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        });
    }

    public class TestPane extends JPanel {

        private List<BufferedImage> images;

        private Map<BufferedImage, Double> imageZoom = new HashMap<>();
        private Map<BufferedImage, Double> imageRotate = new HashMap<>();

        private BlendingTimeLine<Double> zoomTimeLine;
        private BlendingTimeLine<Double> rotateTimeLine;

        public TestPane() throws IOException {

            zoomTimeLine = new BlendingTimeLine<>(new DoubleBlender());
            zoomTimeLine.addKeyFrame(0, 1.0);
            zoomTimeLine.addKeyFrame(0.25, 1.5);
            zoomTimeLine.addKeyFrame(0.75, 1.5);
            zoomTimeLine.addKeyFrame(1.0, 1.0);

            rotateTimeLine = new BlendingTimeLine<>(new DoubleBlender());
            rotateTimeLine.addKeyFrame(0d, 0d);
            rotateTimeLine.addKeyFrame(0.1, 0d);
//            rotateTimeLine.addKeyFrame(0.85, 360.0 * 4d);
            rotateTimeLine.addKeyFrame(1.0, 360.0 * 4d);

            images = new ArrayList<>(9);
            for (int index = 0; index < 9; index++) {
                BufferedImage img = ImageIO.read(getClass().getResource("/images/p" + (index + 1) + ".png"));
                images.add(img);
            }

            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                    int col = (e.getX() - 32) / 210;
                    int row = (e.getY() - 32) / 210;

                    int index = (row * 3) + col;
                    BufferedImage selectedImage = images.get(index);
                    if (imageZoom.containsKey(selectedImage)) {
                        return;
                    }
                    animate(selectedImage);
                }
            });
        }

        protected void animate(BufferedImage img) {
            Animatable animatable = new DefaultAnimatableDuration(Duration.ofSeconds(1), Curves.CUBIC_IN_OUT.getCurve(), new AnimatableAdapter<Double>() {
                @Override
                public void animationTimeChanged(AnimatableDuration animatable) {
                    double progress = animatable.getProgress();
                    Double desiredZoom = zoomTimeLine.getValueAt(progress);
                    imageZoom.put(img, desiredZoom);

                    double desiredAngle = rotateTimeLine.getValueAt(progress);
                    imageRotate.put(img, desiredAngle);

                    repaint();
                }

                @Override
                public void animationStopped(Animatable animator) {
                    imageZoom.remove(img);
                    imageRotate.remove(img);
                    repaint();
                }

            });
            animatable.start();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension((210 * 3) + 64, (210 * 3) + 64);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.translate(32, 32);
            int row = 0;
            int col = 0;
            for (BufferedImage img : images) {
                if (!(imageZoom.containsKey(img) || imageRotate.containsKey(img))) {
                    int x = col * 210;
                    int y = row * 210;

                    Graphics2D gc = (Graphics2D) g2d.create();
                    gc.translate(x, y);

                    gc.drawImage(img, 0, 0, this);

                    gc.dispose();
                }

                col++;
                if (col >= 3) {
                    col = 0;
                    row++;
                }
            }
            row = 0;
            col = 0;
            for (BufferedImage img : images) {
                if (imageZoom.containsKey(img) || imageRotate.containsKey(img)) {
                    int x = col * 210;
                    int y = row * 210;

                    Graphics2D gc = (Graphics2D) g2d.create();
                    gc.translate(x, y);

                    double width = img.getWidth();
                    double height = img.getHeight();

                    double zoom = 1;

                    if (imageZoom.containsKey(img)) {
                        zoom = imageZoom.get(img);
                        width = (img.getWidth() * zoom);
                        height = (img.getHeight() * zoom);

                        double xPos = (width - img.getWidth()) / 2d;
                        double yPos = (height - img.getHeight()) / 2d;

                        gc.translate(-xPos, -yPos);
                    }
                    if (imageRotate.containsKey(img)) {
                        double angle = imageRotate.get(img);
                        gc.rotate(Math.toRadians(angle), width / 2, height / 2);
                    }

                    gc.scale(zoom, zoom);

                    gc.drawImage(img, 0, 0, this);

                    gc.dispose();
                }

                col++;
                if (col >= 3) {
                    col = 0;
                    row++;
                }
            }
            g2d.dispose();
        }

    }
}

nb: The paint workflow is a little more complicated (and could be optimised more) as it focuses on painting the images which are been animated onto of the others, which results in a much nicer result

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • This worked. Thank you! Is there a way to actually see the rotating happening (so see the rotation "live")? – Ronald Dec 29 '21 at 23:34
  • That's a more complex solution revolving around animation. You need to start with a Swing `Timer` and track the angle of rotation – MadProgrammer Dec 30 '21 at 00:40