maintaining relative position when changing scaleX and scaleY? - actionscript-3

I am trying to implement a simple "zoom" function in a map presentation type app. The user interacts with a NumericStepper to dial in a scale value and I then use that value to set the scaleX and scaleY properties of my map sprite. The parent of the map sprite has a scrollRect defined so the map is cropped as it scales. That all seems to work fine.
Naturally when I change the scale, the visible content shifts as the sprite becomes larger or smaller. I would like to keep the content in relatively the same screen location. I've taken a first pass at it below but it's not quite right.
Question: Am I on the right track here thinking that I can determine how much to shift the x/y by comparing the change in the width/height of the sprite after scaling? (as I write this I am thinking I can determine the center of the sprite before scaling, then reposition it so it stays centered over that point. Hmm. . .).
protected function scaleStepper_changeHandler(event:Event):void
{
var cX:Number = wrapper.x + (wrapper.width /2);
var cY:Number = wrapper.y + (wrapper.height /2);
wrapper.scaleX = scaleStepper.value;
wrapper.scaleY = scaleStepper.value;
wrapper.x = cX - (wrapper.width /2);
wrapper.y = cY - (wrapper.height /2);
}

You are on the right track, but for a better solution you should use a matrix to transform your Sprite. Use the following code below to achieve what you need:
private var originalMatrix:Matrix;
private function scaleAroundPoint(target:Sprite, scalePoint:Point, scaleFactor:Number):void
{
if(originalMatrix == null)
originalMatrix = target.transform.matrix;
var matrix:Matrix = originalMatrix.clone();
matrix.translate(-scalePoint.x, -scalePoint.y);
matrix.scale(scaleFactor, scaleFactor);
matrix.translate(scalePoint.x, scalePoint.y);
target.transform.matrix = matrix;
}
You can call this method like this:
scaleAroundPoint(wrapper, new Point(yourWidth/2, yourHeight/2), scaleStepper.value);
Hope this helps and solves your problem.

Am I on the right track here thinking that I can determine how much to shift the x/y by comparing the change in the width/height of the sprite after scaling?
Yes. As all values are known, you don't really have to "test" after scaling. You basically want to distribute the movement of the bounding box borders evenly.
Here's an example in one dimension, scaling factor 2, X is the registration point, | a boundary:
before scaling |--X--|
after scaling |----X----|
No problem there. Now what if the registration point is not in the middle?
before scaling |-X---|
after scaling |--X------|
As a last example, the edge case with the registration point on the boundary:
before scaling |X----|
after scaling |X--------|
Note how the boundaries of all 3 examples are equal before scaling and within each example, the registration point remains constant.
The problem is clearly identified. Now how to solve this?
We do know how much the width changes
before scaling width
after scaling width * scaleFactor
and from the first example we can determine where the left boundary should be after scaling (assuming that the registration point is at 0, so the object is centered):
before scaling -width * 0.5
after scaling -width * 0.5 * scaleFactor
This value depends on where the registration point of course is within the display object relative to the left boarder. To circumvent this dependency, subtract the values from each other to know how much the left boundary is moved to the left after scaling while keeping the object centered:
boundary shift width * 0.5 * (scaleFactor - 1)
Comparing before and after scaling, the left boundary should be further to the left by that amount and the right boundary should be further to the right by that amount.
The problem is that you cannot really set the left or right boundary directly.
You have to set the registration point, which will influence where the boundaries are. To know how far you should move the registration point, imagine both edge cases:
before scaling |X----|
after scaling |X--------|
corrected, |X--------|
before scaling |----X|
after scaling |--------X|
corrected, |--------X|
In both cases, the registration point has to be moved by the amount which the boundary should move, because essentially, the registration point is on the boundary and thus behaves the same way.
Any value in between can be found by linearly interpolating between both cases:
-[width * 0.5 * (scaleFactor - 1)] <= value <= +[width * 0.5 * (scaleFactor - 1)]
-[width * 0.5 * (scaleFactor - 1)] * (1-t) + [width * 0.5 * (scaleFactor - 1)] * t
To find the interpolation value t, which is 0 if X is on the left and 1 when on the right:
t = (X - L) / width
Add -[width * 0.5 * (scaleFactor - 1)] * (1-t) + [width * 0.5 * (scaleFactor - 1)] * t to the x position of the registration point and the scale the object.
Do the same for y in a similar fashion.

Related

LWJGL Picking - Select Certain Block When Hovering ( gluUnProject() )

