Search code examples
javarotationgraphics2daffinetransform

Rotate a moving shape around its center


I'm making a 2D game in Java where the player guides a polygon through obstacles. The polygon moves up and down and the game world scrolls left and right. I need the polygon to rotate around its center, but since it is constantly being translated the point it is being rotated around moves. Trying to translate it back to the original center, rotate it, and translate it back doesn't work. How do I get the center of the shape?

Here are my motion calculations on a 2ms timer:

@Override
public void actionPerformed(ActionEvent e) {

    double theta = angleRad+90;
    if (up == true) {
        if (accelerating == false) {
            time2 = 0;
            moveX0 = moveX;
            moveY0 = moveY;
            accelerating = true;
        }
        time1++;
        double t = time1/500;
        if (accCount % 10 == 0) {
            DronePilot.velocity++;
        }
        moveX = moveX0 + velX*Math.cos(theta)*t;
        moveY = moveY0 + velY*Math.sin(theta)*t-(1/2d)*g*Math.pow(t, 2);
        velX = (DronePilot.velocity)*Math.cos(theta);
        velY = (DronePilot.velocity)*Math.sin(theta)-g*(t);
        accCount++;
    } else if (up == false){
        if (accelerating == true) {
            time1 = 0;
            moveX0 = moveX;
            moveY0 = moveY;
            accelerating = false;
        }
        time2++;
        double t = time2/500;
        moveX = moveX0 + velX*Math.cos(theta)*t;
        moveY = moveY0 + velY*Math.sin(theta)*t-(1/2d)*g*Math.pow(t, 2);
        accCount = 0;
    } if (left == true) {
        angleCount++;
        if (angleCount % 2 == 0) {
            angleDeg++;
        }
        angleRad = Math.toRadians(angleDeg);
    } else if (right == true) {
        angleCount--;
        if (angleCount % 2 == 0) {
            angleDeg--;
        }
        angleRad = Math.toRadians(angleDeg);
    }
    repaint();
}
}

And here is my paintComponent method:

@Override
public void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2D = (Graphics2D)g;

    Graphics g2 = g.create();
    Graphics2D copy = (Graphics2D)g2;




    copy.rotate(-angleRad, xPos, yPos);

    copy.translate(0, -moveY);

    g2D.translate(-moveX, 0);

    copy.draw(player.shape);

    for (Rectangle2D.Double r: DronePilot.rocksFloorArray) {
        g2D.draw(r);
    }
    for (Rectangle2D.Double r: DronePilot.rocksCeilArray) {
        g2D.draw(r);
    }
    for (Rectangle2D.Double r: DronePilot.roomsArray) {
        g2D.draw(r);
    }
}

where (xPos, yPos) are the center of the screen.


