Ensure arbitrarily rotated CGRect fills another when rotation occurs - ios

Update: partially working implementation below.
I've asked a couple questions on this previously here and here.
The first works great to determine if the "image" rect is sufficiently contained inside the "crop" rect.
The second works a little bit, but something's off in my implementation of it that it doesn't really work.
I'm now looking at the problem a little differently, and would like to change the behavior:
When the user begins to rotate the image, I'll run the check method (below) to determine if it needs fixing or not.
If it does need fixing, rather than waiting until the user has finished rotating it, I'd like to resize the image simultaneously to fit the bounds. Is there a simpler (or more reliable) way to implement this behavior?
I'm going to block rotation greater than 35º in either direction so that we don't have to worry about severe enlargements.
Assumptions/Constraints:
I'm using AutoLayout
Point of rotation will be the center of the crop rect, which may or may not be the center of the image rect.
This demonstrates it working with a square crop, but the user can resize it to whatever, so I imagine it's going to bite me in the ass even more so when it's not square.
Code:
- (BOOL)rotatedView:(UIView*)rotatedView containsViewCompletely:(UIView*)cropView {
// If this method returns YES, good! if NO, bad!
CGPoint cropRotated[4];
CGRect rotatedBounds = rotatedView.bounds;
CGRect cropBounds = cropView.bounds;
// Convert corner points of cropView to the coordinate system of rotatedView:
cropRotated[0] = [cropView convertPoint:cropBounds.origin toView:rotatedView];
cropRotated[1] = [cropView convertPoint:CGPointMake(cropBounds.origin.x + cropBounds.size.width, cropBounds.origin.y) toView:rotatedView];
cropRotated[2] = [cropView convertPoint:CGPointMake(cropBounds.origin.x + cropBounds.size.width, cropBounds.origin.y + cropBounds.size.height) toView:rotatedView];
cropRotated[3] = [cropView convertPoint:CGPointMake(cropBounds.origin.x, cropBounds.origin.y + cropBounds.size.height) toView:rotatedView];
// Check if all converted points are within the bounds of rotatedView:
return (CGRectContainsPoint(rotatedBounds, cropRotated[0]) &&
CGRectContainsPoint(rotatedBounds, cropRotated[1]) &&
CGRectContainsPoint(rotatedBounds, cropRotated[2]) &&
CGRectContainsPoint(rotatedBounds, cropRotated[3]));
}
Taking even yet a different spin on this, I'm getting there. But as you can see in the .gif, eventually the calculations get out of whack because the rotation really isn't what I should be using to calculate the new size. How can I implement this with the correct geometry to ensure the image always resizes correctly? For time saving, I put this into an Xcode project so you don't have to fiddle around building your own: https://github.com/r3mus/RotationCGRectFix.git
- (IBAction)gestureRecognized:(UIRotationGestureRecognizer *)gesture {
CGFloat maxRotation = 40;
CGFloat rotation = gesture.rotation;
CGFloat currentRotation = atan2f(_imageView.transform.b, _imageView.transform.a);;
NSLog(#"%0.4f", RADIANS_TO_DEGREES(rotation));
if ((currentRotation > DEGREES_TO_RADIANS(maxRotation) && rotation > 0) || (currentRotation < DEGREES_TO_RADIANS(-maxRotation) && rotation < 0)) {
return;
}
CGAffineTransform rotationTransform = CGAffineTransformRotate(self.imageView.transform, rotation);
gesture.rotation = 0.0f;
if (gesture.state == UIGestureRecognizerStateChanged) {
CGFloat scale = sqrt(_imageView.transform.a * _imageView.transform.a + _imageView.transform.c * _imageView.transform.c);
if ((currentRotation > 0 && rotation > 0) || (currentRotation < 0 && rotation < 0))
scale = 1 + fabs(rotation);
else if (currentRotation == 0)
scale = 1;
else
scale = 1 - fabs(rotation);
CGAffineTransform sizeTransform = CGAffineTransformMakeScale(scale, scale);
CGPoint center = _imageView.center;
_imageView.transform = CGAffineTransformConcat(rotationTransform, sizeTransform);
_imageView.center = center;
}
}
Gif:

Related

Determine if crop rect is entirely contained within rotated UIView

Premise: I'm building a cropping tool that handles two-finger arbitrary rotation of an image as well as arbitrary cropping.
Sometimes the image ends up rotated in a way that empty space is inserted to fill a gap between the rotated image and the crop rect (see the examples below).
I need to ensure that the image view, when rotated, fits entirely into the cropping rectangle. If it doesn't, I then need to re-transform the image (zoom it) so that it fits into the crop bounds.
Using this answer, I've implemented the ability to check whether a rotated UIImageView intersects with the cropping CGRect, but unfortunately that doesn't tell me if the crop rect is entirely contained in the rotated imageview. Hoping that I can make some easy modifications to this answer?
A visual example of OK:
and not OK, that which I need to detect and deal with:
Update: not working method
- (BOOL)rotatedView:(UIView*)rotatedView containsViewCompletely:(UIView*)containedView {
CGRect rotatedBounds = rotatedView.bounds;
CGPoint polyContainedView[4];
polyContainedView[0] = [containedView convertPoint:rotatedBounds.origin toView:rotatedView];
polyContainedView[1] = [containedView convertPoint:CGPointMake(rotatedBounds.origin.x + rotatedBounds.size.width, rotatedBounds.origin.y) toView:rotatedView];
polyContainedView[2] = [containedView convertPoint:CGPointMake(rotatedBounds.origin.x + rotatedBounds.size.width, rotatedBounds.origin.y + rotatedBounds.size.height) toView:rotatedView];
polyContainedView[3] = [containedView convertPoint:CGPointMake(rotatedBounds.origin.x, rotatedBounds.origin.y + rotatedBounds.size.height) toView:rotatedView];
if (CGRectContainsPoint(rotatedView.bounds, polyContainedView[0]) &&
CGRectContainsPoint(rotatedView.bounds, polyContainedView[1]) &&
CGRectContainsPoint(rotatedView.bounds, polyContainedView[2]) &&
CGRectContainsPoint(rotatedView.bounds, polyContainedView[3]))
return YES;
else
return NO;
}
That should be easier than checking for intersection (as in the referenced thread).
The (rotated) image view is a convex quadrilateral. Therefore it suffices to check
that all 4 corner points of the crop rectangle are within the rotated image view.
Use [cropView convertPoint:point toView:imageView] to convert the corner points of the crop rectangle to the coordinate system of the
(rotated) image view.
Use CGRectContainsPoint() to check that the 4 converted corner points are within the bounds rectangle of the image view.
Sample code:
- (BOOL)rotatedView:(UIView *)rotatedView containsCompletely:(UIView *)cropView {
CGPoint cropRotated[4];
CGRect rotatedBounds = rotatedView.bounds;
CGRect cropBounds = cropView.bounds;
// Convert corner points of cropView to the coordinate system of rotatedView:
cropRotated[0] = [cropView convertPoint:cropBounds.origin toView:rotatedView];
cropRotated[1] = [cropView convertPoint:CGPointMake(cropBounds.origin.x + cropBounds.size.width, cropBounds.origin.y) toView:rotatedView];
cropRotated[2] = [cropView convertPoint:CGPointMake(cropBounds.origin.x + cropBounds.size.width, cropBounds.origin.y + cropBounds.size.height) toView:rotatedView];
cropRotated[3] = [cropView convertPoint:CGPointMake(cropBounds.origin.x, cropBounds.origin.y + cropBounds.size.height) toView:rotatedView];
// Check if all converted points are within the bounds of rotatedView:
return (CGRectContainsPoint(rotatedBounds, cropRotated[0]) &&
CGRectContainsPoint(rotatedBounds, cropRotated[1]) &&
CGRectContainsPoint(rotatedBounds, cropRotated[2]) &&
CGRectContainsPoint(rotatedBounds, cropRotated[3]));
}

Understanding CGRectDivide()

I am trying to experiment with UIViews on screen and using pan gestures. So I got some open source code from another project that I am looking at - and trying to learn a few things from it.
-(BOOL)isPointContainedWithinBezelRect:(CGPoint)point {
CGRect leftBezelRect;
CGRect tempRect;
CGFloat bezelWidth = 20;
CGRectDivide(self.view.bounds, &leftBezelRect, &tempRect, bezelWidth, CGRectMinXEdge);
return CGRectContainsPoint(leftBezelRect, point);
}
I understand that CGRectDivide function "Slices up a rect", but thats as far as I can make out.
I hope to get more clarification regarding the function. Also, how does the function return value vide a false / true value?
void CGRectDivide(
CGRect rect,
CGRect *slice,
CGRect *remainder,
CGFloat amount,
CGRectEdge edge
)
The CGRectDivide method splits a CGRect into two CGRects based on the CGRectEdge and distance from the rectangle side amount provided to the method.
Source
You should check
https://developer.apple.com/library/ios/documentation/graphicsimaging/reference/CGGeometry/Reference/reference.html#//apple_ref/c/func/CGRectDivide
and
http://nshipster.com/cggeometry/
But it seems that this method could be simplified to
-(BOOL)isPointContainedWithinBezelRect:(CGPoint)point {
CGRect leftBezelRect = self.view.bounds;
leftBezelRect.size.width = 20;
return CGRectContainsPoint(leftBezelRect, point);
}
or even to
-(BOOL)isPointContainedWithinBezelRect:(CGPoint)point {
return CGRectContainsPoint(self.view.bounds, point) && (point.x <= 20);
}

Moving and rotating a sprite with Accelerometer in SpriteKit

I'm trying to make my first game using Spritekit, so i have a sprite that i need to move around using my accelerometer. Well, no problem doing that; movement are really smooth and responsive, the problem is that when i try to rotate my sprite in order to get it facing its own movement often i got it "shaking" like he has parkinson. (:D)
i did realize that this happens when accelerometer data are too close to 0 on one of x, y axes.
So the question: Is there a fix for my pet parkinson?? :D
Here is some code:
-(void) update:(NSTimeInterval)currentTime{
static CGPoint oldVelocity;
//static CGFloat oldAngle;
if(_lastUpdatedTime) {
_dt = currentTime - _lastUpdatedTime;
} else {
_dt = 0;
}
_lastUpdatedTime = currentTime;
CGFloat updatedAccelX = self.motionManager.accelerometerData.acceleration.y;
CGFloat updatedAccelY = -self.motionManager.accelerometerData.acceleration.x+sinf(M_PI/4.0);
CGFloat angle = vectorAngle(CGPointMake(updatedAccelX, updatedAccelY));
_velocity = cartesianFromPolarCoordinate(MAX_MOVE_PER_SEC, angle);
if(oldVelocity.x != _velocity.x || oldVelocity.y != _velocity.y){
_sprite.physicsBody.velocity = CGVectorMake(0, 0);
[_sprite.physicsBody applyImpulse:CGVectorMake(_velocity.x*_sprite.physicsBody.mass, _velocity.y*_sprite.physicsBody.mass)];
_sprite.zRotation = vectorAngle(_velocity);
oldVelocity = _velocity;
}
}
static inline CGFloat vectorAngle(CGPoint v){
return atan2f(v.y, v.x);
}
i did try to launch the update of the _velocity vector only when updatedAccelX or updatedAccelY are, in absolute value >= of some values, but the result was that i got the movement not smooth, when changing direction if the value is between 0.1 and 0.2, and the problem wasn't disappearing when the value was under 0.1.
i would like to maintain direction responsive, but i also would like to fix this "shake" of the sprite rotation.
I'm sorry for my bad english, and thanks in advance for any advice.
You can try a low pass filter (cf. to isolate effect of gravity) or high pass filter (to isolate effects of user acceleration).
#define filteringFactor 0.1
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
//low pass
accelerX = (acceleration.x * filteringFactor) + (accelerX * (1.0 - filteringFactor));
//idem … accelerY
//idem … accelerZ
//or high pass
accelerX = acceleration.x - ( (acceleration.x * filteringFactor) + (accelerX * (1.0 - filteringFactor)) );
//idem … accelerY
//idem … accelerZ
}

