1

I've found something weird when splitting a translate operation around a scaling one with Java Swing. Maybe I'm doing something stupid but I'm not sure where.

In the first version I center the image, scale it and then translate it to the desired position. In the second version I directly scale the image and then translate to the desired position compensating for having a non centered image. The two solutions should be equivalent. Also this is important when considering rotations around a point and motion in another.. I've code that does that too... but why this does not work?

Here are the two versions of the code. They are supposed to do the exact same thing but they are not. Here are the screenshots:

First produces: screenshot1

Second produces: screenshot2

I think that the two translation operations in draw1 surrounding the scale operation should be equivalent to the scale translate operation in draw2.

Any suggestion?

MCVE:

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.net.URL;

public class Asteroid extends JComponent implements ActionListener {

    public static final Dimension FRAME_SIZE = new Dimension(640, 480);
    public double x = 200;
    public double y = 200;
    public int radius = 40;
    private AffineTransform bgTransfo;
    private final BufferedImage im2;
    private JCheckBox draw1Check = new JCheckBox("Draw 1", true);

    Asteroid() {
        BufferedImage img = null;
        try {
            img = ImageIO.read(new URL("https://i.stack.imgur.com/CWJdo.png"));
        } catch (Exception e) {
            e.printStackTrace();
        }
        im2 = img;
        initUI();
    }

