SKShapeNode.Get Radius? - objective-c

Is it possible to get an SKShapeNode's radius value?
The problem is, I need to create an identical circle after the user releases it, whilst removing the first circle from the view.
SKShapeNode *reticle = [SKShapeNode shapeNodeWithCircleOfRadius:60];
reticle.fillColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.3];
reticle.strokeColor = [UIColor clearColor];
reticle.position = CGPointMake(location.x - 50, location.y + 50);
reticle.name = #"reticle";
reticle.userInteractionEnabled = YES;
[self addChild:reticle];
[reticle runAction:[SKAction scaleTo:0.7 duration:3] completion:^{
[reticle runAction:[SKAction scaleTo:0.1 duration:1]];
}];
Then
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
....
SKNode *node = [self childNodeWithName:#"reticle"];
//take the position of the node, draw an imprint
/////////Here is where I need to get the circle radius.
[node removeFromParent];
}
How does one take the circle radius, so I can then say
SKShapeNode *imprint = [SKShapeNode shapeNodeWithCircleOfRadius:unknownValue];
I've tried, making an exact copy of the circle with
SKShapeNode *imprint = (SKShapeNode *)node;
However, this still follows the animation where as I need it to stop, at the point it was at. I don't want to take a "stopAllAnimations" approach.

You can use the shape node's path property to determine the bounding box of the shape with CGPathGetBoundingBox(path). From the bounds, you can compute the radius of the circle by dividing the width (or height) by 2. For example,
CGRect boundingBox = CGPathGetBoundingBox(circle.path);
CGFloat radius = boundingBox.size.width / 2.0;

Updated for Swift 5
myVariable.path.boundingBox.width / 2

Related

SpriteKit physics body returning to original vertical position after hit/knock

I'm trying to make a physics mechanic where a vertically standing object can be hit or knocked down and then will pivot back up to it's origin position. Think of it like a floor mounted punch bag. So the object will have a low pivot/anchor point.
I just wanted a little theoretical direction in how to approach this using SpriteKit physics.
Any help would be really appreciated.
Thanks
The following creates a composite object by joining two bodies: a circle and a weight. The weight is offset relative to the center of the circle and is much denser. When added to the scene, gravity rotates the combined object so the side with the weight is on the bottom. To use it 1) create a new sprite kit game, 2) replace the default initWithSize and touchesBegan methods with this code, and 3) run and click at various locations in the scene.
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
/* Setup your scene here */
self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
}
return self;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
SKShapeNode *circle = [SKShapeNode node];
circle.path =[UIBezierPath bezierPathWithOvalInRect: CGRectMake(-32, -32, 64, 64)].CGPath;
circle.position = location;
circle.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:32];
SKSpriteNode *weight = [SKSpriteNode spriteNodeWithColor:[UIColor whiteColor] size:CGSizeMake(8, 8)];
// Adjust this to get the desire effect
weight.position = CGPointMake(location.x+1, location.y+28);
weight.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:4];
// Adjust this to get the desired effect
weight.physicsBody.density = 100.0;
// The physics bodies must be in the scene before adding the joint
[self addChild:circle];
[self addChild:weight];
// Join the circle and the weight with a physics joint
SKPhysicsJoint *joint = [SKPhysicsJointFixed jointWithBodyA:circle.physicsBody bodyB:weight.physicsBody anchor:weight.position];
[self.physicsWorld addJoint:joint];
}
}

Antialiasing on CAShapeLayers