This video will show my current situation, and I currently can't find any answers to it online.
https://www.youtube.com/watch?v=O8Mh-1Emoc8&feature=youtu.be
My Code:
public Vector3D pickBlock() {
glDisable(GL_TEXTURE);
IntBuffer viewport = BufferUtils.createIntBuffer(16);
FloatBuffer modelview = BufferUtils.createFloatBuffer(16);
FloatBuffer projection = BufferUtils.createFloatBuffer(16);
FloatBuffer winZ = BufferUtils.createFloatBuffer(1);
float winX, winY;
FloatBuffer position = BufferUtils.createFloatBuffer(3);
glGetFloat(GL_MODELVIEW_MATRIX, modelview);
glGetFloat(GL_PROJECTION_MATRIX, projection);
glGetInteger(GL_VIEWPORT, viewport);
winX = (float)Display.getWidth() / 2;
winY = (float)viewport.get(3) - (float)Display.getHeight() / 2;
glReadPixels(Display.getWidth() / 2, (int)winY, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, winZ);
gluUnProject(winX, winY, winZ.get(), modelview, projection, viewport, position);
glEnable(GL_TEXTURE);
return new Vector3D(position.get(0) / 2 + 0.5f, position.get(1) / 2 + 0.5f, position.get(2) / 2 + 0.5f);
}
It returns "/ 2 + 0.5f" because that is needed because of the offsets I have for the blocks (if I removed the 0.5f, the offset would be in the center instead of the corner)
I seams to me that the error, based on the video, comes from when you are facing in the positive z direction (or whatever your back direction is). My guess is that you aren't taking the facing direction into account as I see in your code that you are just adding a constant 0.5F to the position of your cursor.
Therfore, when you are facing backwards, it adds 0.5 which makes it be behind the wall (since back is negative Z). one simple check would be weather the Z component of your forward vector is positive or negative, and deciding the factor added to the cursor based on that, then doing the same for the X.
Depending on how you implemented your camera (IE: if you used Euler angles (rx, ry, rz) or if you used Quaternions / forward vectors), the way you would do that check would vary, feel free to ask me for examples based on your system if you need.
hope this helped!
PS: if you're using angles, you can either check for the range of the y-axis rotation value and determine which direction you are facing and thus weather to add or subtract, OR you can calculate the forward vector based on your angles, and then check the for sign of the component.

Libgdx Tiled map object to box2d body location

