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();
}
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.
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);
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:
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
..
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With