I have a custom view for a loading indicator which is comprised of a number of CAShapeLayers which I perform various animations on to represent various states. In the init method I create a number of shape layers and add them as sublayers to the view's layer. The issue I'm having is the shapes that I'm drawing have very rough edges and don't seem to be antialiased. I've tried a number of things from similar answers on here but I can't seem to antialias the shapes. I've never had this problem before then drawing directly in drawRect.
Is there anything I'm missing or is there a better way to accomplish what I'm trying to do?
Update: Here's a comparison of how the shapes are being drawn:
On the left I'm drawing in layoutSubviews using this code:
CGFloat lineWidth = 2.5f;
CGFloat radius = (self.bounds.size.width - lineWidth)/2;
CGPoint center = CGPointMake(CGRectGetWidth(self.frame)/2, CGRectGetHeight(self.frame)/2);
CGFloat startAngle = - ((float)M_PI / 2);
CGFloat endAngle = (2 * (float)M_PI) + startAngle;
UIBezierPath *trackPath = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:startAngle
endAngle:endAngle
clockwise:YES];
self.trackLayer.path = trackPath.CGPath;
self.trackLayer.lineWidth = lineWidth;
On the right I'm calling a separate method from awakeFromNib:
-(void)awakeFromNib
{
....
self.trackLayer = [self trackShape]
[self.layer addSublayer:self.trackLayer];
}
-(CAShapeLayer*)trackShape
{
float start_angle = 2*M_PI*-M_PI_2;
float end_angle = 2*M_PI*1-M_PI_2;
float minSize = MIN(self.frame.size.width, self.frame.size.height);
float radius = (minSize-_strokeWidth)/2 -4;
CGPoint center = CGPointMake(self.frame.size.width/2,self.frame.size.height/2);
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:start_angle
endAngle:end_angle
clockwise:YES].CGPath;
circle.strokeColor = self.trackColor.CGColor;
circle.fillColor = nil;
circle.lineWidth = (_strokeWidth == -1.0) ? minSize * _strokeWidthRatio
: _strokeWidth;
circle.rasterizationScale = [[UIScreen mainScreen] scale];
return circle;
}
The picture you provided is exactly what happened to me. I got the chunky circle. In my case, the code was inside the drawRect method. The problem was that I was adding a CAShapeLayer, as a subLayer of my view, every time drawRect was called.
So to anyone who stumbles upon something similar, keep track of the CAShapeLayer you are adding or clear everything before drawing again in the drawRect.

Set size of UIView based on content size

Is it possible to draw something on a UIView, then, after the drawing is completed, resize the view to be the same size as the thing that was previously drawn?
For example, if I draw a circle on a UIView, I would like to crop the UIView to the dimensions of the circle that I just drew on the view.
UPDATE
I am looking into using a CAShapeLayer as a possible solution. Does anyone know how to convert a UIBezierPath to a CAShapeLayer then set the position of the CAShapeLayer?
I have tried:
shapeLayer.path = bezierPath.CGPath;
shapeLayer.position = CGPointMake(0, 0);
but this does not work.
Yes you can do that. Have a look at this example that will answer both your questions.
First of all you need to add a UIView called myView and attach it to an IBOutlet ivar.
Define this global values for demonstration purposes:
#define POSITION CGPointMake(50.0,50.0)
#define SIZE CGSizeMake(100.0,100.0)
Declare two methods, one that will draw a shape in myView and another one that will resize the view to adapt it to the drawn shape:
-(void) circle
{
CAShapeLayer *layerData = [CAShapeLayer layer];
layerData.fillColor = [UIColor greenColor].CGColor;
UIBezierPath * dataPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0.0, 0.0, SIZE.width, SIZE.height)];
layerData.path = dataPath.CGPath;
layerData.position = CGPointMake(POSITION.x, POSITION.y);
[myView.layer addSublayer:layerData];
}
-(void) resize
{
((CAShapeLayer *)[myView.layer.sublayers objectAtIndex:0]).position = CGPointMake(0.0, 0.0);
myView.frame = CGRectMake(POSITION.x + myView.frame.origin.x , POSITION.y + myView.frame.origin.y, SIZE.width, SIZE.height);
}
Finally, in viewWillAppear: call both methods:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self circle];
[self resize];
}
You can run the same code calling only circle and calling both methods. In both cases the drawn circle will be at the same exact position but in the second case myView has been resized to have the same size as the drawn shape.
Hope it helps.

Changing a circle's size with UIPinchGestureRecognizer

