UWP: Calculate Transformation based on ScrollViewer - c#

I have a windows universal app where I am rendering a scene with DirectX. I want to make use of the Scrollviewer and therefore I render my scene behind the Scrollviewer and want to calculate the scene transformation based on the Scrollviewer. It works fine so far, especially the translation and scrolling. But when I zoom in, the scene jumps around in two special situations:
The scene had enough space and was centered and now scrolling is required.
The opposite direction.
More or less I use the following code:
float zoom = scrollViewer.ZoomFactor;
float inverseZoom = 1f / scrollViewer.ZoomFactor;
float scaledContentW = Document.Size.X * scrollViewer.ZoomFactor;
float scaledContentH = Document.Size.Y * scrollViewer.ZoomFactor;
float translateX;
float translateY;
if (scaledContentW < scrollViewer.ViewportWidth)
{
translateX = ((float)scrollViewer.ViewportWidth * inverseZoom - Document.Size.X) * 0.5f;
}
else
{
translateX = -inverseZoom * (float)scrollViewer.HorizontalOffset;
}
if (scaledContentH < scrollViewer.ViewportHeight)
{
translateY = ((float)scrollViewer.ViewportHeight * inverseZoom - Document.Size.Y) * 0.5f;
}
else
{
translateY = -inverseZoom * (float)scrollViewer.VerticalOffset;
}
float visibleX = inverseZoom * (float)scrollViewer.HorizontalOffset;
float visibleY = inverseZoom * (float)scrollViewer.VerticalOffset; ;
float visibleW = Math.Min(Document.Size.X, inverseZoom * (float)scrollViewer.ViewportWidth);
float visibleH = Math.Min(Document.Size.Y, inverseZoom * (float)scrollViewer.ViewportHeight);
Rect2 visibleRect = new Rect2(visibleX, visibleY, visibleW, visibleH);
transform =
Matrix3x2.CreateTranslation(
translateX,
translateY) *
Matrix3x2.CreateScale(zoom);
You can get an example here: https://github.com/SebastianStehle/Win2DZoomTest
To be sure that my eyes are not broken I was zooming around and have written the translation and zoom values to a file. You can see it here:
https://www.dropbox.com/s/9ak6ohg4zb1mnxa/Test.png?dl=0
The meaning of the columns is the following:
Column 1: The computed zoom value of the transformation matrix (M11) = ScrollViewer.ZoomFactor
Column 2: The computed x offset of the matrix (See above)
Column 3: The x value of the result of matrix * vector (500, 500), here: Colum1 * 500 + Column2
You see, that the matrix values look good, but when applying the transformation you get this little jump to the right for some milliseconds. One theory was, that the viewport might change because the scrollbar becomes visible. But this is not the case. I also tried fixed values here, made the scrollbars visible and even created a custom template for the scrollviewer with no scrollbars at all.
Btw: This is a cross post, I also asked the question here: https://github.com/Microsoft/Win2D/issues/125

You see this behavior because when you zoom bigger than the ScrollViewer's size, the zoom center point is moved. To fix this, you just need to subscribe to the ScrollViewer's LayoutUpdated event and inside the handler, manually keep its content in the center.
private void ScrollViewer_LayoutUpdated(object sender, object e)
{
this.ScrollViewer.ChangeView(this.ScrollViewer.ScrollableWidth / 2, this.ScrollViewer.ScrollableHeight / 2, this.ScrollViewer.ZoomFactor, true);
}
This should fix the jumpy movement on the two drawed Rectangles from Win2D.
Update
Just to prove my point, the jumpy behavior is most likely due to unusual translate x and/or y value change when the content size goes over the size of the ScrollViewer. So I wrote some code to log these values on the screen as shown below -
...
this.Test1.Text += ((float)translateX).ToString() + " ";
this.Test2.Text += ((float)translateY).ToString() + " ";
session.Transform =
Matrix3x2.CreateTranslation(
(float)translateX,
(float)translateY) *
Matrix3x2.CreateScale((float)zoom);
Now look at the numbers on the image above. What I did was I tried zooming in until the jumpy scene occurred. See the highlighted translate y value? It is slightly greater than its previous value, which is against the declining trend.
So to fix this, you will need to be able to skip these unusual values caused by ScrollViewer.

Related

How to scale Matrix4x4 around point?

