C++ Zoom into the centre of the screen in 2D coordinates - c++

I'm having difficulty working out the correct calculations in order to zoom into the centre of the screen in 2D coordinates whilst keeping everything in the correct scale.
I have a vector which I use to handle moving around my map editor as follows:
scroll = sf::Vector2<float>(-640.0f, -360.0f);
It's set at -640.0f, -360.0f to make 0,0 the centre of the screen on initialising (based on my window being 1280x720).
My zoom value ranges from 0.1f to 2.0f and it's increased or decreased in 0.05 increments:
zoomScale = zoomScale + 0.05;
When drawing elements on to the screen they are drawn using the following code:
sf::Rect<float> dRect;
dRect.left = (mapSeg[i]->position.x - scroll.x) * (layerScales[l] * zoomScale);
dRect.top = (mapSeg[i]->position.y - scroll.y) * (layerScales[l] * zoomScale);
dRect.width = (float)segDef[mapSeg[i]->segmentIndex]->width;
dRect.height = (float)segDef[mapSeg[i]->segmentIndex]->height;
sf::Sprite segSprite;
segSprite.setTexture(segDef[mapSeg[i]->segmentIndex]->tex);
segSprite.setPosition(dRect.left, dRect.top);
segSprite.setScale((layerScales[l] * zoomScale), (layerScales[l] * zoomScale));
segSprite.setOrigin(segDef[mapSeg[i]->segmentIndex]->width / 2, segDef[mapSeg[i]->segmentIndex]->height / 2);
segSprite.setRotation(mapSeg[i]->rotation);
Window.draw(segSprite);
layerScales is a value used to scale up layers of segments for parallax scrolling.
This seems to work fine when zooming in and out but the centre point seems to shift (an element that I know should always be at 0,0 will be located at different co-ordinates as soon as I zoom). I use the following to calculate what the position as at the mouse to test this as follows:
mosPosX = ((float)input.mousePos.x + scroll.x) / zoomScale)
mosPosY = ((float)input.mousePos.y + scroll.y) / zoomScale)
I'm sure there's a calculation I should be doing to the 'scroll' vector to take into account this zoom but I can't seem to get it to work right.
I tried implementing something like below but it didn't produce the correct results:
scroll.x = (scroll.x - (SCREEN_WIDTH / 2)) * zoomScale - (scroll.x - (SCREEN_WIDTH / 2));
scroll.y = (scroll.y - (SCREEN_HEIGHT / 2)) * zoomScale - (scroll.y - (SCREEN_HEIGHT / 2));
Any ideas what I'm doing wrong?

I will do this the easy way (not most efficient but works fine) and only for single axis (second is the same)
it is better to have offset unscaled:
scaledpos = (unscaledpos*zoomscale)+scrolloffset
know center point should not move after scale change (0 means before 1 means after):
scaledpos0 == scaledpos1
so do this:
scaledpos0 = (midpointpos*zoomscale0)+scrolloffset0; // old scale
scaledpos1 = (midpointpos*zoomscale1)+scrolloffset0; // change zoom only
scrolloffset1+=scaledpos0-scaledpos1; // correct offset so midpoint stays where is ... i usualy use mouse coordinate instead of midpoint so i zoom where the mouse is
when you can not change the scaling equation then just do the same with yours
scaledpos0 = (midpointpos+scrolloffset0)*zoomscale0;
scaledpos1 = (midpointpos+scrolloffset0)*zoomscale1;
scrolloffset1+=(scaledpos0-scaledpos1)/zoomscale1;
Hope I did no silly error in there (writing from memory). For more info see
Zooming graphics based on current mouse position

Related

Getting absolute rectangle coordinates after direct2d translation and scale

