I'm trying to change the affine transform of a CALayer so what's drawn in it gets deformed.
First I was doing this without CALayers, just redrawing with a different CGAffineTransform passed to CGContextConcactCTM.
That worked, but it wasn't fast enough, so I decided to do it with CALayer so it won't get redrawn every time. It just get transformed.
The problem is that setting myLayer.affineTransform = myTransform; doesn't seem to have the same effect as redrawing with CGContextConcatCTM(myTransform);
I'm new to Objective-C and pretty lame at math, so I'm sure I'm doing something wrong. But I can't see what.
Thanks.
Ok, nevermind, I found out.
Turns out myLayer.affineTransform does the transform relative to the center of the layer where as CGContextContactCTM does it relative to the origin.
So I just concatenated 2 other transforms:
CGPoint center;
center.x = capa.bounds.origin.x + capa.bounds.size.width/2;
center.y = capa.bounds.origin.y + capa.bounds.size.height/2;
CGAffineTransform trf1 = CGAffineTransformMakeTranslation(center.x, center.y);
CGAffineTransform trf2 = CGAffineTransformMakeTranslation(-center.x, -center.y);
capa.affineTransform = CGAffineTransformConcat(trf1, CGAffineTransformConcat(mat, trf2));
Related
How can I accept touch input beyond the scene's bounds, so that no matter what I set self.position to, touches can still be detected?
I'm creating a tile based game from Ray Winderlich on Cocos2d version 3.0. I am at the point of setting the view of the screen to a zoomed in state on my tile map. I have successfully been able to do that although now my touches are not responding since I'm out of the coordinate space the touches used to work on.
This method is called to set the zoomed view to the player's position:
-(void)setViewPointCenter:(CGPoint)position{
CGSize winSize = [CCDirector sharedDirector].viewSizeInPixels;
int x = MAX(position.x, winSize.width/2);
int y = MAX(position.y, winSize.height/2);
x = MIN(x, (_tileMap.mapSize.width * _tileMap.tileSize.width) - winSize.width / 2);
y = MIN(y, (_tileMap.mapSize.height * _tileMap.tileSize.height) - winSize.height / 2);
CGPoint actualPosition = ccp(x, y);
CGPoint centerOfView = ccp(winSize.width/2, winSize.height/2);
NSLog(#"centerOfView%#", NSStringFromCGPoint(centerOfView));
CGPoint viewPoint = ccpSub(centerOfView, actualPosition);
NSLog(#"viewPoint%#", NSStringFromCGPoint(viewPoint));
//This changes the position of the helloworld layer/scene so that
//we can see the portion of the tilemap we're interested in.
//That however makes my touchbegan method stop firing
self.position = viewPoint;
}
This is what the NSLog prints from the method:
2014-01-30 07:05:08.725 TestingTouch[593:60b] centerOfView{512, 384}
2014-01-30 07:05:08.727 TestingTouch[593:60b] viewPoint{0, -832}
As you can see the y coordinate is -800. If i comment out the line self.position = viewPoint then the self.position reads {0, 0} and touches are detectable again but then we don't have a zoomed view on the character. Instead it shows the view on the bottom left of the map.
Here's a video demonstration.
How can I fix this?
Update 1
Here is the github page to my repository.
Update 2
Mark has been able to come up with a temporary solution so far by setting the hitAreaExpansion to a large number like so:
self.hitAreaExpansion = 10000000.0f;
This will cause touches to respond again all over! However, if there is a solution that would not require me to set the property with an absolute number then that would be great!
-edit 3-(tldr version):
setting the contentsize of the scene/layer to the size of the tilemap solves this issue:
[self setContentSize: self.tileMap.contentSize];
original replies below:
You would take the touch coordinate and subtract the layer position.
Generally something like:
touchLocation = ccpSub(touchLocation, self.position);
if you were to scale the layer, you would also need appropriate translation for that as well.
-edit 1-:
So, I had a chance to take another look, and it looks like my 'ridiculous' number was not ridiculous enough, or I had made another change. Anyway, if you simply add
self.hitAreaExpansion = 10000000.0f; // I'll let you find a more reasonable number
the touches will now get registered.
As for the underlying issue, I believe it to be one of content scale that is not set correctly, but again, I'll now leave that to you. I did however find out that when looking through some of the tilemap class, that tilesize is said to be in pixels, not points, which I guess is somehow related to this.
-edit 2-:
It bugged me with the sub-optimal answer, so I looked a little further. Forgive me, I hadn't looked at v3 until I saw this question. :p
after inspecting the base class and observing the scene/layer's value of:
- (BOOL)hitTestWithWorldPos:(CGPoint)pos;
it became obvious that the content size of the scene/layer was being set to the current view size, which in the case of an iPad is (1024, 768)
The position of the layer after the setViewPointCenter call is fully above the initial view's position, hence, the touch was being suppressed. by setting the layer/scene contentSize to the size of the tilemap, the touchable area is now expanded over the entire map, which allows the node to process the touch.
FUTURE VIEWERS:
I have managed to finish this rotation animation and code with description can be found on tho question. NSImage rotation in NSView is not working
Before you proceed please up vote Duncan C 's answer. As I manage to achieve this rotation from his answer.
I have an image like this,
I want to keep rotating this sync icon, On a different thread. Now I tried using Quartz composer and add the animation to QCView but it is has very crazy effect and very slow too.
Question :
How do I rotate this image continuously with very less processing expense?
Effort
I read CoreAnimation, Quartz2D documentation but I failed to find the way to make it work. The only thing I know so far is, I have to use
CALayer
CAImageRef
AffineTransform
NSAnimationContext
Now, I am not expecting code, but an understanding with pseudo code will be great!
Getting an object to rotate more than 180 degrees is actually a little bit tricky. The problem is that you specify a transformation matrix for the ending rotation, and the system decides to rotate in the other direction.
What I've done is to create a CABasicAnimation of less than 180 degrees, set up to be additive , and with a repeat count. Each step in the animation animates the object more.
The following code is taken from an iOS application, but the technique is identical in Mac OS.
CABasicAnimation* rotate = [CABasicAnimation animationWithKeyPath: #"transform.rotation.z"];
rotate.removedOnCompletion = FALSE;
rotate.fillMode = kCAFillModeForwards;
//Do a series of 5 quarter turns for a total of a 1.25 turns
//(2PI is a full turn, so pi/2 is a quarter turn)
[rotate setToValue: [NSNumber numberWithFloat: -M_PI / 2]];
rotate.repeatCount = 11;
rotate.duration = duration/2;
rotate.beginTime = start;
rotate.cumulative = TRUE;
rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
CAAnimation objects operate on layers, so for Mac OS, you'll need to set the "wants layer" property in interface builder, and then add the animation to your view's layer.
To make your view rotate forever, you'd set repeat count to some very large number like 1e100.
Once you've created your animation, you'd add it to your view's layer with code something like this:
[myView.layer addAnimation: rotate forKey: #"rotateAnimation"];
That's about all there is to it.
Update:
I've recently learned of another way to handle rotations of greater than 180 degrees, or continuous rotations.
There is a special object called a CAValueFunction that lets you apply a change to your layer's transform using an arbitrary value, including values that specify multiple full rotations.
You create a CABasicAnimation of your layer's transform property, but then instead of providing a transform, the value you supply is an NSNumber that gives the new rotation angle. If you provide a new angle like 20pi, your layer will rotate 10 full rotations (2pi/rotation). The code looks like this:
//Create a CABasicAnimation object to manage our rotation.
CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:#"transform"];
rotation.duration = 10.0;
CGFLOAT angle = 20*M_PI;
//Set the ending value of the rotation to the new angle.
rotation.toValue = #(angle);
//Have the rotation use linear timing.
rotation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
/*
This is the magic bit. We add a CAValueFunction that tells the CAAnimation we are
modifying the transform's rotation around the Z axis.
Without this, we would supply a transform as the fromValue and toValue, and
for rotations > a half-turn, we could not control the rotation direction.
By using a value function, we can specify arbitrary rotation amounts and
directions and even rotations greater than 360 degrees.
*/
rotation.valueFunction =
[CAValueFunction functionWithName: kCAValueFunctionRotateZ];
/*
Set the layer's transform to it's final state before submitting the animation, so
it is in it's final state once the animation completes.
*/
imageViewToAnimate.layer.transform =
CATransform3DRotate(imageViewToAnimate.layer.transform, angle, 0, 0, 1.0);
[imageViewToAnimate.layer addAnimation:rotation forKey:#"transform.rotation.z"];
(I Extracted the code above from a working example application, and took out some things that weren't directly related to the subject. You can see this code in use in the project KeyframeViewAnimations (link) on github. The code that does the rotation is in a method called `handleRotate'
This question has been asked before but in a slightly different way and I was unable to get any of the answers to work the way I wanted, so I am hoping somebody with great Core Animation skills can help me out.
I have a set of cards on a table. As the user swipes up or down the set of cards move up and down the table. There are 4 cards visible on the screen at any given time, but only the second card is showing its face. As the user swipes the second card flips back onto its face and the next card (depending on the swipe direction) lands in it's place showing its face.
I have set up my card view class like this:
#interface WLCard : UIView {
UIView *_frontView;
UIView *_backView;
BOOL flipped;
}
And I have tried flipping the card using this piece of code:
- (void) flipCard {
[self.flipTimer invalidate];
if (flipped){
return;
}
id animationsBlock = ^{
self.backView.alpha = 1.0f;
self.frontView.alpha = 0.0f;
[self bringSubviewToFront:self.frontView];
flipped = YES;
CALayer *layer = self.layer;
CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity;
rotationAndPerspectiveTransform.m34 = 1.0 / 500;
rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, M_PI, 1.0f, 0.0f, 0.0f);
layer.transform = rotationAndPerspectiveTransform;
};
[UIView animateWithDuration:0.25
delay:0.0
options: UIViewAnimationCurveEaseInOut
animations:animationsBlock
completion:nil];
}
This code works but it has the following problems with it that I can't seem to figure out:
Only half of the card across the x-axis is animated.
Once flipped, the face of the card is upside down and mirrored.
Once I've flipped the card I cannot get the animation to ever run again. In other words, I can run the animation block as many times as I want, but only the first time will animate. The subsequent times I try to animate lead to just a fade in and out between the subviews.
Also, bear in mind that I need to be able to interact with the face of the card. i.e. it has buttons on it.
If anybody has run into these issues it would be great to see your solutions. Even better would be to add a perspective transform to the animation to give it that extra bit of realism.
This turned out to be way simpler than I thought and I didn't have to use any CoreAnimation libraries to achieve the effect. Thanks to #Aaron Hayman for the clue. I used transitionWithView:duration:options:animations:completion
My implementation inside the container view:
[UIView transitionWithView:self
duration:0.2
options:UIViewAnimationOptionTransitionFlipFromBottom
animations: ^{
[self.backView removeFromSuperview];
[self addSubview:self.frontView];
}
completion:NULL];
The trick was the UIViewAnimationOptionTransitionFlipFromBottom option. Incidentally, Apple has this exact bit of code in their documentation. You can also add other animations to the block like resizing and moving.
Ok, this won't be a complete solution but I'll point out some things that might be helpful. I'm not a Core-Animation guru but I have done a few 3D rotations in my program.
First, there is no 'back' to a view. So if you rotate something by M_PI (180 degrees) you're going to be looking at that view as though from the back (which is why it's upside down/mirrored).
I'm not sure what you mean by:
Only half of the card across the x-axis is animated.
But, it it might help to consider your anchor point (the point at which the rotation occurs). It's usually in the center, but often you need it to be otherwise. Note that anchor points are expressed as a proportion (percentage / 100)...so the values are 0 - 1.0f. You only need to set it once (unless you need it to change). Here's how you access the anchor point:
layer.anchorPoint = CGPointMake(0.5f, 0.5f) //This is center
The reason the animation only ever runs once is because transforms are absolute, not cumulative. Consider that you're always starting with the identity transform and then modifying that, and it'll make sense...but basically, no animation occurs because there's nothing to animate the second time (the view is already in the state you're requesting it to be in).
If you're animating from one view to another (and you can't use [UIView transitionWithView:duration:options:animations:completion:];) you'l have to use a two-stage animation. In the first stage of the animation, for the 'card' that is being flipped to backside, you'll rotate the view-to-disappear 'up/down/whatever' to M_PI_2 (at which point it will be 'gone', or not visible, because of it's rotation). And in the second stage, you're rotate the backside-of-view-to-disappear to 0 (which should be the identity transform...aka, the view's normal state). In addition, you'll have to do the exact opposite for the 'card' that is appearing (to frontside). You can do this by implementing another [UIView animateWithDuration:...] in the completion block of the first one. I'll warn you though, doing this can get a little bit complicated. Especially since you're wanting views to have a 'backside', which will basically require animating 4 views (the view-to-disappear, the view-to-appear, backside-of-view-to-disappear, and the backside-of-view-to-appear). Finally, in the completion block of the second animation you can do some cleanup (reset view that are rotated and make their alpha 0.0f, etc...).
I know this is complicated, so you might want read some tutorial on Core-Animation.
#Aaron has some good info that you should read.
The simplest solution is to use a CATransformLayer that will allow you to place other CALayer's inside and maintain their 3D hierarchy.
For example to create a "Card" that has a front and back you could do something like this:
CATransformLayer *cardContainer = [CATransformLayer layer];
cardContainer.frame = // some frame;
CALayer *cardFront = [CALayer layer];
cardFront.frame = cardContainer.bounds;
cardFront.zPosition = 2; // Higher than the zPosition of the back of the card
cardFront.contents = (id)[UIImage imageNamed:#"cardFront"].CGImage;
[cardContainer addSublayer:cardFront];
CALayer *cardBack = [CALayer layer];
cardBack.frame = cardContainer.bounds;
cardBack.zPosition = 1;
cardBack.contents = (id)[UIImage imageNamed:#"cardBack"].CGImage; // You may need to mirror this image
[cardContainer addSublayer:cardBack];
With this you can now apply your transform to cardContainer and have a flipping card.
#Paul.s
I followed your approach with card container but when i applt the rotation animation on card container only one half of the first card rotates around itself and finally the whole view appears.Each time one side is missing in the animation
Based on Paul.s this is updated for Swift 3 and will flip a card diagonally:
func createLayers(){
transformationLayer = CATransformLayer(layer: CALayer())
transformationLayer.frame = CGRect(x: 15, y: 100, width: view.frame.width - 30, height: view.frame.width - 30)
let black = CALayer()
black.zPosition = 2
black.frame = transformationLayer.bounds
black.backgroundColor = UIColor.black.cgColor
transformationLayer.addSublayer(black)
let blue = CALayer()
blue.frame = transformationLayer.bounds
blue.zPosition = 1
blue.backgroundColor = UIColor.blue.cgColor
transformationLayer.addSublayer(blue)
let tgr = UITapGestureRecognizer(target: self, action: #selector(recTap))
view.addGestureRecognizer(tgr)
view.layer.addSublayer(transformationLayer)
}
Animate a full 360 but since the layers have different zPositions the different 'sides' of the layers will show
func recTap(){
let animation = CABasicAnimation(keyPath: "transform")
animation.delegate = self
animation.duration = 2.0
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
animation.toValue = NSValue(caTransform3D: CATransform3DMakeRotation(CGFloat(Float.pi), 1, -1, 0))
transformationLayer.add(animation, forKey: "arbitrarykey")
}
I have a really annoying issue trying to draw into a bitmap CGContext. What I am trying to do is I have a couple of images to draw into the full size of the image. One can come in at any UIImageOrientation and I've written the code to correctly rotate that properly, but I'm struggling with the second bit which is trying to draw another view at an arbitrary rotation about its centre.
The other view comprises an image drawn possibly outside of its bounds. What I am having a problem with is drawing these at a rotated angle as though it was a UIView that had an affine transform applied to it. e.g. imagine a UIView at {100, 300} of size {20, 20} and an affine transform rotating it by 45 degrees. It would be rotated about {110, 310}.
What I have tried is this:
- (void)drawOtherViewInContext:(CGContextRef)context atRect:(CGRect)rect withRotation:(CGFloat)rotation contextSize:(CGSize)contextSize {
CGRect thisFrame = <SOLVED_FEATURE_FRAME_RELATIVE_TO_RECT_SIZE>;
thisFrame.origin.y = contextSize.height - thisFrame.origin.y - thisFrame.size.height;
CGRect rotatedRect = CGRectApplyAffineTransform(CGRectMake(0.0f, 0.0f, rect.size.width, rect.size.height), CGAffineTransformMakeRotation(-rotation));
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformTranslate(transform, rect.origin.x, contextSize.height - rect.origin.y - rect.size.height);
transform = CGAffineTransformTranslate(transform,
+(rotatedRect.size.width/2.0f),
+(rotatedRect.size.height/2.0f));
transform = CGAffineTransformRotate(transform, -rotation);
transform = CGAffineTransformTranslate(transform,
-(rect.size.width/2.0f),
-(rect.size.height/2.0f));
CGContextConcatCTM(context, transform);
CGContextDrawImage(context, thisFrame, theCGImageToDraw);
CGContextConcatCTM(context, CGAffineTransformInvert(transform));
}
So what I am doing there, I think, is this:
Translate to the bottom left of rect which is where this view is meant to be drawn.
Translate by half the rotated size in x and y.
Rotate by the required angle.
Translate back half the original size in x and y.
I thought that this would be what I wanted to do because the first step translates the coordinate system to be such that thisFrame is drawn correctly relative to where we're being told to draw (by the rect method parameter). Then it's a pretty normal rotate about the centre of a rectangle.
The problem is that when rotated by say 45 degrees, the image is drawn slightly out of place. It's almost correct, but just not quite. When at 0, 90, 180 or 270 degrees then the position is pretty much spot on, maybe a few pixels out but when at 45, 135, 225, 315 degrees the position is too far up and to the right.
Can anyone see what I'm doing wrong here?
Update:
Silly me, it's bigger because I was passing in the wrong rect! Edited to get rid of references to it being the wrong size. It's still not quite in the right place though.
OK I have fixed it. The first point was that I was passing in the wrong rect at first as I was grabbing the frame from a UIView which had an affine transform applied to it, and as we all know the frame in that case is undefined. More likely it's the CGRect that comes from CGRectApplyAffineTransform(bounds, transform) but anyway, I fixed that one.
Then the main problem of drawing offset was fixed by changing my transform to this:
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformTranslate(transform, rect.origin.x, contextSize.height - rect.origin.y - rect.size.height);
transform = CGAffineTransformTranslate(transform,
+(rect.size.width/2.0f),
+(rect.size.height/2.0f));
transform = CGAffineTransformRotate(transform, -rotation);
transform = CGAffineTransformTranslate(transform,
-(rect.size.width/2.0f),
-(rect.size.height/2.0f));
That's what I had originally thought I should be doing, but for some reason I changed it to use the rotated CGRect.
I have a UIScrollView which I'm using to represent an axis on a graph. I'd like the user to be able to zoom in on the axis using the usual pinch motion, but for it to only scale in the vertical direction, not horizontally.
My question is similar to this one, but I've tried the solution suggested there (overriding the subview's SetTransform method so that it ignores scaling in one direction) and it works perfectly when constraining scaling horizontally, but not vertically. When I try implementing it vertically the first pinch action works fine, but subsequent pinches seem to reset the zoom scale to one before having any effect.
Does anyone know what might be causing this behaviour, and more importantly how I can get around it?
I'm using MonoTouch but answers using Objective-C are fine.
I know this question was posted quite a while ago, but here's an answer for anyone stuck on this problem.
I looked over the question you linked to, rankAmateur, and I think the simple way to fix the solution found there to suit your needs is to replace the CGAffineTransform's "a" property with its "d" property in the setTransform: method.
- (void)setTransform:(CGAffineTransform)newValue;
{
CGAffineTransform constrainedTransform = CGAffineTransformIdentity;
// constrainedTransform.a = newValue.a;
constrainedTransform.d = newValue.d;
[super setTransform:constrainedTransform];
}
I'm not very well versed in CGAffineTransorm, but this worked for me and after browsing the documentation it seems the "a" property corresponds to a view's x-axis and the "d" property corresponds to a view's y-axis.
EDIT
So after going back and realizing what the question really was, I did some more digging into this and I'm a bit stumped, but having experienced the same behavior that rankAmateur mentions above, it seems incredibly unusual for the CGAffineTransform to work perfectly well with zoomScale when zooming is constrained to only horizontally, but not when constrained to only vertically.
The only hypothesis I can offer, is that it might have something to do with the differing default coordinate systems of Core Graphics and UIKit, since in those coordinate systems the x-axis functions in the same way, while the y-axis functions oppositely. Perhaps somehow this gets muddled up in the previously mentioned overriding of setTransform.
This answer depends heavily on the answer from starryVere (thumbs up!)
this is starryVere's code in Swift. It is in the zoomed UIView subclass:
var initialScale: CGFloat = 1.0
override var transform: CGAffineTransform {
set{
//print("1 transform... \(newValue), frame=\(self.frame), bounds=\(self.bounds)")
var constrainedTransform = CGAffineTransformIdentity
constrainedTransform.d = self.initialScale * newValue.d // vertical zoom
//constrainedTransform.a = newValue.a // horizontal zoom
super.transform = constrainedTransform
//print("2 transform... \(constrainedTransform), frame=\(self.frame), bounds=\(self.bounds)")
}
get{
return super.transform
}
}
The commented out prints are very helpful to understand what happens with bounds and frame during the transformation.
Now to the scale problem:
the method scrollViewDidEndZooming of the containing UIScrollViewDelegate has a parameter scale. According to my tests this parameter scale contains the value zoomedView.transform.a which is the horizontal scale factor that we set to 1.0 using CGAffineTransformIdentity. So scale is always 1.0.
The fix is easy:
func scrollViewDidEndZooming(scrollView: UIScrollView, withView view: UIView?, atScale scale: CGFloat) {
let myScale = zoomView.transform.d
}
use myScale like you would use scale in cases with horizontal zoom.
After struggling with the same issue, I was able to come up with a workaround.
Use this code for the setTransform method.
-(void)setTransform:(CGAffineTransform)transform
{
CGAffineTransform constrainedTransform = CGAffineTransformIdentity;
constrainedTransform.d = self.initialScale * transform.d;
constrainedTransform.d = (constrainedTransform.d < MINIMUM_ZOOM_SCALE) ? MINIMUM_ZOOM_SCALE : constrainedTransform.d;
[super setTransform:constrainedTransform];
}
Set the initialScale property from within the scrollViewWillBeginZooming delegate method.
It will be more helpful if you provide a sample code of what you are trying, but i am giving you some lines to try. Actually you have to make the content size width equal to "320" i.e. equal to the the screen size of iPhone.
scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 45,320,480)];
scrollView.contentSize = CGSizeMake(320,1000);
scrollView.showsVerticalScrollIndicator = YES;
The MonoTouch version follows:
scrollView = new UIScrollView (new RectangleF (0, 45, 320, 480)) {
ContentSize = new SizeF (320, 1000),
ShowVerticalScrollIndicator = true
};
Hope it helps.. :)
and Yes dont forget to accept the answer if it helps :D