i am working on a game to spot the difference between 2 images.
I want to add an effect when you spot it. drawing out a circle would be much better than just showing the circle suddenly. but i've never done core animation or opengl before.
i don't think preparing 100 sprites and changing the sprite frame by frame is a good idea.
here is my code: (just add a circle image to both left and right image. )
-(void) show {
CCSprite* leftCircle = [CCSprite spriteWithFile:#"circle.png"];
CCSprite* rightCircle = [CCSprite spriteWithFile:#"circle.png"];
leftCircle.scaleX = size.width / [leftCircle boundingBox].size.width;
leftCircle.scaleY = size.height / [leftCircle boundingBox].size.height;
rightCircle.scaleX = size.width / [rightCircle boundingBox].size.width;
rightCircle.scaleY = size.height / [rightCircle boundingBox].size.height;
leftCircle.anchorPoint = ccp(0, 1);
rightCircle.anchorPoint = ccp(0, 1);
leftCircle.position = leftPosition;
rightCircle.position = rightPosition;
[[GameScene sharedScene] addChild:leftCircle z: 3];
[[GameScene sharedScene] addChild:rightCircle z: 3];
shown = YES;
}
So how can i implement it? It would be great if you can provide some source code.
As a simple way i can recommend you to create a circle and put it's scale to zero. Then create a CCScale action and run it. It will give you a growing circle. Here is the code:
CCSprite *sprite = [CCSprite spriteWithFile:#"mySprite.png"];
[sprite setScale:0.01];
id scale = [CCScale actionWithDuration:0.3 scale:1];
[sprite runAction:scale];
The other action can be used is CCFadeIn.You can make your sprite invisible after creation and make it fade in:
CCSprite *sprite = [CCSprite spriteWithFile:#"mySprite.png"];
[sprite setOpacity:0];
id fade = [CCFadeIn actionWithDuration:0.3];
[sprite runAction:fade];
Also you can combine this actions:
[sprite runAction:fade];
[sprite runAction:scale];
Also you can make it big (set scale 3 for example) and transparent. And make it fade in and scale down to highlight your image
To Draw a circle via drawing effect you can make it from small parts (arcs) and then build your circle from them. I think it also will be cool if you make don't make the part visible when adding, but make it fade in. I mean something like this:
-(void) init
{
NSMutableArray *parts = [[NSMutableArray array] retain]; //store it in your class variable
parts_ = parts;
localTime_ = 0; //float localTime_ - store the local time in your layer
//create all the parts here, make them invisible and add to the layer and parts
}
-(void) update: (CCTime) dt //add update method to your layer that will be called every simulation step
{
localTime_ += dt;
const float fadeTime = 0.1;
int currentPart = localTime_ / fadeTime;
int i = 0;
for (CCSprite *part in parts)
{
//setup the opacity of each part according to localTime
if (i < currentPart) [part setOpacity:255];
else if (i == currentPart)
{
float localLocalTime = localTime - i*fadeTime;
float alpha = localLocalTime / fadeTime;
[part setOpacity:alpha];
}
++i;
}
}
It is rather simple to create the effect you want.
You can realize it by setting the strokeEnd of a CAShapeLayer.
Here are the details:
create a path you then assign to a CAShapeLayer
UIBezierPath* circlePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100.0, 100.0) radius:80.0 startAngle:DEGREES_TO_RADIANS(270) endAngle:DEGREES_TO_RADIANS(270.01) clockwise:NO];
circle = [CAShapeLayer layer];
circle.path = circlePath.CGPath;
circle.strokeColor = [[UIColor blackColor] CGColor];
circle.fillColor = [[UIColor whiteColor] CGColor];
circle.lineWidth = 6.0;
circle.strokeEnd = 0.0;
[self.view.layer addSublayer:circle];
set the strokeEnd to implicitly animate the drawing of the circle
circle.strokeEnd = 1.0;
That's it! The circle will be automatically drawn for you ;). Here is the code from above with a GestureRecognizer to trigger the animation. Copy this to an empty project of type "Single View Application" and paste it to the ViewController.
#define DEGREES_TO_RADIANS(angle) ((angle) / 180.0 * M_PI)
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UIBezierPath* circlePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100.0, 100.0) radius:80.0 startAngle:DEGREES_TO_RADIANS(270) endAngle:DEGREES_TO_RADIANS(270.01) clockwise:NO];
circle = [CAShapeLayer layer];
circle.path = circlePath.CGPath;
circle.strokeColor = [[UIColor blackColor] CGColor];
circle.fillColor = [[UIColor whiteColor] CGColor];
circle.lineWidth = 6.0;
circle.strokeEnd = 0.0;
[self.view.layer addSublayer:circle];
// add a tag gesture recognizer
// add a single tap gesture recognizer
UITapGestureRecognizer* tapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapGesture:)];
[self.view addGestureRecognizer:tapGR];
}
#pragma mark - gesture recognizer methods
//
// GestureRecognizer to handle the tap on the view
//
- (void)handleTapGesture:(UIGestureRecognizer *)gestureRecognizer {
circle.strokeEnd = 1.0;
}
Related
In my iPhone app i need to implement a different type of transition.
that was
next view open from the current view,
it stats like a dot, and the dot expands slowly like a circle with in the circle next view is to be displayed partially with in the part of the circle , finally the circle expands totally the next view appears completely.
I search lots of transitions like CATransitions, and some animations on cocoa controller, but i didn't find this type of transition, can any one help me please.
Well I think I can offer you a workaround. Instead of pushing to the next view like a dot . I suggest you add a simple dot animation in the ViewWillAppear of the view in which you have to get pushed. Now the push method would remain the same like
[self.navigationController pushViewController:NewView animated:YES];
But in the ViewWillAppear the code would be such that the dot would expand to a circle and reveal the New View beneath it. Hope you understand the logic I am trying to explain here. Any issue do let me know .
in my case I did it that way:
set a CAShapeLayer instance as the layer's mask property of your custom view subclass
#interface MyCustomView ()
#property (nonatomic, strong) CircleShapeLayer *circleShapeLayer;
#end
#implementation MyCustomView
- (id) initWithFrame: (CGRect) frame {
self = [super initWithFrame: CGRectZero];
if (self) {
self.layer.mask = self.shapeLayer;
[self.layer.mask setValue: #(0) forKeyPath: #"transform.scale"];
}
return self;
}
zoom this mask layer to fullsize. code of your view:
- (void) zoom {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath: #"transform.scale"];
animation.fromValue = [self.layer.mask valueForKeyPath: #"transform.scale"];
animation.toValue = #(1);
animation.duration = 2.0;
animation.timingFunction = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseInEaseOut];
animation.delegate = self;
// Important: change the actual layer property before installing the animation.
[self.layer.mask setValue: animation.toValue forKeyPath: animation.keyPath];
// Now install the explicit animation, overriding the implicit animation.
[self.layer.mask addAnimation: animation forKey: animation.keyPath];
return;
}
- (CAShapeLayer *) circleShapeLayer {
if (!_ circleShapeLayer) {
_circleShapeLayer = [SGMaskLayer layer];
_circleShapeLayer.delegate = _shapeLayer;
_circleShapeLayer.frame = self.bounds;
_circleShapeLayer.needsDisplayOnBoundsChange = YES;
}
return _circleShapeLayer;
}
#end
the code of the mask layer:
#interface CircleShapeLayer : CAShapeLayer
#end
#implementation CircleShapeLayer
- (void) drawLayer: (CALayer *) layer inContext: (CGContextRef) ctx {
UIGraphicsPushContext(ctx);
UIBezierPath *circlePath = [UIBezierPath bezierPathWithOvalInRect: self.bounds];
self.path = circlePath.CGPath;
UIGraphicsPopContext();
}
#end
from the documentation:
The layer’s alpha channel determines how much of the layer’s content
and background shows through. Fully or partially opaque pixels allow
the underlying content to show through but fully transparent pixels
block that content.
The default value of this property is nil nil. When configuring a
mask, remember to set the size and position of the mask layer to
ensure it is aligned properly with the layer it masks.
so I drew a circle with UIBezierPath to achieve the round mask. at the beginning I set the mask's scale factor to 0 so nothing of the view's layer is visible. then the scale factor is set to 1 (filling the layer's bounds) animated which gives a nice animation of a circle increasing it's radius from the center.
you might need one more animation shifting the center point of your view. both animations can be wrapped in a CAAnimationGroup.
Open in first view:
//Delegate method for annotation did select
- (void)tapOnAnnotation:(RMAnnotation *)annotation onMap:(RMMapView *)map;
{
//To get the touch point of the GMS marker to set them as circle transiton pos
CGPoint markerPoint = annotation.position;
x = markerPoint.x;
y = markerPoint.y;
circleSize = 10;
radiusChange = 0;
//Populate Same Values to next view to close
VenueScreen.x = x;
VenueScreen.y = y;
VenueScreen.view.hidden = YES;
timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:#selector(openVenueScreen) userInfo:nil repeats:YES];
VenueScreen.view.frame = CGRectMake(0, 0, 320, 480);
[self.view addSubview:VenueScreen.view];
}
//Circular transition to open Next view
-(void)openVenueScreen
{
VenueScreen.view.hidden = NO;
VenueScreen.view.userInteractionEnabled = NO;
VenueScreen.view.alpha = 0.9;
self.view.userInteractionEnabled = NO;
int rChange = 0; // Its doing proper masking while changing this value
int radius = circleSize-rChange;
CAShapeLayer *circleShapeLayer = [CAShapeLayer layer];
// Make a circular shape
circleShapeLayer.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(x+radiusChange, y+radiusChange, 2.0*radius, 2.0*radius)
cornerRadius:radius].CGPath;
radiusChange = radiusChange-10;
// Configure the apperence of the circle
[[VenueScreen.view layer] setMask:circleShapeLayer];
//Start Transition
circleSize = circleSize+10;
if (circleSize > 480)
{
[[VenueScreen.view layer] setMask:nil];
[timer invalidate]; //Stop titmer
VenueScreen.view.userInteractionEnabled = YES;
self.view.userInteractionEnabled = YES;
VenueScreen.view.alpha = 1;
//Populate to next view to close
VenueScreen.radiusChange = radiusChange;
}
}
Close in Next View:
//Close button Action
-(IBAction)DismissVenueScreen:(id)sender;
{
timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:#selector(dismissVenueScreen) userInfo:nil repeats:YES];
NSLog(#"close button clciked");
}
//Circular trasition to Close window
-(void)dismissVenueScreen
{
int rChange = 0; // Its doing proper masking while changing this value
int radius = circleSize-rChange;
CAShapeLayer *circleShapeLayer = [CAShapeLayer layer];
// Make a circular shape
circleShapeLayer.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(x+radiusChange,y+radiusChange, 2.0*radius, 2.0*radius)
cornerRadius:radius].CGPath;
// Configure the apperence of the circle
[[self.view layer] setMask:circleShapeLayer];
self.view.layer.masksToBounds = YES;
radiusChange = radiusChange+10;
circleSize = circleSize-10;
if (circleSize <= 0)
{
[timer invalidate]; //Stop titmer
[[self.view layer] setMask:nil];
[self.view removeFromSuperview];
circleSize = 480;
}
}
I'm trying to learn how to animate motion of the circle that I created in custom View. However, during the loop in startMovingCircle the circle doesn't show, it appears only when the loop finishes. I thought that if I call setNeedsDisplay every time it should redraw the view and the animation of moving circle should show up. Could someone tell me what am I doing wrong?
Also, I would like to ask if it is correct to program animation this way or should I use CoreAnimation and what are the benefits?
Can CoreAnimation animate shapes like circle, elipses, rectangle programmed in CoreGraphics?
MyDrawingView.m:
-(id)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
xCoordinate = 100.0;
yCoordinate = 100.0;
speed = 1.0;
}
return self;
}
- (void)drawRect:(CGRect)rect
{
NSLog(#"Drawing");
CGContextRef myContext = UIGraphicsGetCurrentContext();
CGContextBeginPath(myContext);
CGContextAddArc(myContext, xCoordinate, yCoordinate, 20.0, 0.0, 2 * M_PI, 0);
CGContextClosePath(myContext);
CGContextSetLineWidth(myContext, 1);
CGContextSetStrokeColorWithColor(myContext, [UIColor whiteColor].CGColor);
CGContextStrokePath(myContext);
}
-(void)moveCircle{
xCoordinate += speed;
yCoordinate += speed;
NSLog(#"Moving circle. X: %f, Y: %f", xCoordinate, yCoordinate);
[self setNeedsDisplay];
}
MyDrawingViewController.m:
-(void)startMovingCircle{
for(int i = 0; i < 1000; i++){
[myView moveCircle];
}
}
in -(void)moveCircle
you should create a core animation (with blocks)
and animate the center property of the view to animate.
You should add a pause in between iterations of your for() loop.
Based on an earlier stackoverflow suggestion, I am trying to create a playing card using CALayers added to a CATransformLayer. The idea is to create front and back CALayers and combine them in the CATransformLayer which can then be rotated, flipped, etc, with the proper side automatically showing. Below is example code trying to create a single card with a green front and a red back.
The red back is flipped about the Y-axis so that it is facing away from the green front. The green front calayer has a "higher" Z location than the red back.
When I do the transform though, I am simply seeing the card in what appears to be its non-flipped state. Any ideas where I am going wrong here?
#define DEGREES_TO_RADIANS(angle) (angle * M_PI / 180.0)
- (void)viewDidLoad
{
[super viewDidLoad];
CATransformLayer *cardContainer = [CATransformLayer layer];
cardContainer.bounds = CGRectMake(0,0, 150,200);
CALayer *cardFront = [CALayer layer];
cardFront.frame = cardContainer.bounds;
cardFront.backgroundColor = [UIColor greenColor].CGColor;
cardFront.borderColor = [UIColor blackColor].CGColor;
cardFront.borderWidth = 2.0;
cardFront.cornerRadius = 30.0;
cardFront.zPosition = 2; // Put front of card on top relative to back of card
cardFront.doubleSided = NO;
[cardContainer addSublayer:cardFront];
CALayer *cardBack = [CALayer layer];
cardBack.frame = cardContainer.bounds;
cardBack.backgroundColor = [UIColor redColor].CGColor;
cardBack.zPosition = 1;
cardBack.doubleSided = NO;
// Flip cardBack image so it is facing outward and visible when flipped
cardBack.transform = CATransform3DMakeRotation(DEGREES_TO_RADIANS(180),0.0,1.0,0.0);
[cardContainer addSublayer:cardBack];
UIView* cardView = [[UIView alloc] initWithFrame:cardContainer.bounds];
cardView.center = CGPointMake(self.view.center.x, self.view.center.y);
[cardView.layer addSublayer:cardContainer];
[self.view addSubview:cardView];
// Show the card flipped over (desired the red side to be showing, but instead shows green)
cardView.layer.transform = CATransform3DMakeRotation(DEGREES_TO_RADIANS(180),0.0,1.0,0.0);
}
Found the problem, you must apply the transform to the sublayerTransform property, as in:
cardView.layer.sublayerTransform = ...
I'm creating a simple app where when the user presses a button, a series of lines will be drawn on the screen and the user will be able to see these lines drawn in real time (almost like an animation).
My code looks something like this (has been simplified):
UIGraphicsBeginImageContext(CGSizeMake(300,300));
CGContextRef context = UIGraphicsGetCurrentContext();
for (int i = 0; i < 100; i++ ) {
CGContextMoveToPoint(context, i, i);
CGContextAddLineToPoint(context, i+20, i+20);
CGContextSetStrokeColorWithColor(context, [[UIColor blackColor] CGColor]);
CGContextStrokePath(context);
}
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
My problem is that:
1) As soon as the user presses the button, the UIThread blocks until the drawing is done.
2) I can't get the lines to be drawn on the screen one at a time - I've tried setting the UIImage directly inside the loop and also tried setting a layer content inside the loop.
How do I get around these problems?
You say "just like an animation". Why not do an actual animation, a la Core Graphics' CABasicAnimation? Do you really need to show it as a series of lines, or is a proper animation ok?
If you want to animate the actual drawing of the line, you could do something like:
#import <QuartzCore/QuartzCore.h>
- (void)drawBezierAnimate:(BOOL)animate
{
UIBezierPath *bezierPath = [self bezierPath];
CAShapeLayer *bezier = [[CAShapeLayer alloc] init];
bezier.path = bezierPath.CGPath;
bezier.strokeColor = [UIColor blueColor].CGColor;
bezier.fillColor = [UIColor clearColor].CGColor;
bezier.lineWidth = 5.0;
bezier.strokeStart = 0.0;
bezier.strokeEnd = 1.0;
[self.view.layer addSublayer:bezier];
if (animate)
{
CABasicAnimation *animateStrokeEnd = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animateStrokeEnd.duration = 10.0;
animateStrokeEnd.fromValue = [NSNumber numberWithFloat:0.0f];
animateStrokeEnd.toValue = [NSNumber numberWithFloat:1.0f];
[bezier addAnimation:animateStrokeEnd forKey:#"strokeEndAnimation"];
}
}
Then all you have to do is create the UIBezierPath for your line, e.g.:
- (UIBezierPath *)bezierPath
{
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0.0, 0.0)];
[path addLineToPoint:CGPointMake(200.0, 200.0)];
return path;
}
If you want, you can patch a bunch of lines together into a single path, e.g. here is a roughly sine curve shaped series of lines:
- (UIBezierPath *)bezierPath
{
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint point = self.view.center;
[path moveToPoint:CGPointMake(0, self.view.frame.size.height / 2.0)];
for (CGFloat f = 0.0; f < M_PI * 2; f += 0.75)
{
point = CGPointMake(f / (M_PI * 2) * self.view.frame.size.width, sinf(f) * 200.0 + self.view.frame.size.height / 2.0);
[path addLineToPoint:point];
}
return path;
}
And these don't block the main thread.
By the way, you'll obviously have to add the CoreGraphics.framework to your target's Build Settings under Link Binary With Libraries.
I've managed to get along using [UIView animateWithDuration... to get animations done that I need in my UI. Now I want to move an image along a curved path, and that whole CAAnimation cluster looks pretty daunting to me.
I'd be much obliged if someone could help me fill in the method I wish I could code, which would look like this:
- (void)makeAnImageFlyFrom:(UIImageView *)imageViewA to:(UIImageView *)imageViewB alongPath:(CGMutablePathRef)path duration:(NSTimeInterval)duration {
UIImage *imageToFly = imageViewA.image;
// magic, that i'm too lazy to learn right now goes here
// image flys along the path and gets scaled to match imageViewB.
// then view/layer hierarchy is just as it was, but imageViewB has a new image
// maybe this happens on animationDidStop...
imageViewB.image = imageToFly;
}
Feel free to replace params (like path ref) if you think there's a smarter interface for this kind of method. Thanks in advance.
Ok, a whole Sunday later, here's a method that I think works. I'm still unclear about a few things, noted in comments, but if you need this type of thing, feel free to cut and paste. I promise not call you lazy:
- (void)makeAnImageFlyFrom:(UIImageView *)imageViewA to:(UIImageView *)imageViewB duration:(NSTimeInterval)duration {
// it's simpler but less general to not pass in the path. i chose simpler because
// there's a lot of geometry work using the imageView frames here anyway.
UIImageView *animationView = [[UIImageView alloc] initWithImage:imageViewA.image];
animationView.tag = kANIMATION_IMAGE_TAG;
animationView.frame = imageViewA.frame;
[self addSubview:animationView];
// scale
CABasicAnimation *resizeAnimation = [CABasicAnimation animationWithKeyPath:#"bounds.size"];
[resizeAnimation setFromValue:[NSValue valueWithCGSize:imageViewA.bounds.size]];
[resizeAnimation setToValue:[NSValue valueWithCGSize:imageViewB.bounds.size]];
// build the path
CGRect aRect = [imageViewA convertRect:imageViewA.bounds toView:self];
CGRect bRect = [imageViewB convertRect:imageViewB.bounds toView:self];
// unclear why i'm doing this, but the rects converted to this view's
// coordinate system seemed have origin's offset negatively by half their size
CGFloat startX = aRect.origin.x + aRect.size.width / 2.0;
CGFloat startY = aRect.origin.y + aRect.size.height / 2.0;
CGFloat endX = bRect.origin.x + bRect.size.width / 2.0;
CGFloat endY = bRect.origin.y + bRect.size.height / 2.0;
CGFloat deltaX = endX - startX;
CGFloat deltaY = endY - startY;
// these control points suited the path i needed. your results may vary
CGFloat cp0X = startX + 0.3*deltaX;
CGFloat cp0Y = startY - 1.3*deltaY;
CGFloat cp1X = endX + 0.1*deltaX;
CGFloat cp1Y = endY - 0.5*deltaY;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, startX, startY);
CGPathAddCurveToPoint(path, NULL, cp0X, cp0Y, cp1X, cp1Y, endX, endY);
// keyframe animation
CAKeyframeAnimation *keyframeAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
keyframeAnimation.calculationMode = kCAAnimationPaced;
keyframeAnimation.fillMode = kCAFillModeForwards;
keyframeAnimation.removedOnCompletion = NO;
keyframeAnimation.path = path;
// assuming i need to manually release, despite ARC, but not sure
CGPathRelease(path);
// a little unclear about the fillMode, but it works
// also unclear about removeOnCompletion, because I remove the animationView
// but that seems to be insufficient
CAAnimationGroup *group = [CAAnimationGroup animation];
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
[group setAnimations:[NSArray arrayWithObjects:keyframeAnimation, resizeAnimation, nil]];
group.duration = duration;
group.delegate = self;
// unclear about what i'm naming with the keys here, and why
[group setValue:animationView forKey:#"animationView"];
[animationView.layer addAnimation:group forKey:#"animationGroup"];
}
// clean up after like this
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
UIImageView *imageViewForAnimation = (UIImageView *)[self viewWithTag:kANIMATION_IMAGE_TAG];
// get the imageView passed to the animation as the destination
UIImageView *imageViewB = (UIImageView *)[self viewWithTag:kDEST_TAG];
imageViewB.image = imageViewForAnimation.image;
[imageViewForAnimation removeFromSuperview];
}