I'm using direct2d to draw a bitmap (play a video) in a window, and I want to get the absolute coordinates for any position in the playing space, whether transformations are applied or not. So if the resolution is 1280x720, then by hovering the cursor over the image, I should get values like x = 0 ... 1280, y = 0 ... 720.
The positions of the total video area are in the variable m_rcLiveWindowPos, while the variable m_rcDstVideoRect contains the positions of the actual video after adjusting for the aspect ratio. Finally, m_rcSrcVideoRect is just the video resolution (ex: left=0, top=0, right=1280, bottom=720).
Below, I applied a translation and then a scale to the renderTarget. The rawScaleFactor is a number representing the amount to scale the video: if rawScaleFactor=1, then the video should be played at 100%. If 2, then at 200%.
This all works great -- the video zooms in properly and I can click and drag the video around. The problem is that I want to get the absolute x and y coordinates of the video resolution while my cursor is hovering over the video. The first definitions of mousePosInImage work for videos with no zoom/panning with the m_rcDstVideoRect sitting in a "fitted" position, but the values are incorrect for a zoomed-in video.
if (rawScaleFactor != 0)
{
// Make the dragging more precise based on the scaling factor.
float dragPosX = (float)m_rawScaleOffsetX / (rawScaleFactor * 2.0f);
float dragPosY = (float)m_rawScaleOffsetY / (rawScaleFactor * 2.0f);
D2D1_MATRIX_3X2_F translation = D2D1::Matrix3x2F::Translation(dragPosX, dragPosY);
// Get the center point of the current image.
float centerPointX = float(m_rcLiveWindowPos.Width()) / 2;
float centerPointY = float(m_rcLiveWindowPos.Height()) / 2;
// Calculate the amount that the image must scaled by.
D2D1ScaleFactor = ((float)m_videoResolution.width / (float)(m_rcDstVideoRect.right - m_rcDstVideoRect.left)) * (float)rawScaleFactor;
D2D1_MATRIX_3X2_F scale = D2D1::Matrix3x2F::Scale(D2D1::Size(D2D1ScaleFactor, D2D1ScaleFactor),
D2D1::Point2F(centerPointX, centerPointY));
// First translate the image, then scale it.
m_pJRenderTarget->SetTransform(translation * scale);
int32_t width = ((int32_t)m_videoResolution.width);
int32_t height = ((int32_t)m_videoResolution.height);
// This works for non-zoomed in video:
m_mousePosInImageX = int32_t(width * (rawMousePosX - m_rcDstVideoRect.left) / (m_rcDstVideoRect.right - m_rcDstVideoRect.left));
m_mousePosInImageY = int32_t(height * (rawMousePosY - m_rcDstVideoRect.top) / (m_rcDstVideoRect.bottom - m_rcDstVideoRect.top));
// Does not work for all cases...
m_mousePosInImageX = int32_t((centerPointX * D2D1ScaleFactor) - (centerPointX) + (m_mousePosInImageX / D2D1ScaleFactor));
m_mousePosInImageY = int32_t((centerPointY * D2D1ScaleFactor) - (centerPointY) + (m_mousePosInImageY / D2D1ScaleFactor));
}
m_pJRenderTarget-> DrawBitmap(m_pJVideoBitmap,
m_rcDstVideoRect,
1.0f,
D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR,
m_rcSrcVideoRect);
I need a way to "reflect" the changes that SetTransform() did in the mousePosInImage variables.

How to zoom in on cursor point in Mandelbrot Set?

