4

I'm trying to implement scaling about a fixed point using a single global matrix. When run when, if the control is clicked it scaled but the test rectangles move further down and to right with each click. As far as I can tell each transformation (to the origin, scale, and back to the original location) is working fine individually but when I combine all 3 together I don't get the correct behavior.

Scaling Code

When the control is clicked the code (should) translate to the origin, scale up by a factor, then translate back to the original position.

protected override void OnMouseDown(MouseEventArgs e)
      {
         base.OnMouseDown(e);
         if (e.Button == System.Windows.Forms.MouseButtons.Left)
         {
            float xPos = e.Location.X - viewMatrix.OffsetX;
            float yPos = e.Location.Y - viewMatrix.OffsetY;

            Matrix translateOrigin = new Matrix(1, 0, 0, 1, -xPos, -yPos);
            Matrix translateBack = new Matrix(1, 0, 0, 1, xPos, yPos);
            Matrix scaleMatrix = new Matrix(1.5f, 0, 0, 1.5f, 0, 0);

            viewMatrix.Multiply(translateOrigin);
            viewMatrix.Multiply(scaleMatrix);
            viewMatrix.Multiply(translateBack);
         }
         else
         {
            viewMatrix = new Matrix();
         }
         Refresh();
      }

Drawing Code

This is the code that I'm using to draw. The two rectangles are just for reference and the second value on the new Pen(2) is to make sure my lines stay 1 pixel wide.

protected override void OnPaint(PaintEventArgs e)
      {
         base.OnPaint(e);

         GraphicsState gState = e.Graphics.Save();

         e.Graphics.MultiplyTransform(viewMatrix);
         e.Graphics.DrawRectangle(new Pen(Color.Pink, 1.0f / viewMatrix.Elements[3]), -5, -5, 10, 10);
         e.Graphics.DrawRectangle(new Pen(Color.Pink, 1.0f / viewMatrix.Elements[3]), 20, 20, 10, 10);

         e.Graphics.Restore(gState);
      }

Edit

Looking at the code again after a good day (or 2) of rest I realized I had the wrong idea stuck in my head (this is what I get for trying to figure this out at the end of the day). The behavior that I'm looking for is that the view will scale with the clicked point staying in the same spot. Example if I clicked the lower right hand corner of one of the rectangles the view would zoom it keeping the lower right under the mouse.

Edit 2

After a lot of help from @TaW I came out with the following code that will zoom and keep the point under the mouse fixed.

protected override void OnMouseDown(MouseEventArgs e)
{
 base.OnMouseDown(e);

 if (e.Button == System.Windows.Forms.MouseButtons.Left)
 {

    //Get the inverse of the view matrix so that we can transform the mouse point into the view
    Matrix viewMatrixRev = viewMatrix.Clone();
    viewMatrixRev.Invert();

    //Translate the mouse point
    PointF mousePoint = e.Location;
    viewMatrixRev.TransformPoints(new PointF[] { mousePoint });

    //Transform the view
    viewMatrix.Translate(-mousePoint.X, -mousePoint.Y, MatrixOrder.Append);
    viewMatrix.Scale(zoom, zoom, MatrixOrder.Append);
    viewMatrix.Translate(mousePoint.X, mousePoint.Y, MatrixOrder.Append);
 }
 else
 {
    viewMatrix = new Matrix();
 }
 Refresh();
}
amura.cxg
  • 2,348
  • 3
  • 21
  • 44
  • Did you actually try and test this `viewMatrixRev.TransformPoints(new PointF[] { mousePoint });` part?? It is a simplfication over my code but I can't get it to work; structs are value types and placing one in the transform array seems to create just a copy which then is discarded..??? cost me an hour to find out.. – TaW Jan 14 '15 at 17:00
  • Hmmm....I'm more confused now. It did work for me but you're right, if I step through the code `mousePoint` doesn't change after the call to transform. So I tried a few things and completely removed the code translating `mousePoint` and it works just as well – amura.cxg Jan 14 '15 at 17:20
  • Hehe, maybe another good night's sleep? – TaW Jan 14 '15 at 17:22
  • I realize this is what Ripple was trying to say. Somehow I must have screwed his/her solution and thought it wasn't working – amura.cxg Jan 14 '15 at 17:26
  • @amura.cxg Glad you accomplished your goal. The drawn objects and the mouse cursor are in the same coordinate system, so no need to transform the mouse coordinates. These kind of things are always hard to explain for me. Sorry if my poor English and the lack of explanation confused you. – Ripple Jan 15 '15 at 04:18