I'm writing a custom editor window in Unity in which I would like to be able to both scroll in/out and drag the view around. To do so, I've been setting GUI.matrix to Matrix4x4.TRS(offset, Quaternion.identity, Vector3.one * scale), where I have control over offset and scale. This works fine, except when scrolling in/out, it anchors the top left of the window. I would like it to anchor on the mouse's position.
If this just requires changing the offset when zooming, that's great - I'm just not sure what the offset should be here. Matrix4x4s are out of my comfort zone for math.
Here is how I'm currently handling zooming:
if (Event.current.type == EventType.ScrollWheel)
{
_scale *= Math.Sign(Event.current.delta.y) == 1 ? 1.1f : 1f / 1.1f;
_offset += Math.Sign(Event.current.delta.y) * /*What do I put here?*/;
}
Let's try to understand whatthe GUI matrix does. It represents a transform that takes coordinates in world space (where your GUI objects live) and converts them to GUI space (more or less aligned with your window). Since we have no rotation, we can easily interpret what constructing the matrix with TRS() does to a world-space point pWorld:
pGUI = scale * pWorld + offset
Now you want to change scale to scaleNew. In doing so, you want to keep the same world position under the mouse.
If your mouse position is given in GUI space (e.g., from Event.current.mousePosition), then we first need to find the corresponding world space point:
v3World = (1.0 / scaleOld) * (v3GUI - offsetOld)
And we want to fix this point under the mouse, i.e.:
v3GUI = scaleNew * v3World + offsetNew
v3GUI = scaleNew / scaleOld * (v3GUI - offsetOld) + offsetNew
We can solve this to get the new offset:
v3GUI = scaleNew / scaleOld * v3GUI - scaleNew / scaleOld * offsetOld + offsetNew
(1 - scaleNew / scaleOld) * v3GUI + scaleNew / scaleOld * offsetOld = offsetNew
And that's it.
Btw, you can also do this with matrix operations alone. This is what GUIUtility.ScaleAroundPivot() does. This is how it looks:
newMatrix = T(v3GUI) * S(newScale / oldScale) * T(-v3GUI) * oldMatrix
T represents a translation and S a scaling. The translation pair T(v3GUI) and T(-v3GUI) move the temporary origin of the coordinate system to your mouse position and perform the scaling from there. You could then directly read offset and scale from this matrix.

C# Drawing a radar scaling and centering problems

I would like to draw a radar on a pictureBox. Drawing points is no problem but I am struggling with basic maths. Maybe I am too tired.
I have a pictureBox that is 200x200. I have loaded a small, centered image inside the picturebox (4x4) which symbolizes the current player.
I have build a function called
PaintRadar(int meX, int meY, int enemyX, int enemyY)
The parameters could have the following values: meX = 27000, meY = 30000, enemyX = 26000, enemyY = 28000
The desired result is to have the enemies around me and I am always centered in the pictureBox. What do I have to calculate to center meX and meY in the pictureBox?
Thanks
Assume the player is in the middle of the enemies and draw the enemies around the center based on the difference between their positions and the player's position.
Think about it as though the player is the origin. By subtracting the player's position from the enemy's position you are putting the enemy position into a coordinate system with the player at the center. This is essentially what you're radar is.
Example:
// Get differences. d is short for difference (or delta :)).
int dy = enemyY - meY;
int dx = enemyX - meX;
// Then scale the dy and dx values so they fix in the picture box.
dy *= scaleY;
dx *= scaleX;
Then you would draw the enemies at (dx,dy) on the picture box.
Scale should be a formula like this:
scaleY = (1 / maxDetectionDistance) * (heightOfRadarBox / 2);
scaleX = (1 / maxDetectionDistance) * (widthOfRadarBox / 2);
Anything greater than your radar's limit should not be drawn.
// Don't draw if enemy is too far away for radar to pick up.
if (Math.Abs(dy) > maxDetectionDistance || Math.Abs(dx) > maxDetectionDistance)
{
return;
}

Moving mouse in direction and rate

I'm trying to create a simple mouse emulator controlled by a joystick's right thumbstick. I was trying to have the mouse move in the direction the stick pointed with a smooth gradient of pressure values dictating speed, but I've hit a number of snags when trying to do so.
The first is how to accurately translate the angle into accurate X and Y values. I can't find a way to implement the angle correctly. The way I have it, the diagonals are likely to move considerably faster than the cardinals.
I was thinking I need something like Math.Cos(angle) for the X values, and Math.Sin(angle) for the Y values to increment the mouse, but I can't think of a way to set it up.
The second, is smooth movement of the mouse, and this is probably the more important of the two. Since the SetPosition() function only works with integers, the rate at which pixels move over time seems very limited. The code I have is very basic, and only registers whole number values of 1-10. That not only creates small 'jumps' in acceleration, but limits diagonal movement as well.
The goal would to have something like 10 pixels-per-second, with the program running at 100hz, and each cycle outputting 0.1 pixel movement.
I'd imagine I might be able to keep track of the pixel 'decimals' for the X and Y values and add them to the axes when they build to whole numbers, but I'd imagine there's a more efficient way to do so and still not anger the SetPosition() function.
I feel like Vector2 objects should get this done, but I don't know how the angle would fit in.
Sample code:
//Poll Gamepad and Mouse. Update all variables.
public void updateData(){
padOne = GamePad.GetState(PlayerIndex.One, GamePadDeadZone.None);
mouse = Mouse.GetState();
currentStickRX = padOne.ThumbSticks.Right.X;
currentStickRY = padOne.ThumbSticks.Right.Y;
currentMouseX = mouse.X;
currentMouseY = mouse.Y;
angle = Math.Atan2(currentStickRY, currentStickRX);
vectorX = (int)( currentStickRX*10 );
vectorY = (int)( -currentStickRY*10 );
mouseMoveVector.X = vectorX;
mouseMoveVector.Y = vectorY;
magnitude = Math.Sqrt( Math.Pow( (currentStickRX - 0), 2 ) + Math.Pow( (currentStickRY - 0), 2 ) );
if (magnitude > 1){
magnitude = 1;
}
//Get values not in deadzone range and re-scale them from 0-1
if(magnitude >= deadZone){
activeRange = (magnitude - deadZone)/(1 - deadZone);
}
Console.WriteLine(); //Test Code
}
//Move mouse in in direction at specific rate.
public void moveMouse(){
if (magnitude > deadZone){
Mouse.SetPosition( (currentMouseX + vectorX), (currentMouseY + vectorY));
}
previousStickRX = currentStickRX;
previousStickRY = currentStickRY;
previousActiveRange = activeRange;
}
Note: I'm using all the xna frameworks.
Anyway, apologies if I'm explaining these things incorrectly. I haven't been able to find a good resource for this, and the vector examples I searched only move in integer increments and from point A to B.
Any help with any part of this is greatly appreciated.
I haven't tried it myself but from my point of view, you should normalize the pad axis after reading them, that way diagonals would move the same speed as cardinals. And for the second part, I would keep track of the mouse in floating variables, such as a Vector2 and do the cast (maybe rounding better) when setting the mouse position.
public void Start()
{
mousePosV2 = Mouse.GetState().Position.ToVector2();
}
public void Update(float dt)
{
Vector2 stickMovement = padOne.ThumbSticks.Right;
stickMovement.Normalize();
mousePosV2 += stickMovement*dt*desiredMouseSpeed;
/// clamp here values of mousePosV2 according to Screen Size
/// ...
Point roundedPos = new Point(Math.Round(mousePosV2.X), Math.Round(mousePosV2.Y));
Mouse.SetPosition(roundedPos.X, roundedPos.Y);
}