I'm currently trying to implement a zoom feature for the Mandelbrot Set code I've been working on. The idea is to zoom in/out where I left/right click. So far whenever I click the screen, the fractal is indeed zoomed in. The issue is that the fractal is rendered not at the origin-- in other words, it's not zoomed in on the point I want. I was hoping through here I can get both a code review and conceptual understanding of how to zoom in on a point in general.
Here's how I transformed the pixel coordinate before I used escape algorithm:
MandelBrot.Frag
vec2 normalizedFragPos = (gl_FragCoord.xy/windowSize); //normalize fragment position
dvec2 scaledFragPos = normalizedFragPos*aspectRatio;
scaledFragPos -= aspectRatio/2; //Render the fractal at center of window
scaledFragPos /= scale; //Factor to zoom in or out coordinates.
scaledFragPos -= translation; //Translate coordinate
//Escape Algorithm Below
On my left-click handle, I thought I should convert the cursor position to the same coordinate range as the Mandelbrot Range. So I basically did the same thing I did in the fragment shader:
Window.cpp
float x_coord{ float(GET_X_LPARAM(informaton_long))/size.x }; // normalized mouse x-coordinate
float y_coord{ float(GET_Y_LPARAM(informaton_long))/size.y }; // normalized mouse y-coordinate
x_coord *= aspectRatio[0]; //move point based of relative position to length of window.
y_coord *= aspectRatio[1]; //move point based of relative position to width of window.
x_coord /= scale; //Scale point to match previous zoom factor
y_coord /= scale; //Scale point to match previous zoom factor
translation[0] = x_coord;
translation[1] = y_coord;
//increment scale
scale += .15f;
Lets apply some algebra. Your shader does the following transformation:
mandelbrotCoord = aspectRatio * (gl_FragCoord / windowSize - 0.5) / scale - translation
When we zoom in on mouseCoord, we want to change the scale and adjust the translation such that the madelbrotCoord under the mouse stays the same. To do that we first calculate the mandelbrotCoord under the mouse using the old scale:
mandelbrotCoord = aspectRatio * (mouseCoord / windowSize - 0.5) / scale - translation
Then change the scale (which should be changed exponentially BTW):
scale *= 1.1;
Then solve for the new translation:
translation = aspectRatio * (mouseCoord / windowSize - 0.5) / scale - mandelbrotCoord
Also notice that your system probably reports the mouse coordinate with the y coordinate increasing downwards, whereas OpenGL has its window y coordinate increasing upwards (unless you override it with glClipControl). Therefore you're likely to need to flip the y coordinate of the mouseCoord too.
mouseCoord[1] = windowSize[1] - mouseCoord[1];
For best result I would also adjust the mouse coordinates to be in the middle of the pixel (+0.5, +0.5).
Putting it all together:
float mouseCoord[] = {
GET_X_LPARAM(informaton_long) + 0.5,
GET_Y_LPARAM(informaton_long) + 0.5
};
mouseCoord[1] = size[1] - mouseCoord[1];
float anchor[] = {
aspectRatio[0] * (mouseCoord[0] / size[0] - 0.5) / scale - translation[0],
aspectRatio[1] * (mouseCoord[1] / size[1] - 0.5) / scale - translation[1]
};
scale *= 1.1;
translation[0] = aspectRatio[0] * (mouseCoord[0] / size[0] - 0.5) / scale - anchor[0];
translation[1] = aspectRatio[1] * (mouseCoord[1] / size[1] - 0.5) / scale - anchor[1];
Note: some of the math above might be canceled away. However, if you want to implement a proper pan&zoom functionality (when you can zoom with the mouse wheel while you are panning) then you'll need to store the initial mandelbrotCoord of where the panning started, and then reuse it on subsequent motion and wheel events till the mouse is released. Surprisingly large amount of image viewers get this part wrong!

Sprite rotation offset doesn't stay where it belongs. (SDL)