I'm drawing a simple circle in the center of the screen:
int radius = 100;
- (void)addCircle {
self.circle = [CAShapeLayer layer];
self.circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius)
cornerRadius:radius].CGPath;
self.circle.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius,
CGRectGetMidY(self.view.frame)-radius);
self.circle.fillColor = [UIColor clearColor].CGColor;
self.circle.strokeColor = [UIColor blackColor].CGColor;
self.circle.lineWidth = 5;
[self.view.layer addSublayer:self.circle];
}
Using the pinch gesture, I allow the user to increase/decrease the radius of the shape:
- (void)scale:(UIPinchGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer.state != UIGestureRecognizerStateBegan) {
if (gestureRecognizer.scale < lastScale) {
--radius;
}
else if (gestureRecognizer.scale > lastScale) {
++radius;
}
// Center the shape in self.view
self.circle.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius, CGRectGetMidY(self.view.frame)-radius);
self.circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius) cornerRadius:radius].CGPath;
}
lastScale = gestureRecognizer.scale;
}
However, the circle doesn't stay dead center. Instead, it bounces around the middle and doesn't settle until the gesture finishes.
Does anyone know why this is happening and if so, how I can prevent it?
There are a few problems in your code. As #tc. said, you're not setting the shape layer's frame (or bounds). The default layer size is CGSizeZero, which is why you're having to offset the layer's position by the radius every time you change the radius.
Also, the position and path properties of a shape layer are animatable. So by default, when you change them, Core Animation will animate them to their new values. The path animation is contributing to your unwanted behavior.
Also, you should set the layer's position or frame based on self.view.bounds, not self.view.frame, because the layer's position/frame is the coordinate system of self.view, not the coordinate system of self.view.superview. This will matter if self.view is the top-level view and you support interface autorotation.
I would suggest revising how you're implementing this. Make radius a CGFloat property, and make setting the property update the layer's bounds and path:
#interface ViewController ()
#property (nonatomic, strong) CAShapeLayer *circle;
#property (nonatomic) CGFloat radius;
#end
#implementation ViewController
- (void)setRadius:(CGFloat)radius {
_radius = radius;
self.circle.bounds = CGRectMake(0, 0, 2 * radius, 2 * radius);
self.circle.path = [UIBezierPath bezierPathWithOvalInRect:self.circle.bounds].CGPath;
}
If you really want to force the radius to be an integer, I suggest internally tracking it as a float anyway, because the user interaction is smoother if it's a float. Just round it in a temporary variable before creating the CGRect for the bounds and path:
CGFloat intRadius = roundf(radius);
self.circle.bounds = CGRectMake(0, 0, 2 * intRadius, 2 * intRadius);
self.circle.path = [UIBezierPath bezierPathWithOvalInRect:self.circle.bounds].CGPath;
In addCircle, just set the radius property and let that setter take care of setting the layer's bounds and path. Also defer setting the layer's position until the system's layout phase. That way, you'll reposition the circle in the center again after an interface rotation.
- (void)viewDidLoad {
[super viewDidLoad];
[self addCircle];
}
- (void)addCircle {
self.circle = [CAShapeLayer layer];
self.circle.fillColor = nil;
self.circle.strokeColor = [UIColor blackColor].CGColor;
self.circle.lineWidth = 5;
self.radius = 100;
[self.view.layer addSublayer:self.circle];
[self.view setNeedsLayout];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.circle.position = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
}
Finally, to handle a pinch gesture, just set the new radius to the old radius times the gesture's scale. The radius setter will take care of updating the layer's path and bounds. Then reset the gesture's scale to 1. This is simpler than tracking the gesture's prior scale. Also, use CATransaction to disable animation of the path property.
- (IBAction)pinchGestureWasRecognized:(UIPinchGestureRecognizer *)recognizer {
[CATransaction begin]; {
[CATransaction setDisableActions:YES];
self.radius *= recognizer.scale;
recognizer.scale = 1;
} [CATransaction commit];
}

Scaling MKMapView Annotations relative to the zoom level