2 Answers2

6

Matrix.Multiply with one argument

Multiplies this Matrix by the matrix specified in the matrix parameter, by prepending the specified Matrix.

So, your matrix sequence is being applied in reverse order.

Try this instead:

viewMatrix.Multiply(translateOrigin, MatrixOrder.Append);
viewMatrix.Multiply(scaleMatrix, MatrixOrder.Append);
viewMatrix.Multiply(translateBack, MatrixOrder.Append);

EDIT:
The idea is simple.

enter image description here

All you need to do is to translate to the origin, scale, and translate back to the pivot (mouse point) in proper order. Your viewMatrix keeps the previous result, so the new transformation matrix should be applied after it, and that would be done by MatrixOrder.Append.

Now the solution would be:

float xPos = e.Location.X;
float yPos = e.Location.Y;

Matrix translateOrigin = new Matrix(1, 0, 0, 1, -xPos, -yPos);
Matrix translateBack = new Matrix(1, 0, 0, 1, xPos, yPos);
Matrix scaleMatrix = new Matrix(1.5f, 0, 0, 1.5f, 0, 0);

viewMatrix.Multiply(translateOrigin, MatrixOrder.Append);
viewMatrix.Multiply(scaleMatrix, MatrixOrder.Append);
viewMatrix.Multiply(translateBack, MatrixOrder.Append);

In addition, this can be done more simply.

float xPos = e.Location.X;
float yPos = e.Location.Y;

viewMatrix.Translate(-xPos, -yPos, MatrixOrder.Append);
viewMatrix.Scale(1.5f, 1.5f, MatrixOrder.Append);
viewMatrix.Translate(xPos, yPos, MatrixOrder.Append);
Ripple
  • 1,257
  • 1
  • 9
  • 15
  • thanks for the answer, your answer is correct for the original version of the question. However, TaW made me realize that the behavior I was looking for wasn't actually Zooming about a fixed point but rather zooming and keeping the current point fixed. – amura.cxg Jan 14 '15 at 15:53
  • I like the simplified version you've suggested at the end. I'll implement that in my final solution as it cuts down a lot of extra work. Thanks again! – amura.cxg Jan 14 '15 at 16:03
2

Your code works fine imo. But of course you need to be clear about what you want!

This is the point you use to zoom into:

float xPos = e.Location.X - viewMatrix.OffsetX;
float yPos = e.Location.Y - viewMatrix.OffsetY;

And that's what happens.

If you want the 1st Rectangles to keep its position (centered around the origin) you simply need to change it to

float xPos = - viewMatrix.OffsetX;
float yPos = - viewMatrix.OffsetY;    

thereby ignoring the position of the mouse click.

When you zoom in only one point can actually stay at the same position!

Update : If you want that point to be the mouse click location, all you need is a translation that makes it the new origin:

float xPos = -e.Location.X;
float yPos = -e.Location.Y;

Now, when you click in the middle of a square, that rectangle will stay fixed and will grow right around the mouse cursor..

Note: The minus signs are there to make up for the way you wrote your code. Conceptually, you first move the Origin (positive), then you move it back (negative).

Update 2:

The above code change will only work if you don't change the point you zoom in on. To make it work for a series of clicks anywhere it is a little more involved.

The problem is, that there are two distinct views after the 1st zoom:

  • The control you click on reports an unscaled and untranslated point for the mouse location..
  • ..but the user sees and clicks on a scaled and translated graphic.