So, here is the code for my 2D point class to rotate:
float nx = (x * cos(angle)) - (y * sin(angle));
float ny = (y * cos(angle)) + (x * sin(angle));
x = nx;
y = ny;
x and y are local variables in the point class.
And here is the code for my sprite class's rotation:
//Make clip
SDL_Rect clip;
clip.w = width;
clip.h = height;
clip.x = (width * _frameX) + (sep * (_frameX) + osX);
clip.y = (height * _frameY) + (sep * (_frameY) + osY);
//Make a rotated image
col bgColor = image->format->colorkey;
//Surfaces
img *toEdit = newImage(clip.w, clip.h);
img *toDraw = 0;
//Copy the source into the workspace
drawRect(0, 0, toEdit->w, toEdit->h, toEdit, bgColor);
drawImage(0, 0, image, toEdit, &clip);
//Edit the image
toDraw = SPG_Transform(toEdit, bgColor, angle, xScale, yScale, SPG_NONE);
SDL_SetColorKey(toDraw, SDL_SRCCOLORKEY, bgColor);
//Find new origin and offset by pivot
2DVec *pivot = new xyVec(pvX, pvY);
pivot->rotate(angle);
//Draw and remove the finished image
drawImage(_x - pivot->x - (toDraw->w / 2), _y - pivot->y - (toDraw->h / 2), toDraw, _destination);
//Delete stuff
deleteImage(toEdit);
delete pivot;
deleteImage(toDraw);
The code uses the center of the sprite as the origin. It works fine if I leave the pivot at (0,0), but if I move it somewhere else, the character's shoulder for instance, it starts making the sprite dance around as it spins like a spirograph, instead of the pivot staying on the character's shoulder.
The image rotation function is from SPriG, a library for drawing primitives and transformed images in SDL. Since the pivot is coming from the center of the image, I figure the new size of the clipped surface produced by rotating shouldn't matter.
[EDIT]
I've messed with the code a bit. By slowing it down, I found that for some reason, the vector is rotating 60 times faster than the image, even though I'm not multiplying anything by 60. So, I tried to just divide the input by 60, only now, it's coming out all jerky and not rotating to anything between multiples of 60.
The vector rotation code I found on this very site, and people have repeatedly confirmed that it works, so why does it only rotate in increments of 60?
I haven't touched the source of SPriG in a long time, but I can give you some info.
If SPriG has problems with rotating off of center, it would probably be faster and easier for you to migrate to SDL_gpu (and I suggest SDL 2.0). That way you get a similar API but the performance is much better (it uses the graphics card).
I can guess that the vector does not rotate 60 times faster than the image, but rather more like 57 times faster! This is because you are rotating the vector with sin() and cos(), which accept values in radians. The image is being rotated by an angle in degrees. The conversion factor for radians to degrees is 180/pi, which is about 57. SPriG can use either degrees or radians, but uses degrees by default. Use SPG_EnableRadians(1) to switch that behavior. Alternatively, you can stick to degree measure in your angle variable by multiplying the argument to sin() and cos() by pi/180.

CDC::Ellipse doesn't draw correctly circles