The Problem
I'm trying to create a visual radius circle around a annonation, that remains at a fixed size in real terms. Eg. So If i set the radius to 100m, as you zoom out of the Map view the radius circle gets progressively smaller.
I've been able to achieve the scaling, however the radius rect/circle seems to "Jitter" away from the Pin Placemark as the user manipulates the view.
I'm lead to believe this is much easier to achieve on the forthcoming iPhone OS 4, however my application needs to support 3.0.
The Manifestation
Here is a video of the behaviour.
The Implementation
The annotations are added to the Mapview in the usual fashion, and i've used the delegate method on my UIViewController Subclass (MapViewController) to see when the region changes.
-(void)mapView:(MKMapView *)pMapView regionDidChangeAnimated:(BOOL)animated{
//Get the map view
MKCoordinateRegion region;
CGRect rect;
//Scale the annotations
for( id<MKAnnotation> annotation in [[self mapView] annotations] ){
if( [annotation isKindOfClass: [Location class]] && [annotation conformsToProtocol:#protocol(MKAnnotation)] ){
//Approximately 200 m radius
region.span.latitudeDelta = 0.002f;
region.span.longitudeDelta = 0.002f;
region.center = [annotation coordinate];
rect = [[self mapView] convertRegion:region toRectToView: self.mapView];
if( [[[self mapView] viewForAnnotation: annotation] respondsToSelector:#selector(setRadiusFrame:)] ){
[[[self mapView] viewForAnnotation: annotation] setRadiusFrame:rect];
}
}
}
The Annotation object (LocationAnnotationView)is a subclass of the MKAnnotationView and it's setRadiusFrame looks like this
-(void) setRadiusFrame:(CGRect) rect{
CGPoint centerPoint;
//Invert
centerPoint.x = (rect.size.width/2) * -1;
centerPoint.y = 0 + 55 + ((rect.size.height/2) * -1);
rect.origin = centerPoint;
[self.radiusView setFrame:rect];
}
And finally the radiusView object is a subclass of a UIView, that overrides the drawRect method to draw the translucent circles. setFrame is also over ridden in this UIView subclass, but it only serves to call [UIView setNeedsDisplay] in addition to [UIView setFrame:] to ensure that the view is redrawn after the frame has been updated.
The radiusView object's (CircleView) drawRect method looks like this
-(void) drawRect:(CGRect)rect{
//NSLog(#"[CircleView drawRect]");
[self setBackgroundColor:[UIColor clearColor]];
//Declarations
CGContextRef context;
CGMutablePathRef path;
//Assignments
context = UIGraphicsGetCurrentContext();
path = CGPathCreateMutable();
//Alter the rect so the circle isn't cliped
//Calculate the biggest size circle
if( rect.size.height > rect.size.width ){
rect.size.height = rect.size.width;
}
else if( rect.size.height < rect.size.width ){
rect.size.width = rect.size.height;
}
rect.size.height -= 4;
rect.size.width -= 4;
rect.origin.x += 2;
rect.origin.y += 2;
//Create paths
CGPathAddEllipseInRect(path, NULL, rect );
//Create colors
[[self areaColor] setFill];
CGContextAddPath( context, path);
CGContextFillPath( context );
[[self borderColor] setStroke];
CGContextSetLineWidth( context, 2.0f );
CGContextSetLineCap(context, kCGLineCapSquare);
CGContextAddPath(context, path );
CGContextStrokePath( context );
CGPathRelease( path );
//CGContextRestoreGState( context );
}
Thanks for bearing with me, any help is appreciated.
Jonathan
First, what's foo used in the first function? And I'm assuming radiusView's parent is the annotation view, right?
The "Jittering"
Also, the center point of radiusView should coincide with that of the annotationView. This should solve your problem:
-(void) setRadiusFrame:(CGRect)rect{
rect.origin.x -= 0.5*(self.frame.size.width - rect.size.width);
rect.origin.y -= 0.5*(self.frame.size.height - rect.size.height);
[self.radiusView setFrame:rect]
}
Unnecessary method
You could set the frame directly on the radiusView and avoid the above calculation:
UIView * radiusView = [[[self mapView] viewForAnnotation: annotation] radiusView];
rect = [[self mapView] convertRegion:foo toRectToView: radiusView.superView];
[radiusView setFrame:rect];
When drawing the ellipse, don't use the rect passed to drawRect:, it doesn't have to be the same as the frame. It's safer to directly use self.frame
Unnecessary view
Now I gave the above points if you need to use the above hierarchy, but I don't see why don't you just draw your ellipses directly in the LocationAnnotationView? It's there for this purpose after all. This is how you do this:
when scaling, change the annotationView's rect directly:
rect = [[self mapView] convertRegion:foo toRectToView: self.mapView];
[[[self mapView] viewForAnnotation: annotation] setFrame:rect];
Move the implementation of drawRect: to LocationAnnotationView.
This is easier to implement, and should address your problem as the center point of the annotation view moves with your pin and you shouldn't see this problem.
Fixes
There are two other issues in the code:
Set longitudeDelta like this:
region.span.longitudeDelta = 0.002*cos(region.center.latitude*pi/180.0);
as the longitude delta converted to meters changes with the latitude. Alternatively, you could only set latitude delta, then modify the rect so it becomes rectangular (width==height).
in drawRect:, don't use the passed rect; instead use self.frame. There's no guarantee that these are the same, and rect could have any value.
Let me know if these work ;-)