    private final void initUI() {
        draw1Check.addActionListener(this);
        JFrame frame = new JFrame("FrameDemo");
        frame.add(BorderLayout.CENTER, this);
        frame.add(BorderLayout.PAGE_START, draw1Check);
        frame.pack();
        frame.setVisible(true);
        frame.setDefaultCloseOperation(frame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {
        Asteroid asteroid = new Asteroid();
    }

    @Override
    public Dimension getPreferredSize() {
        return FRAME_SIZE;
    }

    @Override
    public void paintComponent(Graphics g0) {
        Graphics2D g = (Graphics2D) g0;
        g.setColor(Color.white);
        g.fillRect(0, 0, 640, 480);
        if (draw1Check.isSelected()) {
            draw1(g);
        } else {
            draw2(g);
        }
    }

    public void draw1(Graphics2D g) {//Draw method - draws asteroid
        double imWidth = im2.getWidth();
        double imHeight = im2.getHeight();
        double stretchx = (2.0 * radius) / imWidth;
        double stretchy = (2.0 * radius) / imHeight;

        bgTransfo = new AffineTransform();
        //centering
        bgTransfo.translate(-imWidth / 2.0, -imHeight / 2.0);
        //scaling
        bgTransfo.scale(stretchx, stretchy);
        //translation
        bgTransfo.translate(x  / stretchx, y / stretchy);

        //draw correct position
        g.setColor(Color.CYAN);
        g.fillOval((int) (x - radius), (int) (y - radius), (int) (2 * radius), (int) (2 * radius));

        //draw sprite
        g.drawImage(im2, bgTransfo, this);
    }

    public void draw2(Graphics2D g) {//Draw method - draws asteroid
        double imWidth = im2.getWidth();
        double imHeight = im2.getHeight();
        double stretchx = (2.0 * radius) / imWidth;
        double stretchy = (2.0 * radius) / imHeight;

        bgTransfo = new AffineTransform();

        //scale
        bgTransfo.scale(stretchx, stretchy);

        //translate and center
        bgTransfo.translate((x - radius) / stretchx, (y - radius) / stretchy);

        //draw correct position
        g.setColor(Color.CYAN);
        g.fillOval((int) (x - radius), (int) (y - radius), (int) (2 * radius), (int) (2 * radius));

        //draw sprite 
        g.drawImage(im2, bgTransfo, this);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        repaint();
    }
}
  • 3
    Because `position` is the center of each circle whereas `fillOval` and `drawImage` require the position to be the top left corner. – meowgoesthedog Mar 07 '18 at 20:20
  • For better help sooner, post a [MCVE] or [Short, Self Contained, Correct Example](http://www.sscce.org/). – Andrew Thompson Mar 07 '18 at 21:27
  • @meowgoesthedog yes I know I realign the images (e.g. i substract radius.. in both version.. in the first with the first translation, in the second version in the uniq translation..) – Dimitri Ognibene Mar 07 '18 at 23:26
  • Yes but you pass `position` instead of `position - radius` when calling `bgTransfo.translate` in `draw1`. That is the key line. – meowgoesthedog Mar 07 '18 at 23:31
  • Dmitri, I think the fundamental question here is: do you understand what `AffineTransform` does and what exactly combination of `scale` + `translate` do? I think the question becomes pretty obvious if you remove the `scale` for a moment (and don't divide by `stretchx`/`stretchy` as well). Then when you apply `scale` it obviously does nothing regarding this issue given the divide by `stretchx`/`stretchy` inside `translate`. – SergGr Mar 07 '18 at 23:41
  • *"I think this is a quite minimal example."* Sure, but is it as minimal as your attention span? There is more to MCVE than 'Mininal', and more to SSCCE than 'Short'. Fulfill the rest of the requirements. – Andrew Thompson Mar 07 '18 at 23:41
  • @AndrewThompson, I think you picking too much here. The code is not really complete but it is pretty easy to add missing bits to run it, if you really need that to see what's wrong (which I believe meowgoesthedog already pointed out in the very first comment) – SergGr Mar 07 '18 at 23:43
  • @meowgoesthedog I subtract radius in the first translation – Dimitri Ognibene Mar 08 '18 at 00:01
  • I was talking about this line: `bgTransfo.translate((x)/stretchx,(y)/stretchy);` in `draw1`. Where's the radius subtraction there? You tell me. – meowgoesthedog Mar 08 '18 at 00:57
  • *"it is pretty easy to add missing bits to run it"* If it's pretty easy for 10 of us to do it, it's 10% of pretty easy for you to do it. As an aside. There are **two** close reasons that mention 'lack of MCVE' as part of the reason. Given this now has two close votes, and no answers, you may want to consider doing the minimum effort that I, and probably others, expect before giving a problem close attention. – Andrew Thompson Mar 08 '18 at 06:06
  • @AndrewThompson added the main and swing init bits.. hope it is still short or minimal – Dimitri Ognibene Mar 08 '18 at 06:40
  • @SergGr I think that the two translation operations in draw1 surrounding the scale operation should be equivalent to the scale tranlate operation in draw2. What am I missing? – Dimitri Ognibene Mar 08 '18 at 06:45
  • @meowgoesthedog first step is radius subtraction `bgTransfo.translate(-imWidth/2.0,-imHeight/2.0);` – Dimitri Ognibene Mar 08 '18 at 07:20
  • *"hope it is still short or minimal"* Yes it's still minimal. People regularly read too much importance into that part - if all the code is actually necessary, I'd regard minimal to mean up to 200 lines of code. As others might disagree, I am reticent to state that often. Having said that, I get a compile error on `draw1(g);` - it should be included in the main source rather than included separately. Also a tip: `img = ImageIO.read(new File("asteroid1.png"));` One way to get image(s) for an example is to **hot link** to images seen in [this Q&A](http://stackoverflow.com/q/19209650/418556). – Andrew Thompson Mar 08 '18 at 07:22
  • @AndrewThompson Thanks, do you suggest that I just put the whole code in one section istead of splitting them? I tested the code and it runs here. The image of the asteroid is also uploaded with number 1.. – Dimitri Ognibene Mar 08 '18 at 07:27
  • @AndrewThompson I don't know why some parameters e.g. im2 where changed to null. I brought back to the original version... still this should not give you compile error.. – Dimitri Ognibene Mar 08 '18 at 07:37
  • OK.. look. I reworked the code bits into a single MCVE that hot links to the asteroid image and allows whoever runs that code to choose between the two methods using a check box. Now I've done that, I edited the MCVE into the question, because.. well I've just realised I don't quite understand why you don't just use the second method. I'm obviously missing something. Why not just use the 2nd method? – Andrew Thompson Mar 08 '18 at 09:07
  • @AndrewThompson Thanks a lot. I want to understand why the first fails... It should not... In other conditions, e.g. rotation it is normal to perform a first translation to the rotation axis, rotate and then doing a second translation.. so this should work.. – Dimitri Ognibene Mar 08 '18 at 12:26

1 Answers1

0

Not sure if this question is still really open. Anyway here is my answer.

I think the crucial part to understand this behavior is the difference between AffineTransform.concatenate and AffineTransform.preConcatenate methods. The thing is that resulting transformation depends on the order the sub-transformations are applied. To quote the concatenate JavaDoc

Concatenates an AffineTransform Tx to this AffineTransform Cx in the most commonly useful way to provide a new user space that is mapped to the former user space by Tx. Cx is updated to perform the combined transformation. Transforming a point p by the updated transform Cx' is equivalent to first transforming p by Tx and then transforming the result by the original transform Cx like this: Cx'(p) = Cx(Tx(p))

compare this with preConcatenate:

Concatenates an AffineTransform Tx to this AffineTransform Cx in a less commonly used way such that Tx modifies the coordinate transformation relative to the absolute pixel space rather than relative to the existing user space. Cx is updated to perform the combined transformation. Transforming a point p by the updated transform Cx' is equivalent to first transforming p by the original transform Cx and then transforming the result by Tx like this: Cx'(p) = Tx(Cx(p))

The scale and translate methods are effectively concatenate. Lets call 3 transformations in your draw1 method C (center), S (scale), and T (translate). So your compound transformation is effectively C(S(T(p))). Particularly it means that S is applied to the T but not to the C so your C does not really center the image. A simple fix would be to change the order of S and C but I think that a more proper fix would be something like this:

public void draw3(Graphics2D g) {
    //Draw method - draws asteroid
    double imWidth = im2.getWidth();
    double imHeight = im2.getHeight();
    double stretchx = (2.0 * radius) / imWidth;
    double stretchy = (2.0 * radius) / imHeight;

    AffineTransform bgTransfo = new AffineTransform();

    //translation
    bgTransfo.translate(x, y);

    //scaling
    bgTransfo.scale(stretchx, stretchy);

    //centering
    bgTransfo.translate(-imWidth / 2.0, -imHeight / 2.0);

    //draw correct position
    g.setColor(Color.CYAN);
    g.fillOval((int) (x - radius), (int) (y - radius), (int) (2 * radius), (int) (2 * radius));

    //draw sprite
    g.drawImage(im2, bgTransfo, this);
}

I think the big advantage of this method is that you don't have to re-calculate the T using stretchx/stretchy

SergGr
  • 23,570
  • 2
  • 30
  • 51
  • would this work if instead of scale you had rotation around the center of the object? – Dimitri Ognibene Mar 14 '18 at 18:51
  • @DimitriOgnibene, I'm not sure what exactly your additional (rotation) question means. It might work or might not depending on your goal and your code. You probably should provide more details to get more specific answer. – SergGr Mar 14 '18 at 19:03