In a project of mine (VC++2010, MFC), I want to draw a circle using the CDC::Ellipse. I set two points: the first one is the center of the circle, the second one is a point I want it to be on the circumference.
I pass to the CDC::Ellipse( int x1, int y1, int x2, int y2 ) the coordinates of the upper-left corner and lower-right one.
Briefly: with Pitagora Theorem I calculate the distance between the two points ( radius ), then I subtract this value from the coordinates of the center to obtain the upper-left corner and add to obtain the lower-right one.
When I draw the cirlce and the points, and I zoom in, I see that the second one isn't on the circumference as expected, it is slightly inside unless you set it at 0°, 45°, 90° and so on with respect to the absolute sistem of coordinates.
Then I tried to draw the same circle using CDC::Polyline, I gave to this method the points obtained rotating another point around the center, at the distance equal to the radius. In this case the point is on the circumference every where I set it.
The overlap of these two circles has shown that they perfectly overlap at 0°, 45°, 90° and so on, but the gap is maximum at 22.5°, 67.5° and so on.
Has anyone ever noticed a similar behavior?
Thanks to everybody that can help me!
Code snippet:
this is how I calculate the radius given 2 points:
centerPX = vvFPoint( 1380, 845 );
secondPointPX = vvFPoint( 654,654 );
double radiusPX = (sqrt( (secondPointPX.x - centerPX.x) * (secondPointPX.x - centerPX.x) + (secondPointPX.y - centerPX.y) * (secondPointPX.y - centerPX.y) ));
( vvFPoint is a custom type derived from CPoint )
this is how I draw the "circle" with the CDC::Ellipse:
int up = (int)(((double)(m_p1.y-(double)originY - m_radius) / zoom) + 0.5) + offY;
int left = (int)(((double)(m_p1.x-(double)originX - m_radius) / zoom) + 0.5) + offX;
int down = (int)(((double)(m_p1.y-(double)originY + m_radius) / zoom) + 0.5) + offY;
int right = (int)(((double)(m_p1.x-(double)originX + m_radius) / zoom) + 0.5) + offX;
pDC->Ellipse( left, up, right, down);
(m_p1 is the center of the circle, originX/Y is the origin of the image, m_radius is the radius of the circle, zoom is the scale factor, offX/Y is an offset in the client area of my SW)
this is how I draw the circle "manually" (and quite trivial method) using a custom polyline class:
1) create the array of points:
point.x = centerPX.x + radiusPX;
point.y = centerPX.y;
for ( i=0; i < 3600; i++ )
{
pt1.RotateDeg ( centerPX, (double)0.1 );
poly->AddPoint( pt1 );
}
(RotateDeg is a custom method to rotate a point using first argument as a pivot and second argument as angle value in degrees, AddPoint is a custom method to create the array of points, poly is my custom polyline object).
2) draw it:
When I call the Draw( CDC* pDC ) I use the previous array to draw the polyline:
pDC->MoveTo(p);
I hope this can help you to reproduce my weird observations!
code snippet 2:
void vvPoint<Tipo>::RotateDeg(const vvPoint<Tipo> &center, double angle)
{
vvPoint<Tipo> ptB;
angle *= -(M_PI / 180);
*this -= center;
ptB.x = ((this->x * cos(angle)) - (this->y * sin(angle)));
ptB.y = ((this->x * sin(angle)) + (this->y * cos(angle)));
*this = ptB + center;
}
But to let you better understand my observations I would like to add a few images so you can see where my whole question started from... The problem is: I can't add images since I need to have 10 reputation. I uploaded a .zip file on dropbox and if you want I can send you the URL of this file. Let me know if this is the correct (and safe..) way to bypass this problem.
Thanks!
This might be a possible explanation. As MSDN says about CDC::Ellipse (with my emphasis):
The center of the ellipse is the center of the bounding rectangle
specified by x1, y1, x2, and y2, or lpRect. The ellipse is drawn with
the current pen, and its interior is filled with the current brush.
The figure drawn by this function extends up to, but does not include,
the right and bottom coordinates. This means that the height of the
figure is y2 – y1 and the width of the figure is x2 – x1.
The way you described how you calculate the bounding rectangle is not entirely clear (some source code would have helped) but, given the second paragraph quoted above, you possibly need to add 1 to your x2 and y2 values, to make sure you have a circle with the desired radius.
It's also worth noting that there may be slight rounding differences between your two drawing methods where you have an odd-sized bounding box (i.e. so the centre point falls logically on a half-pixel).
UPDATE
Using your code snippets (thanks), and assuming no zoom and zero offsets etc., I get a radius of 750.704 pixels and the following parameters for the ellipse:
pDC->Ellipse(629, 94, 2131, 1596);
According to MSDN, this means that the ellipse will be drawn in a figure of the following dimensions:
width = (2131 - 629) = 1502
height = (1596 - 94) = 1502
So as far as I can see, this should produce a circle rather than an ellipse.
The next thing to do is to find out how you're drawing the polygon - for that we need to see the implementation of RotateDeg - can you post that code? I'm suspecting some simple rounding error here, that maybe gets magnified when you zoom.
UPDATE 2
Just looking at this code:
for ( i=0; i < 3600; i++ )
{
pt1.RotateDeg ( centerPX, (double)0.1 );
poly->AddPoint( pt1 );
}
You are rotating your polygon points incrementally by 0.1 degrees each time. This will possibly accumulate some errors, so it may be worth doing it something like this instead:
for ( i=0; i < 3600; i++ )
{
vvFPoint ptNew = pt1;
ptNew.RotateDeg ( centerPX, (double)i * 0.1 );
poly->AddPoint( ptNew );
}
Maybe this will mean you have to change your RotateDeg function to take care of the correct quadrants.
One other point, you mentioned that you see the problem when you zoom into the image. If this means you are using you zoom variable, it is worth checking in this line ...:
pDC->Ellipse( left, up, right, down);
... that the parameters still form a square shape, so (right - left) == (down - up).
UPDATE 3
I just ran your RotateDeg function, in its current form, to see how the error accumulates (by feeding in the previous result to the next iteration). At each step, I calculated the distance between the new point and the centre and compared this with the required radius.
The chart below shows the result, where you can see an error of 4 pixels by the time the points have been calculated.
I think that this at least explains part of the difference (i.e. your polygon drawing is flawed) and - depending on zoom - you may introduce asymmetry into the ellipse parameters, which you can debug by comparing the width to the height as I described above.