Updating sprite's alpha as Scale increases

I have a sprite which size changes from 0.6f to 2.0f during an animation.
At the same time I want its alpha value to change from 1.0f to 0 as its scale increase. So the sprite is fully opaque at 0.6f and gradually disappear until the size is 2.0f where it becomes totally transparent.
How do I correlate those values?
spriteBatch.Draw(texture, Vector2.Zero, Color.White*alpha,null,0f,origin,scale,1f);
Where
alpha = MathHelper.Clamp(0, 1 , 1 - (scale - startScale)/deltaScale);
In your case
startScale = 0.6f,deltaScale = 2.0f-0.6f = 1.4f;
It's going to be easier and more expandable if you animate each value separately. In this case scale and alpha, but the same logic could be applied to anything.
Animations are a function of time, so the variable you're missing here is how long you want the animation to last.
Each animation has a startValue and an endValue. From that you can derive 'change per second' like this:
var changePerSecond = (endValue - startValue) / totalSeconds;
Then in each frame, you simply increment the desired property by multiplying by the frame time (deltaTime), e.g.
scale += changePerSecond * deltaTime;
Okay, with the basic concept out of the way you can see how it would work for scale and alpha in your animation (using a 1 second animation as an example).
var scalePerSecond = (2.0f - 0.6f) / 1.0f;
var alphaPerSecond = (0.0f - 1.0f) / 1.0f;
Then in your update method..
scale += scalePerSecond * deltaTime;
alpha += alphaPerSecond * deltaTime;
Note that this is an example of the simplest linear animation off the top of my head to demonstrate the concept. You'll likely need other code to detect the end of the animation and what to do when it finishes.

Move a 2D sprite in a curved upward arc in XNA?

Alright, so here's my problem.
I've been trying to create a sort of a visual day/night cycle in XNA, where I have an underlying class that updates and holds time and a Sky class which outputs a background based on the time that the class updates.
What I can't figure out though is how to make the moon/sun move in a curved upward arc that spans the screen based on what time of the day it is. The most problematic part is getting the Y axis to curve while the X axis moves as the time progresses.
Anyone that could help me here?
EDIT:
Alright, looks like Andrew Russels example helped me to do what I needed to do.
Although I had to expermient for a bit, I finally reached a suitable solution:
float Time = (float)Main.inGameTime.Seconds / (InGameTime.MaxGameHours * 60 * 60 / 2);
this.Position.X = Time * (Main.Viewport.X + Texture.Width * 2) - Texture.Width;
this.Position.Y = Main.Viewport.Y - (Main.Viewport.Y * (float)Math.Sin(Time * MathHelper.Pi) / 2) - (Main.Viewport.Y / 2) + 50;
Try looking at the Math.Sin or Math.Cos functions. These are the trigonometric functions you're looking for.
Something like this (giving a position for SpriteBatch):
float width = GraphicsDevice.Viewport.Width;
float height = GraphicsDevice.Viewport.Height;
float time = 0.5f; // assuming 0 to 1 is one day
Vector2 sunPosition = new Vector2(time * width,
height - height * (float)Math.Sin(time * width / MathHelper.TwoPi));
(Disclaimer: I haven't tested this code.)
There is also the Curve class.

Categories

Resources