Dynamically scaling canvas in WPF by setting in- and outpoints - c#

I'm working on plotting program in WPF using the canvas element. What I want to achieve is a scrollbar with draggable endpoints. Example of these kinds of scrollbars are in the After Effects video editing software by Adobe.
Basic functionality of such a scrollbar is that it is able to scroll trough content that is bigger then it's container, but both the left and right endpoint can be dragged to dynamically change the scale of the content.
I have implemented a similar scrollbar in the plotting program; users should be able to drag around the in and outpoint (Rectangles in a canvas), and the plot canvas should respond to this by scaling to the desired range.
Information I need for this:
Width of the total plot (amount of plotpoints)
Width of the container (static, 600px)
Percentage of the in and out points relative to the total width of the scrollbar canvas
Link to current screenshot
With this information I have created a MatrixTransform, using the ScaleAt() method to scale the plot canvas inside the container so that it matches the in and outpoints in the scrollbar below. For this I used the following code. resetTransform gets called FPS times a second to keep up with the incoming data and XMAX and YMAX are updated elsewhere to reflect this.
public void resetTransform(Boolean useSlider = false)
{
//Add transformgroup to plot
double yscale = plot.Height / view.YMAX; //YMAX is maximum plot value received
double xscale = plot.Width / view.XMAX; //XMAX is total ammount of plotted points
Matrix m = new Matrix(1, 0, 0, 1, 0, 0);
if (useSlider)
{
double maxVal = zoomBar.ActualWidth - outPoint.Width;
double outP = Canvas.GetLeft(outPoint); //points position relative to the scrollbar
double inP = Canvas.GetLeft(inPoint);
double center = (((outP + inP) / 2) / maxVal) * plot.ActualWidth;
double delta = (outP-inP);
double factor = (maxVal/delta) * xscale;
double mappedinP = (inP / maxVal) * view.XMAX;
double anchorOut = (outP / maxVal) * view.XMAX;
double anchorIn = (inP / maxVal) * view.XMAX;
m.ScaleAt(factor, -yscale,center,0); //scale around the center point,
m.Translate(0, plot.Height); //to compensate the flipped graph, move it back down
}
scale = new ScaleTransform(m.M11, m.M22, 0, 0); //save scale factors in a scaletransform for reference
signals.scaleSignalStrokes(scale); //Scale the plotlines to compensate for canvas scaling
MatrixTransform matrixTrans = new MatrixTransform(m); //Create matrixtransform
plot.RenderTransform = matrixTrans; //Apply to canvas
}
Expectation: Everything should work and the plotted graph would scale nicely when the amount of plotpoints grows over time. Reality: The graph scales when moving the points around, but it is not representative; Moreover, the more plot points are added, the more the whole canvas shifts to the right and the less control I seem to have over the transformation. The algorithm as it is now is probably to wrong approach to get the result I need, but I have spent quite some time thinking how to do this right.
Update
I have uploaded a video to give a clearer picture on the interaction. In the video you can clearly see the canvas shifting to the right.
Screencapture video
How should I scale the canvas (plot) to fit within two boundaries?

So, after some struggling, I found the right algorithm to solve this problem. I will post the adjusted version of the resetTransform function below:
//Reset graph transform
public void resetTransform(Boolean useSlider = false)
{
double yscale = plot.Height / view.YMAX; //YMAX is maximum plot value received
double xscale = plot.Width / view.XMAX; //XMAX is total ammount of plotted points
Matrix m = new Matrix(1, 0, 0, 1, 0, 0);
if (useSlider)
{
double maxVal = zoomBar.ActualWidth - outPoint.Width;
double outP = Canvas.GetLeft(outPoint); //points position relative to the scrollbar
double inP = Canvas.GetLeft(inPoint);
double delta = (outP-inP);
double factor = (maxVal/delta) * xscale;
anchorOut = (outP / maxVal) * view.XMAX; //Define anchorpoint coordinates
anchorIn = (inP / maxVal) * view.XMAX;
double center = (anchorOut +anchorIn)/2; //Define centerpoint
m.Translate(-anchorIn, 0); //Move graph to inpoint
m.ScaleAt(factor, -yscale,0,0); //scale around the inpoint, with a factor so that outpoint is plot.Height(=600px) further away
m.Translate(0, plot.Height); //to compensate the flipped graph, move it back down
}
scale = new ScaleTransform(m.M11, m.M22, 0, 0); //save scale factors in a scaletransform for reference
signals.scaleSignalStrokes(scale); //Scale the plotlines to compensate for canvas scaling
MatrixTransform matrixTrans = new MatrixTransform(m); //Create matrixtransform
plot.RenderTransform = matrixTrans; //Apply to canvas
}
So rather than scaling around the centre point, I should first translate the image and then scale around the origin of the canvas with a factor. This means the other side of the canvas is exactly plot.Height pixels away (with some added scaling)
Everything seems to work fine now, but because I am using custom controls (draggable Rectangles in a canvas) I notice these rectangles do not always fire the mousse events.
Since it is out of the scope of this question, I've described the issue further in this post

Related