Calculating vertices of a rotated rectangle

I am trying to calculate the vertices of a rotated rectangle (2D).
It's easy enough if the rectangle has not been rotated, I figured that part out.
If the rectangle has been rotated, I thought of two possible ways to calculate the vertices.
Figure out how to transform the vertices from local/object/model space (the ones I figured out below) to world space. I honestly have no clue, and if it is the best way then I feel like I would learn a lot from it if I could figure it out.
Use trig to somehow figure out where the endpoints of the rectangle are relative to the position of the rectangle in world space. This has been the way I have been trying to do up until now, I just haven't figured out how.
Here's the function that calculates the vertices thus far, thanks for any help
void Rect::calculateVertices()
{
if(m_orientation == 0) // if no rotation
{
setVertices(
&Vertex( (m_position.x - (m_width / 2) * m_scaleX), (m_position.y + (m_height / 2) * m_scaleY), m_position.z),
&Vertex( (m_position.x + (m_width / 2) * m_scaleX), (m_position.y + (m_height / 2) * m_scaleY), m_position.z),
&Vertex( (m_position.x + (m_width / 2) * m_scaleX), (m_position.y - (m_height / 2) * m_scaleY), m_position.z),
&Vertex( (m_position.x - (m_width / 2) * m_scaleX), (m_position.y - (m_height / 2) * m_scaleY), m_position.z) );
}
else
{
// if the rectangle has been rotated..
}
//GLfloat theta = RAD_TO_DEG( atan( ((m_width/2) * m_scaleX) / ((m_height / 2) * m_scaleY) ) );
//LOG->writeLn(&theta);
}
I would just transform each point, applying the same rotation matrix to each one. If it's a 2D planar rotation, it would look like this:
x' = x*cos(t) - y*sin(t)
y' = x*sin(t) + y*cos(t)
where (x, y) are the original points, (x', y') are the rotated coordinates, and t is the angle measured in radians from the x-axis. The rotation is counter-clockwise as written.
My recommendation would be to do it out on paper once. Draw a rectangle, calculate the new coordinates, and redraw the rectangle to satisfy yourself that it's correct before you code. Then use this example as a unit test to ensure that you coded it properly.
I think you were on the right track using atan() to return an angle. However you want to pass height divided by width instead of the other way around. That will give you the default (unrotated) angle to the upper-right vertex of the rectangle. You should be able to do the rest like this:
// Get the original/default vertex angles
GLfloat vertex1_theta = RAD_TO_DEG( atan(
(m_height/2 * m_scaleY)
/ (m_width/2 * m_scaleX) ) );
GLfloat vertex2_theta = -vertex1_theta; // lower right vertex
GLfloat vertex3_theta = vertex1_theta - 180; // lower left vertex
GLfloat vertex4_theta = 180 - vertex1_theta; // upper left vertex
// Now get the rotated vertex angles
vertex1_theta += rotation_angle;
vertex2_theta += rotation_angle;
vertex3_theta += rotation_angle;
vertex4_theta += rotation_angle;
//Calculate the distance from the center (same for each vertex)
GLfloat r = sqrt(pow(m_width/2*m_scaleX, 2) + pow(m_height/2*m_scaleY, 2));
/* Calculate each vertex (I'm not familiar with OpenGL, DEG_TO_RAD
* might be a constant instead of a macro)
*/
vertexN_x = m_position.x + cos(DEG_TO_RAD(vertexN_theta)) * r;
vertexN_y = m_position.y + sin(DEG_TO_RAD(vertexN_theta)) * r;
// Now you would draw the rectangle, proceeding from vertex1 to vertex4.
Obviously more longwinded than necessary, for the sake of clarity. Of course, duffymo's solution using a transformation matrix is probably more elegant and efficient :)
EDIT: Now my code should actually work. I changed (width / height) to (height / width) and used a constant radius from the center of the rectangle to calculate the vertices. Working Python (turtle) code at http://pastebin.com/f1c76308c