Solution

  • Transformations are (normally) compounding

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2D = (Graphics2D)g;
    
        Graphics g2 = g.create();
        Graphics2D copy = (Graphics2D)g2;
    
        copy.rotate(-angleRad, xPos, yPos);
        copy.translate(0, -moveY);
    
        g2D.translate(-moveX, 0);
    
        copy.draw(player.shape);
        for (Rectangle2D.Double r: DronePilot.rocksFloorArray) {
            g2D.draw(r);
        }
        for (Rectangle2D.Double r: DronePilot.rocksCeilArray) {
            g2D.draw(r);
        }
        for (Rectangle2D.Double r: DronePilot.roomsArray) {
            g2D.draw(r);
        }
    }
    

    In your above code, you are translating both the original Graphics context and the copy. In this context, copy won't be affected by the original and the original won't be affected by the copy, BUT, the original context is a shared resource and since you don't reset the translation, you will continue to get a translated context each time (compounding).

    As a general rule of thumb, do ALL transformations on a copy and dispose of it when you're done.

    For example...

    Graphics2D g2d = (Graphics2D)g.create();
    AffineTransform at = AffineTransform.getTranslateInstance(playerPoint.x, playerPoint.y);
    at.rotate(Math.toRadians(angle), player.getBounds2D().getCenterX(), player.getBounds2D().getCenterY());
    g2d.setTransform(at);
    g2d.setColor(Color.RED);
    g2d.fill(player);
    g2d.setColor(Color.BLACK);
    g2d.draw(player);
    g2d.dispose();
    

    This basically translates the position of the object to the player's position and then rotates the object about the centre of the object

    You could, also, apply one transformation, create a copy of that context and apply another transformation, which would become compounded (so you could translate one context, copy it, and then rotate the copy and the first translation would still applied to the copy)

    This incredible simple example demonstrates two basic example...

    1. Using the Graphics context and a AffineTransform to translate and rotate the player object (about it's centre point)
    2. Using a Path2D to generate a transformed shape (this example makes two objects, but you could use a single AffineTransform translated and rotated and apply it once).

    In both cases, they don't affect the original shape

    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.Point;
    import java.awt.Rectangle;
    import java.awt.Shape;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.geom.AffineTransform;
    import java.awt.geom.Path2D;
    import java.awt.geom.Rectangle2D;
    import java.util.Random;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.Timer;
    
    public class Test {
    
        public static void main(String[] args) {
            new Test();
        }
    
        public Test() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    JFrame frame = new JFrame();
                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    frame.add(new TestPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
        public class TestPane extends JPanel {
    
            private Shape player;
            private Point playerPoint;
            private float angle;
            private float deltaZ = 1.0f;
    
            private int deltaX, deltaY;
    
            public TestPane() {
                player = new Rectangle(0, 0, 20, 20);
                playerPoint = new Point(80, 80);
                Random rnd = new Random();
                deltaX = 1;
                deltaY = -1;
    
                Timer timer = new Timer(5, new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        playerPoint.x += deltaX;
                        playerPoint.y += deltaY;
    
                        Shape rotatedPlayer = rotatedAndTranslatedPlayer();
                        Rectangle2D bounds = rotatedPlayer.getBounds2D();
                        if (bounds.getX() < 0.0) {
                            playerPoint.x = (int)(bounds.getX() * -1);
                            deltaX *= -1;
                        } else if (bounds.getX() + bounds.getWidth() >= getWidth()) {
                            playerPoint.x = getWidth() - (int)bounds.getWidth();
                            deltaX *= -1;
                        }
                        if (bounds.getY() < 0) {
                            playerPoint.y = 0;
                            deltaY *= -1;
                        } else if (bounds.getY() + bounds.getHeight() > getHeight()) {
                            playerPoint.y = getHeight() - (int)bounds.getHeight();
                            deltaY *= -1;
                        }
                        angle += deltaZ;
                        repaint();
                    }
                });
                timer.start();
            }
    
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(200, 200);
            }
    
            protected Shape rotatedAndTranslatedPlayer() {
                Path2D.Double rotated = new Path2D.Double(player,  AffineTransform.getRotateInstance(
                        Math.toRadians(angle),
                        player.getBounds2D().getCenterX(), 
                        player.getBounds2D().getCenterY()));
                return new Path2D.Double(rotated, AffineTransform.getTranslateInstance(playerPoint.x, playerPoint.y));            
            }
    
            // Simply paints the "area" that the player takes up when it's rotated and
            // translated
            protected void paintAutoTranslatedShape(Graphics2D g2d) {
                g2d.setColor(Color.DARK_GRAY);
                g2d.fill(rotatedAndTranslatedPlayer().getBounds2D());
            }
    
            // Uses a AffineTransform to translate and rotate the player
            protected void paintPlayer(Graphics2D g2d) {
                AffineTransform at = AffineTransform.getTranslateInstance(playerPoint.x, playerPoint.y);
                at.rotate(Math.toRadians(angle), player.getBounds2D().getCenterX(), player.getBounds2D().getCenterY());
                g2d.setTransform(at);
                g2d.setColor(Color.RED);
                g2d.fill(player);
                g2d.setColor(Color.BLACK);
                g2d.draw(player);
            }
    
            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                Graphics2D g2d = (Graphics2D) g.create();
                paintAutoTranslatedShape(g2d);
                g2d.dispose();
                g2d = (Graphics2D) g.create();
                paintPlayer(g2d);
                g2d.dispose();
            }
    
        }
    }