A Tiled map object hat a position x, y in pixels and a rotation in degrees.
I am loading the coordinates and the rotation from the map and trying to assign them to a box2d Body. There are a couple of differences between the location models, for example Tiled object rotation is in degrees and box2d body angle is in radians.
How do I convert the location to the BodyDef coordinates x, y and angle so that the body will be created at the correct position?
Background:
Using the code:
float angle = -rotation * MathUtils.degreesToRadians;
bodyDef.angle = angle;
bodyDef.position.set(x, y);
Works when the rotation is 0, but the body is not positioned correctly when rotation is different than 0.
I found a couple of hints here:
http://www.tutorialsface.com/2015/12/qu ... dx-solved/
and here:
https://github.com/libgdx/libgdx/issues/2742
That seem to tackle this exact problem, however neither solution worked for me, the body objects are still positioned wrong after applying those transformations. By positioned wrong I mean that the body is positioned in the area of the map where it should be but slightly off depending on its rotation.
I feel that it should be pretty simple but I do not know how to mediate the differences between Tiled and box2d locations.
For reference these are the two solutions I tried from the links above (after transforming the values x, y, width, height from pixels to world units):
float angle = rotation * MathUtils.degreesToRadians;
bodyDef.angle = -angle;
Vector2 correctionPosition = new Vector2(
height * MathUtils.cosDeg(-rotation - 90),
height + height * MathUtils.sinDeg(-rotation - 90));
bodyDef.position.set(x, y).add(correctionPosition);
and
float angle = rotation * MathUtils.degreesToRadians;
bodyDef.angle = -angle;
// Top left corner of object
Vector2 correctedPosition = new Vector2(x, y + height);
// half of diagonal for rectangular object
float radius = (float)Math.sqrt(width * width + height * height) / 2.0f;
// Angle at diagonal of rectangular object
float theta = (float)Math.tanh(height / width) * MathUtils.degreesToRadians;
// Finding new position if rotation was with respect to top-left corner of object.
// X=x+radius*cos(theta-angle)+(h/2)cos(90+angle)
// Y=y+radius*sin(theta-angle)-(h/2)sin(90+angle)
correctedPosition = correctedPosition
.add(
radius * MathUtils.cos(theta - angle),
radius * MathUtils.sin(theta - angle))
.add(
((height / 2) * MathUtils.cos(MathUtils.PI2 + angle)),
(-(height / 2) * MathUtils.sin(MathUtils.PI2 + angle)));
bodyDef.position.set(correctedPosition);
Any hint would be highly welcomed.
Found the correct solution, lost about 1 day of my life :)
The information from above links is incorrect and/or outdated. Currently Tiled saves the object position depending on it's type. For an image is relative to bottom-left position.
Box2d doesn't really have an "origin" point, but you can consider is its center and the shapes of the fixtures attached to the body should be positioned relative to (0,0).
Step 1: Read tiled properties
float rotation = textureMapObject.getRotation();
float x = textureMapObject.getX();
float y = textureMapObject.getY();
float width = textureMapObject.getProperties()
.get("width", Float.class).floatValue();
float height = textureMapObject.getProperties()
.get("height", Float.class).floatValue();
Step 2: Scale these acording to your box2d world size, for example x = x * 1/25; etc.
Step 3: Create a body without any position or angle.
Step 4: Transform body position and angle with:
private void applyTiledLocationToBody(Body body,
float x, float y,
float width, float height,
float rotation) {
// set body position taking into consideration the center position
body.setTransform(x + width / 2, y + height / 2, 0);
// bottom left position in local coordinates
Vector2 localPosition = new Vector2(-width / 2, -height / 2);
// save world position before rotation
Vector2 positionBefore = body.getWorldPoint(localPosition).cpy();
// calculate angle in radians
float angle = -rotation * MathUtils.degreesToRadians;
// set new angle
body.setTransform(body.getPosition(), angle);
// save world position after rotation
Vector2 positionAfter = body.getWorldPoint(localPosition).cpy();
// adjust position with the difference (before - after)
// so that the bottom left position remains unchanged
Vector2 newPosition = body.getPosition()
.add(positionBefore)
.sub(positionAfter);
body.setTransform(newPosition, angle);
}
Hope it helps.

clearRect issue in animation

