I'm having difficulties adding momentum to my spinning wheel.
I have this wheel (something like this) and I'm rotating it around it's center by using a single touch event.
No problems here, but when the touch (aka drag) ends; I want the wheel to keep it's momentum and ease out it's movement.
Anyone who can give me some pointers, it doesn't necessarily have to be in objective-c.
AS3, javascript or JAVA will also be sufficient.
* UPDATE (code for rotating the wheel) *
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
rotation = _startAngle = atan2(384 - touchPoint.y, 512 - touchPoint.x);
};
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
CGPoint touchPoint = [[touches anyObject] locationInView:self.view];
rotation = atan2(384 - touchPoint.y, 512 - touchPoint.x);
rotation = fmod(rotation - _startAngle, M_PI * 2.0f);
[wheel setTransform:CGAffineTransformMakeRotation(_circleRotationOffset + rotation)];
};
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
_circleRotationOffset = fmod(_circleRotationOffset + rotation, M_PI * 2);
};
You want the momentum to reduce due to friction; friction is a force that's a function of velocity. So technically you've got a differential equation going on. That's not really worth investing too much thought into though, because the solution is probably more easily reached by hand waving.
So: store current angle and current angular velocity. n times a second (probably via an NSTimer or a CADisplayLink) add the angular velocity to the angle, then multiply the angular velocity by something to make it smaller — such as 0.995. Constants closer to 1.0 will make it take longer to slow down; if you go above 1.0 it'll obviously accelerate. This is effectively a form of Euler integration but, again, it's not worth worrying about.
It's possibly also worth putting a minimum cap on angular velocity so that if it drops below, say 0.01 radians/second then you snap it down to 0. That effectively modifies your model of friction slightly to jump from kinetic to static friction at an opportune moment, and acts as a floating point precision buffer zone.
To get initial velocity from a drag you can just work out the vector from the centre of the wheel to the start of the drag, rotate that vector by 90 degrees, do a dot product with that and the drag vector and scale according to distance from the centre.
If you are spinning the UIImageView with code like:
[UIView beginAnimations:#"rotateImage" context:nil];
[UIView setAnimationDuration:4.0];
wheelImageView.transform = CGAffineTransformMakeRotation(3.14159265*5);
[UIView commitAnimations];
You could use [UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
This makes it so the animation will start off fast and start to slow down over time.
Click Here for More info on UIViewAnimationCurve
I managed to get some nice results.
How I did it, should be tweaked more, but this is the basics:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
CGPoint touchPoint = [[touches anyObject] locationInView:self.view];
rotation = _startAngle = atan2(384 - touchPoint.y, 512 - touchPoint.x);
[_history removeAllObjects];
};
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
CGPoint touchPoint = [[touches anyObject] locationInView:self.view];
[_history insertObject:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithDouble:CFAbsoluteTimeGetCurrent()], #"time", [NSValue valueWithCGPoint:touchPoint], #"point", [NSNumber numberWithFloat: _circleRotationOffset + rotation], #"rotation", nil] atIndex:0];
if ([_history count] == 3) {
[_history removeLastObject];
}
rotation = atan2(384 - touchPoint.y, 512 - touchPoint.x) - _startAngle;
[circleImage setTransform:CGAffineTransformMakeRotation(_circleRotationOffset + rotation)];
};
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
CGPoint tp = [[touches anyObject] locationInView:self.view];
_circleRotationOffset += rotation;
NSDictionary *lo = [_history lastObject];
CGPoint pp = [[lo objectForKey:#"point"] CGPointValue];
double timeDif = CFAbsoluteTimeGetCurrent() - [[lo objectForKey:#"time"] doubleValue];
float lastRotation = [[lo objectForKey:#"rotation"] floatValue];
// Calculate strength
float dist = sqrtf(((pp.x - tp.x) * (pp.x - tp.x)) + ((pp.y - tp.y) * (pp.y - tp.y)));
float strength = MIN(1.0f, dist / 80.0f) * (timeDif / .025) * M_PI;
float p = _circleRotationOffset;
float dif = _circleRotationOffset - lastRotation;
BOOL inc = dif > 0;
if (dif > 3 || dif < -3) { // Some correction
inc = !inc;
}
if (inc) {
_circleRotationOffset += strength;
} else {
_circleRotationOffset -= strength;
}
[circleImage.layer removeAllAnimations];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.duration = MAX(strength / 2.5, 1.0f);
animation.cumulative = YES;
animation.repeatCount = 1;
animation.values = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:p],
[NSNumber numberWithFloat: _circleRotationOffset], nil];
animation.keyTimes = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0],
[NSNumber numberWithFloat:1.0], nil];
animation.timingFunctions = [NSArray arrayWithObjects:
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut], nil];
animation.removedOnCompletion = YES;
animation.delegate = self;
animation.fillMode = kCAFillModeForwards;
[circleImage.layer addAnimation:animation forKey:#"rotate"];
};
Related
I am developing 'Paper Toss' for iphone using cocos2d & I would like to know that how to implement 3D perspective view into this,because while we are throwing the paper ball to the bin,we have to get the 3D feel.I'am attaching the code which i have done,using this i have got a straight line motion.Please help me..
*- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
// Choose one of the touches to work with
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:[touch view]];
location = [[CCDirector sharedDirector] convertToGL:location];
// Set up initial location of projectile
CGSize winSize = [[CCDirector sharedDirector] winSize];
CCSprite *projectile = [CCSprite spriteWithFile:#"ball.png"
rect:CGRectMake(0, 0, 40, 40)];
projectile.position = ccp(winSize.width/2,20);
// Determine offset of location to projectile
int offX = location.x - projectile.position.x;
int offY = location.y - projectile.position.y;
// Bail out if we are shooting down or backwards
if (offY <= 0) return;
// Ok to add now - we've double checked position
[self addChild:projectile];
// Determine where we wish to shoot the projectile to
int realY = winSize.height + (projectile.contentSize.width/2);
float ratio = (float) offX / (float) offY;
int realX = (realY * ratio) + projectile.position.x;
CGPoint realDest = ccp(realX, realY);
// Determine the length of how far we're shooting
int offRealX = realX + projectile.position.x;
int offRealY = realY + projectile.position.y;
float length = sqrtf((offRealX*offRealX)+(offRealY*offRealY));
float velocity = 480/1; // 480pixels/1sec
float realMoveDuration = length/velocity;
// Move projectile to actual endpoint
[projectile runAction:[CCSequence actions:
[CCMoveTo actionWithDuration:realMoveDuration position:realDest],
[CCCallFuncN actionWithTarget:self selector:#selector(spriteMoveFinished:)],
nil]];
//add to the projectiles array
projectile.tag = 2;
[_projectiles addObject:projectile];
}*
Finally I have completed paper toss using cocos2d.I implemented bezier curve and here it is,
// Bezier curve control points
bezier.controlPoint_1 = ccp(location.x-CONTROL_POINT1_X, CONTROL_POINT1_Y);
bezier.controlPoint_2 = ccp(location.x-CONTROL_POINT2_X, CONTROL_POINT2_Y);
bezier.endPosition = ccp(location.x-CONTROL_POINT1_X,distance);
// Motion along bezier curve and finally call a function
[projectile runAction:[CCSequence actions:
[CCAutoBezier actionWithDuration:DEFAULT_ACTION_DURATION bezier:bezier],
[CCCallFuncN actionWithTarget:self selector:#selector(collisionCheck:)], nil]];
Just scale your sprite image so it gets smaller the "further away" it is.
Hey you can use bezier curve for 3d perspective view in cocos2d.
bezier.controlPoint_1 = ccp(location.x-CONTROL_POINT1_X, CONTROL_POINT1_Y);
bezier.controlPoint_2 = ccp(location.x-CONTROL_POINT2_X, CONTROL_POINT2_Y);
I have an arrow that spins when the user clicks a button using CAKeyframeAnimation.
The position the arrow points to dictates what image is seen by the user.
This works fine.
The final position of the rotation is selected randomly and sets up the next image for the user when they click the button.
-(void)rotateAnimation
{
[self setDialStartPosition];
[self setDialEndPosition];
//Spin the Arrow
CALayer *layer = imageView.layer;
CAKeyframeAnimation *animation;
animation = [CAKeyframeAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.duration = 8;
animation.cumulative = NO;
animation.repeatCount = 0;
animation.values = [NSArray arrayWithObjects: // i.e., Rotation values for the 3 keyframes, in RADIANS
[NSNumber numberWithFloat:[self dialStartPosition] * M_PI],
[NSNumber numberWithFloat:2.5 * M_PI],
[NSNumber numberWithFloat:[self dialEndPosition] * M_PI], nil];
animation.keyTimes = [NSArray arrayWithObjects: // Relative timing values for the 3 keyframes
[NSNumber numberWithFloat:0],
[NSNumber numberWithFloat:.055],
[NSNumber numberWithFloat:1], nil];
animation.timingFunctions = [NSArray arrayWithObjects:
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn],
// from keyframe 1 to keyframe 2
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut], nil]; // from keyframe 2 to keyframe 3
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
[layer addAnimation:animation forKey:nil];
....
I have this working, but I want the user to be able to move the arrow manually using a touch and have the arrow point in the direction of their finger as they move it around.
When they lift their finger and hit the button, i want the position of the Arrow to dictate what image is then seen by the user. So touchesEnded would have to update the dialStartPosition.
Here is my touches code..
double wrapd(double _val, double _min, double _max)
{
if(_val < _min) return _max - (_min - _val);
if(_val > _max) return _min - (_max - _val);
return _val;
}
- (void) touchesBegan:(NSSet *)_touches withEvent:(UIEvent *)_event
{
UITouch* touch = [_touches anyObject];
CGPoint location = [touch locationInView:self.view];
m_locationBegan = location;
}
- (void) touchesMoved:(NSSet *)_touches withEvent:(UIEvent *)_event
{
UITouch* touch = [_touches anyObject];
CGPoint location = [touch locationInView:self.view];
[self updateRotation:location];
}
- (void) touchesEnded:(NSSet *)_touches withEvent:(UIEvent *)_event
{
UITouch* touch = [_touches anyObject];
CGPoint location = [touch locationInView:self.view];
m_currentAngle = [self updateRotation:location];
}
- (float) updateRotation:(CGPoint)_location
{
float fromAngle = atan2(m_locationBegan.y-imageView.center.y, m_locationBegan.x-imageView.center.x);
float toAngle = atan2(_location.y-imageView.center.y, _location.x-imageView.center.x);
float newAngle = wrapd(m_currentAngle + (toAngle - fromAngle), 0, 2*3.14);
CGAffineTransform cgaRotate = CGAffineTransformMakeRotation(newAngle);
imageView.transform = cgaRotate;
return newAngle;
}
Heres whats happing now..
Before I hit the button I can move the arrow around with a touch the way I want.
When I hit the button it doesn't respond to where I have set the position manually with my touch.
Once the CAKeyframeAnimation plays it no longer will respond to any touches unless I change
animation.fillMode = kCAFillModeForwards;
to
animation.fillMode = kCAFillModeRemoved;
but if I do this it removes the animation after it runs and shows the arrow in the spot I left it when I set it manually.
I'm quite new a programming so any help would be appreciated
Thanks
transform a square image or round image is easy to do...
But what if a rectangle image ???
This is the image I want to transform with touch event .
The circle center is (30 , 236) , if I touch any place on the screen
Imageview will transform the arrow to my touch point.
But the circle center still the same place .
I try to transform the image use a test angle
It will become like this ...
How to adjust the image ?
Also the code is here
- (void)viewDidLoad {
CGRect arrowImageRect = CGRectMake(20.0f, 20.0f, 15.0f, 220.0f);
arrow = [[UIImageView alloc] initWithFrame:arrowImageRect];
[arrow setImage:[UIImage imageNamed:#"arrow.png"]];
arrow.opaque = YES;
[self.view addSubview:arrow];
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
CGPoint touchPoint = [touch locationInView:self.view];
NSLog(#"Position X: %f \n, Y: %f",touchPoint.x,touchPoint.y);
CGAffineTransform transform = CGAffineTransformIdentity;
arrow.transform = CGAffineTransformRotate(transform, ?????? );
}
The ?????? part should be the last solution ...
Or Maybe I need to resize the imageview ???
Thanks for any reply or answer :-)
Webber
Here is the final solution I figure this problem
check this code
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch=[[event allTouches]anyObject];
[self transformSpinnerwithTouches:touch];
}
-(void)transformSpinnerwithTouches:(UITouch *)touchLocation
{
CGPoint touchLocationpoint = [touchLocation locationInView:self.view];
CGPoint PrevioustouchLocationpoint = [touchLocation previousLocationInView:self.view];
//origin is the respective piont from that i gonna measure the angle of the current position with respective to previous position ....
CGPoint origin;
origin.x = arrow.center.x;
origin.y = arrow.center.y;
CGPoint previousDifference = [self vectorFromPoint:origin toPoint:PrevioustouchLocationpoint];
CGAffineTransform newTransform = CGAffineTransformScale(arrow.transform, 1, 1);
CGFloat previousRotation = atan2(previousDifference.y, previousDifference.x);
CGPoint currentDifference = [self vectorFromPoint:origin toPoint:touchLocationpoint];
CGFloat currentRotation = atan2(currentDifference.y, currentDifference.x);
CGFloat newAngle = currentRotation- previousRotation;
temp1 = temp1 + newAngle;
//NSLog(#"temp1 is %f",temp1);
//NSLog(#"Angle:%F\n",(temp1*180)/M_PI);
newTransform = CGAffineTransformRotate(newTransform, newAngle);
[self animateView:arrow toPosition:newTransform];
}
-(void)animateView:(UIView *)theView toPosition:(CGAffineTransform) newTransform
{
arrow.transform = newTransform;
}
-(CGPoint)vectorFromPoint:(CGPoint)firstPoint toPoint:(CGPoint)secondPoint
{
CGFloat x = secondPoint.x - firstPoint.x;
CGFloat y = secondPoint.y - firstPoint.y;
CGPoint result = CGPointMake(x, y);
//NSLog(#"%f %f",x,y);
return result;
}
From the documentation about transform method:
The origin of the transform is the value of the center property, or the layer’s anchorPoint property if it was changed. (Use the layer property to get the underlying Core Animation layer object.) The default value is CGAffineTransformIdentity.
So you should be able to set the center of your rotation this way!
And for determining the rotation angle, use the atan2 or atan2f function:
float angleInRadians = atan2f(touchPoint.y-circleCenter.y , touchPoint.x-circleCenter.x);
You can re set the center after transform.
CGPoint centerBeforeTransform = view.center;
//your transform code
view.center = centerBeforeTransform;
I am trying to implement a wheel type functionality. I calculate the center point of the view and find the angle created on moving in touchesMoved: method. For the first move it rotated by some degree. But for next move it comes back to the original position of the view and then rotating.Actually i want the rotation from the end point of the previous rotation.
Any thing missing?Any help
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
float a, b,c;
UITouch *aTouch = [touches anyObject];
if ([aTouch view] == dialView) {
CGPoint loc = [aTouch locationInView:dialView];
CGPoint prevloc = [aTouch previousLocationInView:dialView];
c = [self GetSides:prevloc point2:loc];
a = [self GetSides:loc point2:cntrePoint];
b = [self GetSides:prevloc point2:cntrePoint];
float angle = acos((a*a+b*b-c*c)/(2*a*b));// Calculating angle created on moving
CABasicAnimation *rotate ;
rotate = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotate.fromValue = [NSNumber numberWithFloat:0];
rotate.toValue = [NSNumber numberWithFloat:((angle*M_PI)/180)];
rotate.autoreverses = NO;
rotate.fillMode = kCAFillModeForwards;
rotate.duration = 1;
rotate.repeatCount = 1;
rotate.removedOnCompletion = NO;
rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[dialView.layer addAnimation:rotate forKey:#"360"];
}
}
The animation you are performing, which looks to be drawn from this answer, has both the to and from values specified, so it will simply rotate from 0 to angle radians every time. That means that your image will jump back to the start on every animation.
Normally, if you were to remove the fromValue line, your animation should go from the current angle to your new angle, but it looks like the transform property behaves a little differently. You'll need to set the fromValue to be the current value of the rotational transform extracted from your layer's presentationLayer. The following code should rotate to the target angle from your current angle:
CABasicAnimation *rotate;
rotate = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotate.fromValue = [[dialView.layer presentationLayer] valueForKeyPath:#"transform.rotation.z"];
rotate.toValue = [NSNumber numberWithFloat:angle];
rotate.fillMode = kCAFillModeForwards;
rotate.duration = 1;
rotate.removedOnCompletion = NO;
rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
Note that I've removed the degrees-to-radians conversion in the toValue, because your acos() calculation returns values in radians.
EDIT (10/23/2009): Corrected my earlier assumption about the functioning of fromValue with the transform property.
I have a view that I would like the user to rotate around its center, by tapping and holding somewhere and just move their finger round and round.
I have all the geometry worked out; What I do is store the initial touch angle relative to the center as offsetAngle, then my touchesMoved method looks like this:
- (void) touchesMoved: (NSSet *)touches withEvent:(UIEvent *)event {
self.transform = CGAffineTransformMakeRotation(0);
CGPoint location = [[touches anyObject] locationInView: self];
CGPoint actualCenter = [self convertPoint: self.center fromView: self.superview];
CGPoint relativeTouch = [MathHelper translatePoint:location relativeTo: actualCenter];
CGPoint zero = CGPointMake(0, 0);
float angle = [MathHelper getAngleForPoint:zero andPoint:relativeTouch];
self.transform = CGAffineTransformMakeRotation(offsetAngle-angle);
}
The ugly bit is the first line, where I have to restore the rotation in order to get the correct event positions in the view. If I don't that, then the locations jump all over the place as there's no continuity, since the view is rotating...
Another issue is when you want to manipulate a view's frame (eg. moving it down), when the view has a transform applied:
- (IBAction)toggleSettingsView {
BOOL state = [settingsSwitch isOn];
float height = optionsView.bounds.size.height;
CGRect polygonFrame = polygonView.frame;
[UIView beginAnimations:#"advancedAnimations" context:nil];
[UIView setAnimationDuration:0.3];
if (state) {
optionsView.alpha = 1.0;
polygonFrame.origin.y += height;
} else {
optionsView.alpha = 0.0;
polygonFrame.origin.y -= height;
}
polygonView.frame = polygonFrame;
[UIView commitAnimations];
}
This distorts the view heavily.
I must mention that both the CGTransform and the CALayer transform have the same effect.
This smells like I'm doing something wrong, but I don't know what I should be doing.
I would use the coordinate system of the superview, since it is unaffected by the rotation:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint location = [[touches anyObject] locationInView:self.superview];
CGPoint relativeTouch = [MathHelper translatePoint:location relativeTo:self.center];
CGPoint zero = CGPointMake(0, 0);
float angle = [MathHelper getAngleForPoint:zero andPoint:relativeTouch];
self.transform = CGAffineTransformMakeRotation(offsetAngle-angle);
}
Just save the original transform in an ivar and call it a day.