What we need for the next translations are the points where we would click on the original version. To get these points we can keep another Matrix with the reverse transformations needed to bring the mouse locations back from the perceived to the original coordinates:

// make the zoom factor accessible
float zoom = 1.5f;
// the graphics transformation
Matrix viewMatrix = new Matrix();
// the reverse transformation for the mouse point
Matrix viewMatrixRev = new Matrix();

private void panel1_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == System.Windows.Forms.MouseButtons.Left)
    {
        // first we reverse translate the point
        // we need an array!
        PointF[] tv = new PointF[] { e.Location };
        viewMatrixRev.TransformPoints(tv);
        // after the reversal we can use the coordinates
        float xPos = tv[0].X;
        float yPos = tv[0].Y;

        // revers translation for the point
        Matrix scaleMatrixRev = new Matrix(1f / zoom, 0, 0, 1f / zoom, 0, 0);
        // the other transformations
        Matrix scaleMatrix = new Matrix(zoom, 0, 0, zoom, 0, 0);
        Matrix translateOrigin = new Matrix(1, 0, 0, 1, xPos, yPos);
        Matrix translateBack = new Matrix(1, 0, 0, 1, -xPos, -yPos);

        // we need two different orders, not sure yet why(?)
        MatrixOrder moP = MatrixOrder.Prepend;
        MatrixOrder moA = MatrixOrder.Append;

        // graphics transfomation
        viewMatrix.Multiply(translateOrigin, moP );
        viewMatrix.Multiply(scaleMatrix, moP ); 
        viewMatrix.Multiply(translateBack, moP ); 

        // store the next point reversal:
        viewMatrixRev.Multiply(translateBack, moA); 
        viewMatrixRev.Multiply(scaleMatrixRev, moA); 
        viewMatrixRev.Multiply(translateOrigin, moA); 

    }
    else
    {
        // reset
        viewMatrix = new Matrix();
        viewMatrixRev = new Matrix();
    }
    panel1.Invalidate();
}

Now I can click anywhere and it will zoom in on the mouse.

But why did the zooming work before on any point as long as we didn't change the point? Because the point we clicked on was not moved so it stayed invariant and 'valid' all the time..

BTW, I don't think you need to save the Graphics state in the Paint event. It is reset in each call anyway. - My code is working on a simple Panel; you can adapt it to your OnMouseDown..

TaW
  • 53,122
  • 8
  • 69
  • 111
  • Thanks @TaW, after coming back to this with a clear head you're right, it is working the way it should. However I want a different behavior. I've updated my question to (hopefully) explain the behavior I was looking for. – amura.cxg Jan 12 '15 at 15:35
  • Thank you! That's much closer to the behavior I was looking for, however if your first click say the center of the second rectangle then click again in the bottom left of the same rectangle it doesn't zoom in around the bottom left (it'll zoom slightly away from that point). Is this just a flaw with the way I'm attempting to implement this? – amura.cxg Jan 13 '15 at 16:19
  • You are right. Took me a while to figure it out (and the MatrixOrder is still not quite clear to me!), but now it works for me. - Have a look at the updated answer..! – TaW Jan 14 '15 at 05:35
  • I didn't even think about using a reverse of the view matrix, I tried using the view matrix it self but obviously that won't work. Thank you for the help, I really appreciate it! Also, I have the Graphics state stuff in there as this is just a test app the hopes is the code would be used elsewhere and the resetting the state may be important so I left it in. – amura.cxg Jan 14 '15 at 15:42
  • 1
    Something occurred to me, you could use the inverse of `viewMatrix` to translate the points back instead of keeping a separate `viewMatrixRev`. I tried the code using the inverse of `viewMatrix` and it behaves the same as your code. Regardless, your help was much appreciated as I would have taken me forever to figure this out my self :p – amura.cxg Jan 14 '15 at 15:50
  • Ah, that's clever. I didn't think of that; the Point matrix is one step behind as it's last value is used to create the next graphics matrix but that shouldn't be a problem.. – TaW Jan 14 '15 at 15:54