Im having issue with clearRect, i have an image u can move up and down and which follow the angle where the mousse is but in some frames of the animation the clearRect let a small edge of the previous image state ( 'this' reference to the image and 'ctx' is the 2d context, 'this.clear()' is called each frame before redrawing the image at the new coordinates )
this.clear = function(){
game.ctx.save();
game.ctx.translate(this.x+this.width/2, this.y+this.height/2);//i translate to the old image center
game.ctx.rotate(this.angle);//i rotate the context to the good angle
game.ctx.clearRect(this.width/-2, this.height/-2, this.width, this.height);//i clear the old image
game.ctx.restore();
};
if i replace the clearRect line by
game.ctx.clearRect(this.width/-2-1, this.height/-2-1, this.width+2, this.height+2);
it works but its not the logical way
The problem is that you are only clearing at position half the width/height, not position minus half the width/height.
Regarding anti-aliasing: when you do a rotation there will be anti-aliased pixels regardless of the original position being integer values. This is because after the pixels relative positions are run through the transformation matrix their offsets will in most cases be float values.
Try to change this line:
game.ctx.clearRect(this.width/-2, this.height/-2, this.width, this.height);
to this instead including compensation for anti-aliased pixels (I'll split the lines for clearity):
game.ctx.clearRect(this.x - this.width/2 - 1, /// remember x and y
this.y - this.height/2 - 1,
this.width + 2,
this.height + 2);

Zoom to and from point

I'm trying to zoom a DisplayObject into a certain point. I figured it would be easy, but I've spent a day now trying to figure it out.
Basically, I think this should work. Emphasis on should.
//newPoint is the point being centered. There is no initial scaling, so I do not need to compensate for that (yet)
//scale is the zoom level
//container is the parent of the obj
//obj is the object being scaled/panned
var p:Point = new Point(
( this.container.width - this.obj.width * scale + newPoint.x * scale ) / 2,
( this.container.height - this.obj.height * scale + newPoint.y * scale ) / 2
);
this.obj.scaleX = this.obj.scaleY = scale;
this.obj.x = p.x;
this.obj.y = p.y;
It centers the point if scale is 1, but it gets further and further away from center as you increase the scale. I've tried dozens of different methods. This method, which I have seen on several sites, produced the same exact results. Anyone have any idea how to get this to work?
EDIT 10-1-12:
As a followup, I took the code snippet that LondonDrugs_MediaServices provided as a basis for my original issue. I needed to be able to zoom to a specific point at a specific scale relative to the unscaled image (think how Google Maps zooms to a specific location). To do this, I had to center my image on the point before running the translation code. I've posted the additional code below. For other uses (pinch to zoom, scrolling, and double click), I used the code provided by Vesper, which worked quite well.
//obj is the object being translated
//container is its parent
//x and y are the coordinates to be zoomed to, in untranslated scaling
//obj.scaleX and obj.scaleY are always identical in my class, so there is no need to account for that
//calculates current center point, with scaling
var center:Point = new Point( ( this.container.width - this.obj.width * this.obj.scaleX ) / 2, ( this.container.height - this.obj.height * this.obj.scaleX ) / 2 );
//calulcates the distance from center the point is, with scaling
var distanceFromCenter:Point = new Point( this.obj.width * this.obj.scaleX / 2 - x * this.obj.scaleX, this.obj.height * this.obj.scaleX / 2 - y * this.obj.scaleX );
//center the object on that specific point
this.obj.x = center.x + distanceFromCenter.x;
this.obj.y = center.y + distanceFromCenter.y;
var mat:Matrix=new Matrix();
mat.translate(-p.x,-p.y);
mat.scale(desiredScale,desiredScale);
mat.translate(p.x,p.y);
yourObject.transform.matrix=mat;
The core point is that scaling is done around (0,0), but you can do it with matrix that describes affine transformations. You first make an empty matrix (that is, a matrix that doesn't transform), then apply a set of transformations to it. First, place a desired point at (0,0) by translating by -1*coordinates, then scale, then translate back.
hie guys....
thank's your comments...
i found the answer...
code :
gambar.addEventListener(TransformGestureEvent.GESTURE_ZOOM , onZoom);
function onZoom(event:TransformGestureEvent):void {
var locX:Number=event.localX;
var locY:Number=event.localY;
var stX:Number=event.stageX;
var stY:Number=event.stageY;
var prevScaleX:Number=gambar.scaleX;
var prevScaleY:Number=gambar.scaleY;
var mat:Matrix;
var externalPoint=new Point(stX,stY);
var internalPoint=new Point(locX,locY);
gambar.scaleX *= event.scaleX;
gambar.scaleY *= event.scaleY;
if(event.scaleX>1 && gambar.scaleX>6){
gambar.scaleX=prevScaleX;
gambar.scaleY=prevScaleY;
}
if(event.scaleY>1 && gambar.scaleY>6){
gambar.scaleX=prevScaleX;
gambar.scaleY=prevScaleY;
}
if(event.scaleX<1 && gambar.scaleX<0.8){
gambar.scaleX=prevScaleX;
gambar.scaleY=prevScaleY;
}
if(event.scaleY<1 && gambar.scaleY<0.8){
gambar.scaleX=prevScaleX;
gambar.scaleY=prevScaleY;
}
mat=gambar.transform.matrix.clone();
MatrixTransformer.matchInternalPointWithExternal(mat,internalPoint,externalPoint);
gambar.transform.matrix=mat;
}
The matrix answer is absolutely correct, but if you happen to be a Club GreenSock member you can get some nice functionality with very simple code with the TransformAroundPointPlugin
http://www.greensock.com/as/docs/tween/com/greensock/plugins/TransformAroundPointPlugin.html
You can see an example in the plugin explorer here:
http://www.greensock.com/tweenlite/#plugins
I use this to tween all my zooms and have much better performance than when I tried to do this manually. IMO the whole library is worth it's weight in gold (and no I have no connection other than liking the library). If you need any of the other features I'd look into it. It also has the ThrowProps plugin ( http://www.greensock.com/throwprops/ )which is very important if you are going to have a bounding box on mobile that you want to have a smooth return into the boundaries.
Set obj.x to -p.x and obj.y to -p.y, set the container scaleX and scaleY to the desired value and add p.x to the container x and p.y to the container y. Done!

Setting X Coordinate from Mouse Location

i have a darkBlueRect rectangle sprite and a copy of the same sprite with a larger scale and a lighter color - lightBlueRect.
i'm attempting to shift the location of the lightBlueRect according to the mouse.x location when the mouse is moving over the darkBlueRect. this way if the mouse is on the right of the darkBlueRect than the location of the scaled up lightBlueRect will be on the opposite side and shifted towards the left proportionate to the mouse position and scale. in addition, the lightBlueRect must appear "locked" to the darkBlueRect so lightBlueRect.x must never be more than darkBlueRect.x and lightBlueRect.x + lightBlueRect.width must never be less than darkBlueRect.x + darkBlueRect.width.
the image below depicts 3 states of what i'm attempting to accomplish:
State A: mouse.x is over darkBlueRect.x = 1 and both sprites are aligned to the left.
State B: mouse.x is in the middle of darkBlueRect and both sprites are aligned to the middle.
State C: mouse.x is on the last pixel of darkBlueRect and both sprites are aligned to the right.
for this example, the darkBlueRect.width is equal to 170 and the lightBlueRect.width is equal to 320, or a scale of 1.89.
each time the mouse changes it's x position over darkBlueRect the following is called. however, while my current code works for the most part, it's not exactly correct. when the mouse.x is over darkBlueRect.x = 1, as shown in State A, the lightBlueRect.x is not property aligned with darkBlueRect and appears less than darkBlueRect.x.
var scale:Number = 1.89;
lightBlueRect.x = darkBlueRect.x - Math.round((mouse.x * scale) / darkBlueRect.width * (lightBlueRect.width - darkBlueRect.width));
what equation can i use so that no matter the scale of the lightBlueRect it's first position (mouse over first pixel) and last position (mouse over last pixel) will result in the 2 sprites being aligned as well as property proportionate positioning in between?
[EDIT] the coordinates of the darkBlueRect is {0, 0}, so when the lightBlueRect moves towards the left it is moving into the negative. i could have simply written my code (what doesn't work) like this instead:
var scale:Number = 1.89;
lightBlueRect.x = 0 - Math.round((mouse.x * scale) / darkBlueRect.width * (lightBlueRect.width - darkBlueRect.width));
[EDIT 2]
when the display objects are small, the problem is difficult to notice. however, when they are large the problem becomes move obvious. the problem, here, being that the objects on the left side are misaligned.
also the problem is probably exasperated by the fact that both the lightBlueRect and darkBlueRect are scalable. darkBlueRect is scaled down and lightBlueRect is scaled up.
here is a link to the test displaying the problem. mousing over the shape quickly will obviously result in inaccurate alignment since it's based on frame rate speed, but this is not my concern. still, when you slowly mouse over the shape it will not align correctly on the left side when the mouse is over the first pixel of darkBlueRect: http://www.geoffreymattie.com/test/test.html
[SWF(width = "1000", height = "600", backgroundColor = "0xCCCCCC")]
import flash.display.Sprite;
import flash.events.MouseEvent;
var downScale:Number = 0.48;
var upScale:Number = 2.64;
var darkBlueRect:Sprite = createSprite();
darkBlueRect.scaleX = darkBlueRect.scaleY = downScale;
darkBlueRect.x = stage.stageWidth / 2 - darkBlueRect.width / 2;
darkBlueRect.y = stage.stageHeight / 2 - darkBlueRect.height / 2;
addChild(darkBlueRect);
var lightBlueRect:Sprite = createSprite();
lightBlueRect.scaleX = lightBlueRect.scaleY = upScale;
lightBlueRect.y = stage.stageHeight / 2 - lightBlueRect.height / 2;
lightBlueRect.x = stage.stageWidth;
lightBlueRect.mouseEnabled = false;
addChild(lightBlueRect);
darkBlueRect.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveEventHandler);
function mouseMoveEventHandler(evt:MouseEvent):void
{
lightBlueRect.x = darkBlueRect.x + Math.max(0.0, Math.min(darkBlueRect.mouseX / darkBlueRect.width * downScale, 1.0)) * (darkBlueRect.width - lightBlueRect.width);
}
function createSprite():Sprite
{
var result:Sprite = new Sprite();
result.graphics.beginFill(0x0000FF, 0.5);
result.graphics.drawRect(0, 0, 700, 200);
result.graphics.endFill();
return result;
}
i believe the problem is that the scaling of the shapes.
Assuming you have a Clamp function handy, and that width is floating-point so that division works as expected:
lBR.x = dBR.x + Clamp((mouse.x - dBR.x) / dBR.width, 0, 1) * (dBR.width - lBR.width);
(You can define Clamp(x, m, M) = min(max(x, m), M) if you don't have one.)