Why aren't the CenterX and CenterY of my ScaleTransform not working the same for my TextBlock as my lines?

I've got a canvas where, among other things, I have many groups of points that I created by making two lines in a cross along with a TextBlock with the points name. When the user zooms in on this canvas, I want the user to be able to scale up or down the points in order to view them easier.
The problem I'm having is specifically the scaling of the textblock. I need both the two lines as well as the textblock to scale to the CanvasCenter (center of the crosshairs the respective point). The two lines scale perfectly towards the center when I set the transforms CenterX and CenterY to the the center but the textblock scales way off, towards the bottom right of the canvas.
Am I misunderstanding what exactly centerX and centerY are?
public void ScaleDown()
{
ScaleTransform lineTransform = new ScaleTransform();
ScaleTransform textTransform = new ScaleTransform();
scale *= 0.90;
lineTransform.ScaleX = scale;
lineTransform.ScaleY = scale;
lineTransform.CenterX = CanvasCenter[0];
lineTransform.CenterY = CanvasCenter[1];
textTransform.ScaleX *= scale;
textTransform.ScaleY *= scale;
textTransform.CenterX = CanvasCenter[0];
textTransform.CenterY = CanvasCenter[1];
NameText.RenderTransform = textTransform;
Line1.RenderTransform = lineTransform;
Line2.RenderTransform = lineTransform;
}
I have tried scaling to the center and I expected it to move closer to that center, but it moved in a completely different direction.

Recompute Panel AutoScrollPosition after zoom

Have C# forms application with PictureBox embedded in Panel, to take advantage of Panel AutoScroll as suggested in other posts when image and thus PictureBox need to be scrolled horizontally or vertically. Want to zoom the image and recompute AutoScrollPosition to keep same Point visible after zooming. Can double size of PictureBox, then recopy source image, accomplishing zoom. But AutoScrollPosition remains unchanged, thus what was visible before zoom has moved off screen.
How to recompute AutoScrollPosition to keep image focus after zoom?
There are three typical types of zooming:
zoom into the center, triggered by zoom buttons
zoom into the mouse position, triggered by clicking or scroll-wheeling
zoom into a rectangle, by drawing a rectangle
I assume the typical setup: A PictureBox set to SizeMode=Zoom nested in a Panel with AutoScroll=true and zooming that takes care to keep the aspect ratios of Image and PictureBox equal.
Let's start by introducing terminology:
There is an Image we call bitmap and
it is displayed by a PictureBox; let's call it canvas..
.. which is nested in a Panel we call frame
User-friendly zooming needs a fixed point, that is a point that shall stay put.
For 1) it is the center of the frame, for 2) it is the mouse location and for 3) it is the center of the rectangle.
Before zooming we calculate the old zoom ratio, the fixed point in the frame, the fixed point in the canvas and finally the fixed point in the bitmap.
After zoming we calculate the new zoom ratio and the new fixed point in the canvas. Finally we use it to move the canvas to bring the fixed canvas point to the fixed frame point.
Here is an example for zooming into the (current) center; it is a common click event for two buttons and it only doubles and halves the zoom ratio.
Much finer grained factors are of course simple to implement; even better is a fixed list of zoom levels, like Photoshop has!
private void zoom_Click(object sender, EventArgs e)
{
PictureBox canvas = pictureBox1;
Panel frame = panel1;
// Set new zoom level, depending on the button
float zoom = sender == btn_ZoomIn ? 2f : 0.5f;
// calculate old ratio:
float ratio = 1f * canvas.ClientSize.Width / canvas.Image.Width;
// calculate frame fixed pixel:
Point fFix = new Point( frame.Width / 2, frame.Height / 2);
// calculate the canvas fixed pixel:
Point cFix = new Point(-canvas.Left + fFix.X, -canvas.Top + fFix.Y );
// calculate the bitmap fixed pixel:
Point iFix = new Point((int)(cFix.X / ratio),(int)( cFix.Y / ratio));
// do the zoom
canvas.Size = new Size( (int)(canvas.Width * zoom), (int)(canvas.Height * zoom) );
// calculate new ratio:
float ratio2 = 1f * canvas.ClientSize.Width / canvas.Image.Width;
// calculate the new canvas fixed pixel:
Point cFix2 = new Point((int)(iFix.X * ratio2),(int)( iFix.Y * ratio2));
// move the canvas:
canvas.Location = new Point(-cFix2.X + fFix.X, -cFix2.Y + fFix.Y);
}
Note that while one can try to restore the relative AutoScrollValues this is not only hard, because their values are a little quirky but it is also won't be adaptable to the other zoom types.

C# WPF Image Point to Point animation

I need to pan an image at a preset Scale level (no zoom). When panning from left to right, I need the left-side of the image to disappear as the leading right edge appears. This would give the image the appearance of a video as the camera pans the scene. What I've done so far:
double origX = 0.0;
double origY = 0.5;
double newx;
double newy;
image1.RenderTransformOrgin = new Point(origX, origY);
image1.RenderTransform = new ScaleTransform(2.0, 2.0);
if (newX == 0.0)
{
image1.RenderTransformOrgin = new Point(origX, origY);
}
else
{
image1.RenderTransformOrgin = new Point(newX, newY);
}
Using this code I am able to start the virtual "camera lens" on the left and then move it to any other point of the image. By manually increasing newX's value incrementally I am able to create the effect I need. My question is how best to increase the newX value to produce a smooth, animated appearance? Thank You.