How to simulate Gravity in z-axis in a 2D game with Sprite Kit

I'm writing a 2D ball game with sprite kit on iOS 7 and currently struggling on one physic simulation.
To explain the expected behavior: if a ball is dropped into a tea cup, it will circle around, loosing speed and finally stand still in the center of the cup.
I've tried to archive this with gravity, but gravity in sprite kit only applies to vertical X and Y axis, not Z-axis. I also tried to use level gravity by switching gravity values with small physic bodies on beginContact depending on the current ball position in the tea cup. But some contacts are dropped and the result is far away to look realistic.
I think I need to solve this in the update: method, but I have no idea which way to go.
Any advice greatly welcome and I need to mention that I'm not an expert on math, please explain your path to go. :-)
Since there's no built-in support for this kind of behavior in SpriteKit, rather than trying to hack existing functions to get what you want, you're probably better off integrating some published 2D physics formulas in your x,y 2D world. I would think that something like simulating magnetic or a homing behavior might be right for this.
A simple example would be something like (in the scene's -update: method):
CGFloat strength = 0.5; //(some scaling value)
CGPoint ballLocation = ball.position;
CGPoint cupLocation = cup.position;
[ball.physicsBody applyForce:CGVectorMake((cupLocation.x - ballLocation.x) * strength,
(cupLocation.y - ballLocation.y) * strength)];
following Joshd great idea, I have created an NSArray with like explained in my comment above. Hope this snippets does help some others...
The result could be found on youtube: http://youtu.be/Uephg94UH30
Sorry for the bad Airplay frame rate, it runs perfectly smooth on my iPad
The -update: functions does the work but only triggered if _meditationIsActive. This bool is set in -didBeginContact: when any ball gets in contact with a hole.
if (_lastCheck > 0.005)
{
if (_meditationIsActive)
{
CGFloat strength = 0.1; //(some scaling value)
CGPoint ballLocation;
CGPoint holeLocation;
for (MeditationHole * holeObj in _meditationHoles)
{
if (holeObj.connectedMeditationBall != nil)
{
ballLocation = holeObj.connectedMeditationBall.position;
holeLocation = holeObj.position;
[holeObj.connectedMeditationBall.physicsBody applyForce:CGVectorMake(
(holeLocation.x - ballLocation.x) * strength, (holeLocation.y - ballLocation.y) * strength)];
}
}
_meditationIsActive = [self doesMeditationApplies];
}
_lastCheck = 0;
}
At the end I'm checking if there is a valid ball out of the array in contact with a hole to avoid checking during every update. This is done with the following function where position check +/- 48 detects a ball close to a hole and +/-1 ball stands still
- (bool)doesMeditationApplies
{
bool isInArea = NO;
int perfectMatchCount = 0;
for (MeditationHole * holeObj in _meditationHoles)
{
if (holeObj)
{
if (holeObj.connectedMeditationBall != nil)
{
MeditationBall * ballObj = holeObj.connectedMeditationBall;
if ((ballObj.position.x >= holeObj.position.x - 48) &&
(ballObj.position.x <= holeObj.position.x + 48) &&
(ballObj.position.y >= holeObj.position.y - 48) &&
(ballObj.position.y <= holeObj.position.y + 48))
{
isInArea = YES;
}
else
{
holeObj.connectedMeditationBall = nil;
}
if ((ballObj.position.x >= holeObj.position.x - 1) &&
(ballObj.position.x <= holeObj.position.x + 1) &&
(ballObj.position.y >= holeObj.position.y - 1) &&
(ballObj.position.y <= holeObj.position.y + 1))
{
perfectMatchCount++;
isInArea = YES;
}
}
}
}
if (perfectMatchCount == _oxydStonesMax)
{
if (_sound)
{
self.pauseMusicPlaybackBlock(YES);
NSLog(#"PlaySound Meditation");
[OxydScene PlaySystemSound:#"Win2"];
}
isInArea = NO;
[self showPauseScreenWithWin:YES andPauseOnly:NO];
}
return isInArea;
}

Keep Rotating sprite from going off screen, but bounce back.

Im using a technique to control a sprite by rotating left/right and then accelerating forward. I have 2 questions regarding it. (The code it pasted together from different classes due to polymorphism. If it doesn't make sense, let me know. The movement works well and the off screen detection as well.)
When player moves off screen i call the Bounce method. I want the player not to be able to move off screen but to change direction and go back. This works on top and bottom but left and right edge very seldom. Mostly it does a wierd bounce and leaves the screen.
I would like to modify the accelerate algorithm so that i can set a max speed AND a acceleration speed. Atm the TangentalVelocity does both.
float TangentalVelocity = 8f;
//Called when up arrow is down
private void Accelerate()
{
Velocity.X = (float)Math.Cos(Rotation) * TangentalVelocity;
Velocity.Y = (float)Math.Sin(Rotation) * TangentalVelocity;
}
//Called once per update
private void Deccelerate()
{
Velocity.X = Velocity.X -= Friction * Velocity.X;
Velocity.Y = Velocity.Y -= Friction * Velocity.Y;
}
// Called when player hits screen edge
private void Bounce()
{
Rotation = Rotation * -1;
Velocity = Velocity * -1;
SoundManager.Vulture.Play();
}
//screen edge detection
public void CheckForOutOfScreen()
{
//Check if ABOVE screen
if (Position.Y - Origin.Y / 2 < GameEngine.Viewport.Y) { OnExitScreen(); }
else
//Check if BELOW screen
if (Position.Y + Origin.Y / 2 > GameEngine.Viewport.Height) { OnExitScreen(); }
else
//Check if RIGHT of screen
if (this.Position.X + Origin.X / 2 > GameEngine.Viewport.Width) { OnExitScreen(); }
else
//Check if LEFT of screen
if (this.Position.X - Origin.X / 2 < GameEngine.Viewport.X) { OnExitScreen(); }
else
{
if (OnScreen == false)
OnScreen = true;
}
}
virtual public void OnExitScreen()
{
OnScreen = false;
Bounce();
}
Let's see if I understood correctly. First, you rotate your sprite. After that, you accelerate it forward. In that case:
// Called when player hits screen edge
private void Bounce()
{
Rotation = Rotation * -1;
Velocity = Velocity * -1; //I THINK THIS IS THE PROBLEM
SoundManager.Vulture.Play();
}
Let's suposse your sprite has no rotation when it looks up. In that case, if it's looking right it has rotated 90º, and its speed is v = (x, 0), with x > 0. When it goes out of the screen, its rotation becomes -90º and the speed v = (-x, 0). BUT you're pressing the up key and Accelerate method is called so immediately the speed becomes v = (x, 0) again. The sprite goes out of the screen again, changes its velocity to v = (-x, 0), etc. That produces the weird bounce.
I would try doing this:
private void Bounce()
{
Rotation = Rotation * -1;
SoundManager.Vulture.Play();
}
and check if it works also up and bottom. I think it will work. If not, use two different Bounce methods, one for top/bottom and another one for left/right.
Your second question... It's a bit difficult. In Physics, things reach a max speed because air friction force (or another force) is speed-dependent. So if you increase your speed, the force also increases... at the end, that force will balance the other and the speed will be constant. I think the best way to simulate a terminal speed is using this concept. If you want to read more about terminal velocity, take a look on Wikipedia: http://en.wikipedia.org/wiki/Terminal_velocity
private void Accelerate()
{
Acceleration.X = Math.abs(MotorForce - airFriction.X);
Acceleration.Y = Math.abs(MotorForce - airFriction.Y);
if (Acceleration.X < 0)
{
Acceleration.X = 0;
}
if (Acceleration.Y < 0)
{
Acceleration.Y = 0;
}
Velocity.X += (float)Math.Cos(Rotation) * Acceleration.X
Velocity.Y += (float)Math.Sin(Rotation) * Acceleration.Y
airFriction.X = Math.abs(airFrictionConstant * Velocity.X);
airFriction.Y = Math.abs(airFrictionConstant * Velocity.Y);
}
First, we calculate the accelartion using a "MotorForce" and the air friction. The MotorForce is the force we make to move our sprite. The air friction always tries to "eliminate" the movement, so is always postive. We finally take absolute values because the rotation give us the direction of the vector. If the acceleration is lower than 0, that means that the air friction is greater than our MotorForce. It's a friction, so it can't do that: if acceleration < 0, we make it 0 -the air force reached our motor force and the speed becomes constant.
After that, the velocity will increase using the acceleration. Finally, we update the air friction value.
One thing more: you may update also the value of airFriction in the Deccelarate method, even if you don't consider it in that method.
If you have any problem with this, or you don't understand something (sometimes my English is not very good ^^"), say it =)

Resources