I am developing a game for Android using LibGDX. I have added pinch zoom and pan. My issue is how to keep from going outside of the play area. As it is, you can pan outside of the play area into blackness. When zoomed out fully I know how to deal with it, I just said:
if(camera.zoom == 1.0f) ;
else {
}
But, if zoomed in, how do I accomplish this. I know this is not that complicated, I just can't seem to figure it out. Upon creation I set the camera to the middle of the screen. I know how to pan, I am using camera.translate(-input.deltaX, -input.deltaY, 0), I just need to test before this call to see if the position is outside of the play area. When I am zoomed in, how do I test if I am at the edge of the screen?
You can use one of
camera.frustum.boundsInFrustum(BoundingBox box)
camera.frustum.pointInFrustum(Vector3 point)
camera.frustum.sphereInFrustum(Vector3 point, float radius)
to check if a point/box/sphere is within your camera's view.
What I normally do is define 4 boxes around my world where the player should not be allowed to see. If the camera is moved and one of the boxes is in the frustum, I move the camera back to the previous position.
Edit: AAvering has implemented this in code below.
Credit goes to Matsemann for idea, here is the implementation I used.
Make a custom MyCamera class extending OrthographicCamera and add the following code:
BoundingBox left, right, top, bottom = null;
public void setWorldBounds(int left, int bottom, int width, int height) {
int top = bottom + height;
int right = left + width;
this.left = new BoundingBox(new Vector3(left - 2, 0, 0), new Vector3(left -1, top, 0));
this.right = new BoundingBox(new Vector3(right + 1, 0, 0), new Vector3(right + 2, top, 0));
this.top = new BoundingBox(new Vector3(0, top + 1, 0), new Vector3(right, top + 2, 0));
this.bottom = new BoundingBox(new Vector3(0, bottom - 1, 0), new Vector3(right, bottom - 2, 0));
}
Vector3 lastPosition = new Vector3();
#Override
public void translate(float x, float y) {
lastPosition.set(position.x, position.y, 0);
super.translate(x, y);
}
public void translateSafe(float x, float y) {
translate(x, y);
update();
ensureBounds();
update();
}
public void ensureBounds() {
if (frustum.boundsInFrustum(left) || frustum.boundsInFrustum(right) || frustum.boundsInFrustum(top) || frustum.boundsInFrustum(bottom)) {
position.set(lastPosition);
}
}
Now, in you custom sceene or whathever you use (in my case it was a custom Board class) call:
camera.setWorldBounds()
and in your GestureListener.pan method you can call
camera.translateSafe(x, y);
it should keep your camera in bounds
Here's the code I call after the position of the camera is updated due to panning or zooming in my 2D game using an orthographic camera. It corrects the camera position so that it doesn't show anything outside the borders of the play area.
float camX = camera.position.x;
float camY = camera.position.y;
Vector2 camMin = new Vector2(camera.viewportWidth, camera.viewportHeight);
camMin.scl(camera.zoom/2); //bring to center and scale by the zoom level
Vector2 camMax = new Vector2(borderWidth, borderHeight);
camMax.sub(camMin); //bring to center
//keep camera within borders
camX = Math.min(camMax.x, Math.max(camX, camMin.x));
camY = Math.min(camMax.y, Math.max(camY, camMin.y));
camera.position.set(camX, camY, camera.position.z);
camMin is the lowest left corner that the camera can be without showing anything outside of the play area and is also the offset from a corner of the camera to the center.
camMax is the opposite highest right location the camera can be in.
The key part I'm guessing you're missing is scaling the camera size by the zoom level.
Here's my solution:
float minCameraX = camera.zoom * (camera.viewportWidth / 2);
float maxCameraX = worldSize.x - minCameraX;
float minCameraY = camera.zoom * (camera.viewportHeight / 2);
float maxCameraY = worldSize.y - minCameraY;
camera.position.set(Math.min(maxCameraX, Math.max(targetX, minCameraX)),
Math.min(maxCameraY, Math.max(targetY, minCameraY)),
0);
Where:
targetX and targetY are world coordinates of where your target is.
worldSize is a Vector2 of the size of the world.
I don't have enough reputation to write comments, so I'll point to some previous answers.
AAverin's solution with bounding box that's made with Matsemann's idea isn't good because it annoyingly slows when you are near the one edge (boundary) and trying to translate diagonally in which case you are panning to one side out of bounds and other in proper direction.
I strongly suggest that you try solution from the bottom of handleInput method presented at
https://github.com/libgdx/libgdx/wiki/Orthographic-camera
That one works smoothly, and some of the previous answers look like that one but this one uses MathUtils.clamp wihch is a straight forward and much cleaner.
Perfect class for this, (partly thanks to AAverin)
This class not only sticks into the bounds it also snaps into the bounds when you zoom.
Call these for setting bounds and moving the camera.
camera.setWorldBounds()
camera.translateSafe(x, y);
When zooming call
camera.attemptZoom();
And here's the class:
public class CustomCamera extends OrthographicCamera
{
public CustomCamera() {}
public CustomCamera(float viewportWidth, float viewportHeight)
{
super(viewportWidth, viewportHeight);
}
BoundingBox left, right, top, bottom = null;
public void setWorldBounds(int left, int bottom, int width, int height) {
int top = bottom + height;
int right = left + width;
this.left = new BoundingBox(new Vector3(left - 2, 0, 0), new Vector3(left -1, top, 0));
this.right = new BoundingBox(new Vector3(right + 1, 0, 0), new Vector3(right + 2, top, 0));
this.top = new BoundingBox(new Vector3(0, top + 1, 0), new Vector3(right, top + 2, 0));
this.bottom = new BoundingBox(new Vector3(0, bottom - 1, 0), new Vector3(right, bottom - 2, 0));
}
Vector3 lastPosition;
#Override
public void translate(float x, float y) {
lastPosition = new Vector3(position);
super.translate(x, y);
}
public void translateSafe(float x, float y) {
translate(x, y);
update();
ensureBounds();
update();
}
public void ensureBounds()
{
if(isInsideBounds())
{
position.set(lastPosition);
}
}
private boolean isInsideBounds()
{
if(frustum.boundsInFrustum(left) || frustum.boundsInFrustum(right) || frustum.boundsInFrustum(top) || frustum.boundsInFrustum(bottom))
{
return true;
}
return false;
}
public void attemptZoom(float newZoom)
{
this.zoom = newZoom;
this.snapCameraInView();
}
private void snapCameraInView()
{
float halfOfCurrentViewportWidth = ((viewportWidth * zoom) / 2f);
float halfOfCurrentViewportHeight = ((viewportHeight * zoom) / 2f);
//Check the vertical camera.
if(position.x - halfOfCurrentViewportWidth < 0f) //Is going off the left side.
{
//Snap back.
float amountGoneOver = position.x - halfOfCurrentViewportWidth;
position.x += Math.abs(amountGoneOver);
}
else if(position.x + halfOfCurrentViewportWidth > viewportWidth)
{
//Snap back.
float amountGoneOver = (viewportWidth - (position.x + halfOfCurrentViewportWidth));
position.x -= Math.abs(amountGoneOver);
}
//Check the horizontal camera.
if(position.y + halfOfCurrentViewportHeight > viewportHeight)
{
float amountGoneOver = (position.y + halfOfCurrentViewportHeight) - viewportHeight;
position.y -= Math.abs(amountGoneOver);
}
else if(position.y - halfOfCurrentViewportHeight < 0f)
{
float amountGoneOver = (position.y - halfOfCurrentViewportHeight);
position.y += Math.abs(amountGoneOver);
}
}
}
The CustomCamera class given doesn't work very well. I used it to map a pinch gesture to zoomSafe and the camera would bounce/flash from left to right constantly when on the edge of the bounds. The camera also doesnt work properly with panning. If you try to pan along the edge of the bounds it doesnt pan anywhere as if the edges are "sticky". This is because it just translates back to the last position instead of just adjusting the coordinate that it outside the bounds.
Related
In the picture below, the left screen shows the starting state of the animation and the right screen shows the ending state.
What the animation does is that while the list view is being moved from top to bottom, the Welcome label will gradually move up, and the profile image view will gradually scale smaller and move to the top right corner.
When the list view is being moved back to the top, the reversed animation will happen.
I would like to ask how I can do that?
The picture about the starting and ending states of the animation
Try this transition , It might not be perfect you might have to fix a little bit
Take 2 imageviews starting an ending of animation . make ending animation imageview Invisible
scrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
int scrollY = scrollView.getScrollY();
interpolate(start_profile_imageView, end_profile_imageView, scrollY );
}
});
Add these in the same class
private RectF mRect1 = new RectF();
private RectF mRect2 = new RectF();
private void interpolate(View view1, View view2, float interpolation) {
getOnScreenRect(mRect1, view1);
getOnScreenRect(mRect2, view2);
float scaleX = 1.0F + interpolation * (mRect2.width() / mRect1.width() - 1.0F);
float scaleY = 1.0F + interpolation * (mRect2.height() / mRect1.height() - 1.0F);
float translationX = 0.5F * (interpolation * (mRect2.left + mRect2.right - mRect1.left - mRect1.right));
float translationY = 0.5F * (interpolation * (mRect2.top + mRect2.bottom - mRect1.top - mRect1.bottom));
view1.setTranslationX(translationX);
view1.setTranslationY(translationY);
view1.setScaleX(scaleX);
view1.setScaleY(scaleY);
}
private RectF getOnScreenRect(RectF rect, View view) {
rect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
return rect;
}
Also check out this project
I have a camera set up in LibGDX which draws what is a HUD-like layer of buttons and status. In the middle of this, I want a block of text, which could be longer than the screen, so I want to be able to pan around it using gestures.
I was thinking, the way to do this would be to define a second camera, with an large viewport, i.e.:
textCamera = new OrthographicCamera(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
textCamera.setToOrtho(true, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
And then apply it to my batch before writing out the bulk of the text. However, how can I restrict it such that this textCamera will only ever draw contents to the screen between, say, (0, 100) -> (600, 800). In this case the screen width, just for example, is 600 wide, and the height is maybe 1000 so I want to leave a gap at the top and bottom.
So, basically I want a big viewport, write all text out to viewport, be able to view it at 1:1 scale, but also be able to pan around the text. Just like you do when you pan up and down a website while surfing on an Android.
I think you want a scissor stack? This lets you define a rectangular sub-region of the display to render to, and only pixels inside that rectangle will be rendered.
Scissors API
One of the answers to this question has an example of using the scissor stack:
https://gamedev.stackexchange.com/questions/67024/how-do-i-crop-a-cameras-viewport
The libgdx scissorstack wiki page is pretty weak, but shows how to use it with a SpriteBatch.
You should create second stage for a HUD and then add to it ActorGestureListener with defined pan method. You can easily control its camera position by checking if the position is not bigger/lower than some value in the method.
Stage hudStage; //create it with even the same viewport as stage and add to it all hud's actors
...
hudStage.addListener(aListener);
...
final float MAX_X = 100, MIN_X = -100, MAX_Y = 100, MIN_Y = -100;
ActorGestureListener aListener = new ActorGestureListener()
{
#Override
public void pan(InputEvent event, float x, float y, float deltaX, float deltaY)
{
//if you want to move slower you can divide deltaX and deltaY by some value like:
//deltaX /= 5f;
if( stage.getCamera().position.x + deltaX < MAX_X && stage.getCamera().position.x + deltaX > MIN_X )
{
stage.getCamera().position.x += deltaX;
}
if( stage.getCamera().position.y + deltaY < MAX_Y && stage.getCamera().position.y + deltaY > MIN_Y )
{
stage.getCamera().position.y += deltaY;
}
}
};
What's up guys, I need a little help with this one. I'm trying to achieve a simple(but not really) folding animation on a listview that is being scrolled. Basically, I'm attempting to fold the listview's first visible child backward as if a sheet of paper is being folded downward along the X axis. This goes on on continuously as the user scrolls up and down the list. This is my first time playing around with Matrix animations and Android's camera from the graphics api, so I'm definitely off the mark here.
This is the effect I'm trying to achieve
And this is the effect I'm getting.
I want the animation to begin at the origin(0,0) but both the left and right side, animating from the top of the list item instead of the upper left corner. I'm not very familiar with matrix translations or animations so If anyone much more experience with these techniques than myself can shed some knowledge, it'll be greatly appreciated.
Basically I'm overriding the onDrawChild method of ListView, grabbing the child's bitmap from a drawing cache, and using a matrix to perform the animation. The lighting and camera implementation is code that I took from another sample app in order to get the animation to look as 3D as possible.
I tried playing around with the ListView animations library, but without much luck. I also tried to hack together a solution using code from the developer guides here that uses object animators to achieve a nice little card flip animation, but it started feeling a bit hacky and I couldn't quite get it the way I wanted.
Here's my current implementation. If anyone can shed some light or direction on this one, or maybe if anyone wrote an awesome library that I didn't come across on my searches, please feel free to share. Thanks
#Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
View first = getChildAt(0);
if (child == first) {
if (child.getTop() < 0) {
Bitmap bitmap = getChildDrawingCache(child);
final int top = child.getTop();
child.getRight();
child.getBottom();
child.getLeft();
final int childCenterY = child.getHeight() / 2;
// final int childCenterX = child.getWidth() / 2;
final int parentCenterY = getHeight() / 2; // center point of
// child relative to list
final int absChildCenterY = child.getTop() + childCenterY;
// final int bottom = child.getBottom();
// distance of child center to the list center final int
int distanceY = parentCenterY - absChildCenterY;
final int r = getHeight() / 2;
if (mAnimate) {
prepareMatrix(mMatrix, distanceY, r);
mMatrix.preTranslate(0, top);
mMatrix.postTranslate(0, -top);
}
canvas.drawBitmap(bitmap, mMatrix, mPaint);
}
else {
super.drawChild(canvas, child, drawingTime);
}
} else {
super.drawChild(canvas, child, drawingTime);
}
return false;
}
private void prepareMatrix(final Matrix outMatrix, int distanceY, int r) { // clip
// the
// distance
final int d = Math.min(r, Math.abs(distanceY)); //
// circle formula
final float translateZ = (float) Math.sqrt((r * r) - (d * d));
double radians = Math.acos((float) d / r);
double degree = 45 - (180 / Math.PI) * radians;
// double degree = -180;
mCamera.save();
mCamera.translate(0, 0, r - translateZ);
mCamera.rotateX((float) degree);
if (distanceY < 0) {
degree = 360 - degree;
}
mCamera.rotateY((float) degree);
mCamera.getMatrix(outMatrix);
mCamera.restore();
// highlight elements in the middle
mPaint.setColorFilter(calculateLight((float) degree));
}
private Bitmap getChildDrawingCache(final View child) {
Bitmap bitmap = child.getDrawingCache();
if (bitmap == null) {
child.setDrawingCacheEnabled(true);
child.buildDrawingCache();
bitmap = child.getDrawingCache();
}
return bitmap;
}
private LightingColorFilter calculateLight(final float rotation) {
final double cosRotation = Math.cos(Math.PI * rotation / 180);
int intensity = AMBIENT_LIGHT + (int) (DIFFUSE_LIGHT * cosRotation);
int highlightIntensity = (int) (SPECULAR_LIGHT * Math.pow(cosRotation,
SHININESS));
if (intensity > MAX_INTENSITY) {
intensity = MAX_INTENSITY;
}
if (highlightIntensity > MAX_INTENSITY) {
highlightIntensity = MAX_INTENSITY;
}
final int light = Color.rgb(intensity, intensity, intensity);
final int highlight = Color.rgb(highlightIntensity, highlightIntensity,
highlightIntensity);
return new LightingColorFilter(light, highlight);
}
JazzyListView
has a lot of stuff that's similar to what you want if not exactly what you want. Take a look at how they're defined under jazzy effect and mix and match. I think reverse fly or maybe flip is close to what you want.
I'm new to this so don't blame me. I am trying to develop an android app that would make music. I am trying to make a bar that rotates over a bunch of buttons that are displayed in the form of a circle, and when it does to play the sound represented by every button. However so far I managed to make an image rotate around the middle of the screen by setting x and y coordinates representing the centre of the circle, but when I try to put the formula (x + radius*sin(angle)), (y + radius*cos(angle)), it just moves the image I want to rotate at that point. So basically I am trying to rotate an Image around an circle defined by buttons or coordinates rather then an actual circle image. So I need to rotate an image or imageView around a circle, not just a point.
I have added the code ass well so you could have a look at what I'm doing wrong.
ImageView bara = (ImageView) findViewById(R.id.floating_image);
layoutParams[9] = new RelativeLayout.LayoutParams
(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
toop = Math.round(size.x/2); // + 90*Math.sin(ANGLE));
lefft = Math.round(size.y/2); // + 90*Math.cos(ANGLE));
top = (int) toop;
left = (int) lefft;
layoutParams[9].setMargins(top, left, 0, 0);
bara.setLayoutParams(layoutParams[9]);
RotateAnimation rAnim = new RotateAnimation(0.0f, 360.0f, Animation.RELATIVE_TO_SELF, 0 , Animation.RELATIVE_TO_SELF, 0);
rAnim.setRepeatCount(Animation.INFINITE);
rAnim.setInterpolator(new LinearInterpolator());
rAnim.setDuration(8000);
bara.startAnimation(rAnim);
Any help would be really appreciated !!
the code looks like :
private float mCalcX;//x-coord of object
private float mCalcY;//y-coord of object
private double mCenterX;//x-coord of center of circle
private double mCenterY;//y-coord of center of circle
private double mRadius;//circle radius
private double mAngleRadians;//angle of your object to draw in RADs
// whenever you draw the object, calculate the new X and Y coords
mCalcX = (float) (mCenterX+(mRadius*Math.cos(mAngleRadians)));
mCalcY = (float) (mCenterY+(mRadius*Math.sin(mAngleRadians)));
public void setRadius(double r)
{
mRadius = r;
}
public void setStartingAngle(double radians)
{
mAngleRadians = radians;
}
public void setRotationSpeed(double radians)
{
mRotationSpeed = radians;
}
public void increaseRotationAngle()
{
mAngleRadians += mRotationSpeed;
}
public void decreaseRotationAngle()
{
mAngleRadians -= mRotationSpeed;
}
x^2 + y^2 = r^2
Reference: http://www.mathwarehouse.com/geometry/circle/equation-of-a-circle.php
You should animate the centre of your object around all the (x,y) that satisfy that equation for your chosen value of r (the radius of the circle).
I'm not a graphics guy, so forgive the terseness of my response.
So I have an ImageView using a Matrix to scale the Bitmap I'm displaying. I can double-tap to zoom to full-size, and my ScaleAnimation handles animating the zoom-in, it all works fine.
Now I want to double-tap again to zoom out, but when I animate this with ScaleAnimation, the ImageView does not draw the newly exposed areas of the image (as the current viewport shrinks), instead you see the portion of visible image shrinking in. I have tried using ViewGroup.setClipChildren(false), but this only leaves the last-drawn artifacts from the previous frame - leading to an trippy telescoping effect, but not quite what I was after.
I know there are many zoom-related questions, but none cover my situation - specifically animating the zoom-out operation. I do have the mechanics working - ie aside from the zoom-out animation, double-tapping to zoom in and out works fine.
Any suggestions?
In the end I decided to stop using the Animation classes offered by Android, because the ScaleAnimation applies a scale to the ImageView as a whole which then combines with the scale of the ImageView's image Matrix, making it complicated to work with (aside from the clipping issues I was having).
Since all I really need is to animate the changes made to the ImageView's Matrix, I implemented the OnDoubleTapListener (at the end of this post - I leave it as an "exercise to the reader" to add the missing fields and methods - I use a few PointF and Matrix fields to avoid excess garbage creation). Basically the animation itself is implemented by using View.post to keep posting a Runnable that incrementally changes the ImageView's image Matrix:
public boolean onDoubleTap(MotionEvent e) {
final float x = e.getX();
final float y = e.getY();
matrix.reset();
matrix.set(imageView.getImageMatrix());
matrix.getValues(matrixValues);
matrix.invert(inverseMatrix);
doubleTapImagePoint[0] = x;
doubleTapImagePoint[1] = y;
inverseMatrix.mapPoints(doubleTapImagePoint);
final float scale = matrixValues[Matrix.MSCALE_X];
final float targetScale = scale < 1.0f ? 1.0f : calculateFitToScreenScale();
final float finalX;
final float finalY;
// assumption: if targetScale is less than 1, we're zooming out to fit the screen
if (targetScale < 1.0f) {
// scaling the image to fit the screen, we want the resulting image to be centred. We need to take
// into account the shift that is applied to zoom on the tapped point, easiest way is to reuse
// the transformation matrix.
RectF imageBounds = new RectF(imageView.getDrawable().getBounds());
// set up matrix for target
matrix.reset();
matrix.postTranslate(-doubleTapImagePoint[0], -doubleTapImagePoint[1]);
matrix.postScale(targetScale, targetScale);
matrix.mapRect(imageBounds);
finalX = ((imageView.getWidth() - imageBounds.width()) / 2.0f) - imageBounds.left;
finalY = ((imageView.getHeight() - imageBounds.height()) / 2.0f) - imageBounds.top;
}
// else zoom around the double-tap point
else {
finalX = x;
finalY = y;
}
final Interpolator interpolator = new AccelerateDecelerateInterpolator();
final long startTime = System.currentTimeMillis();
final long duration = 800;
imageView.post(new Runnable() {
#Override
public void run() {
float t = (float) (System.currentTimeMillis() - startTime) / duration;
t = t > 1.0f ? 1.0f : t;
float interpolatedRatio = interpolator.getInterpolation(t);
float tempScale = scale + interpolatedRatio * (targetScale - scale);
float tempX = x + interpolatedRatio * (finalX - x);
float tempY = y + interpolatedRatio * (finalY - y);
matrix.reset();
// translate initialPoint to 0,0 before applying zoom
matrix.postTranslate(-doubleTapImagePoint[0], -doubleTapImagePoint[1]);
// zoom
matrix.postScale(tempScale, tempScale);
// translate back to equivalent point
matrix.postTranslate(tempX, tempY);
imageView.setImageMatrix(matrix);
if (t < 1f) {
imageView.post(this);
}
}
});
return false;
}