Prevent image clipping during rotation Windows Phone

I am using the WriteableBitmapEx extension method to rotate a WriteableBitmap. bi in the code is a WritableBitmap. The RotateFree method rotates the bitmap in any degree and returns a new rotated WritableBitmap. My code:
private void rotate()
{
degree += 1;
var rotate = bi.RotateFree(degree);
ImageControl.Source = rotate;
}
My problem is since the ImageControl size is fixed, it causes the rotated bitmap to be clipped. So what is the best way to prevent this? I guess I am looking for a way to resize the ImageControl during rotation to prevent clipping. Any suggestions?
UPDATE
Based on this useful info Calculate rotated rectangle size from known bounding box coordinates I think I managed to calculate the bounding box width (bx) and height(by) and resize it accordingly during the rotation
double radian = (degree / 180.0) * Math.PI;
double bx = rotate.PixelWidth * Math.Cos(radian) + rotate.PixelHeight * Math.Sin(radian);
double by = rotate.PixelWidth * Math.Sin(radian) + rotate.PixelHeight * Math.Cos(radian);
While it appears that the ImageControl width and height increases/decreases during rotation, the image is still being clipped.
UPDATE 2
Based on #Rene suggestion, I managed to prevent the clipping. Combined with the ImageControl Width/Height calculation, the image size is retained during rotation by also setting its stretch property to NONE.
The issue now is to make sure the ImageControl resize from its center so that it does not appear moving. I can include a sample project if anyone interested
UPDATE 3
For those who might be interested the final solution. This is how I do it. The result is, the image is rotated without clipping and its size is retained during rotation. In addition, the rotation appears to originate from the center.
To adjust the ImageControl position as it's resizing so that the rotation appears to originated from center, I use this code.
var translationDelta = new Point((ImageControl.ActualWidth - bx) / 2.0, (ImageControl.ActualHeight - by) / 2.0);
UpdateImagePosition(translationDelta);
ApplyPosition();
// This code update the ImageControl position on the canvas
public void UpdateImagePosition(Point delta)
{
var newPosition = new Point(ImagePosition.X + delta.X, ImagePosition.Y + delta.Y);
ImagePosition = newPosition;
}
//This apply the new position to make the ImageControl rotate from center
public void ApplyPosition()
{
ObjComposite.TranslateX = ImagePosition.X;
ObjComposite.TranslateY = ImagePosition.Y;
}
Use RotateFree with the crop parameter set to false: RotateFree(degree, false). Also set the ImageControl Stretch property to Uniform: Stretch="Uniform".
rene

Invalidate a single Pixel?

I'm working on some image processing and for debug I'm overlaying colours on the original bitmap.
The problem is the image is rendered in a picture box with SizeMode set to Zoom and invalidating every time I update a pixel is Really slow and just gets slower the larger picturebox is (for the same size image)
What I think might help is if I only invalidate the pixel(s) I've changed but I don't know how convert the co-ordinates of the pixel I've changed into a rectangle rendered on the control. Obviously if the image is being drawn larger than the original image then the rectangle I'm invalidating is going to be more than one pixel
Added a method to get the zoom and padding of the picture pox
private void CalculateZoomAndPadding()
{
Double imageAspect = (Double)pictureBox1.Image.Width / (Double)pictureBox1.Image.Height;
Double pbAspect = (Double)pictureBox1.Width / (Double)pictureBox1.Height;
Boolean heightRestricted = imageAspect < pbAspect;
hPadding = 0;
vPadding = 0;
if (heightRestricted)
{
zoom = (Double)pictureBox1.Height / (Double)pictureBox1.Image.Height;
Double imageWidth = (Double)pictureBox1.Image.Width * zoom;
hPadding = (Double)(pictureBox1.Width - imageWidth) / 2d;
}
else
{
zoom = (Double)pictureBox1.Width / (Double)pictureBox1.Image.Width;
Double imageHeight = (Double)pictureBox1.Image.Height * zoom;
vPadding = (Double)(pictureBox1.Height - imageHeight) / 2d;
}
}
then to invalidate a pixel called invalidate like this:
pictureBox1.Invalidate(new Rectangle(Convert.ToInt32(Math.Floor(x * zoom)) + Convert.ToInt32(hPadding) -1, Convert.ToInt32(Math.Floor(y * zoom)) + Convert.ToInt32(vPadding) -1, PixelSize, PixelSize));
when I first did this I only invalidated the are directly covered by the pixel but found that this was subject to rounding errors so expanded it to include a few extra.
Can you change all the pixels and then just invalidate the image once?
I'd just add a timer that fires 30 or 60 times per second that invalidates the whole control. While there might be a slight delay in updating you shouldn't be able to notice it due to your monitor's refresh rate most likely being 60 Hz only anyway.

Categories

Resources