How to add shadow to a group of CALayer using CATransform3D - objective-c

How can I add shadow to a group of CALayers?
I have a "FoldingView"-class which maintans several "SliceView"-classes. Each sliceview's layer will be givven a CATransform3D where I'm using the perspective property (.m34 = 1.0 / -1000).
How can I add a shadow with good visual logic? This is what I've been thinking so far:
I could get the path of each slice and combine these to get a shadow path.
I don't know how the get a path of CALayer when the layer is using CATransform3D
It might work visually, but I'm afraid it won't be totally right if the light is supposed to come from top left.
I could just apply standard CALayer shadow to all layers
It does not look good due to the shadow is overlapping each other
If anyone has any other suggestions or know how to code idea number 1 I'll be very happy! Here is a link to the sample project you see screenshot from.
Download zipped application with code

This seems to work as wanted. This is done per sliceview.
- (UIBezierPath *)shadowPath
{
if(self.progress == 0 && self.position != VGFoldSliceCenter)
{
UIBezierPath *path = [UIBezierPath bezierPath];
return path;
}
else
{
CGPoint topLeft = pointForAnchorPointInRect(CGPointMake(0, 0), self.bounds);
CGPoint topRight = pointForAnchorPointInRect(CGPointMake(1, 0), self.bounds);
CGPoint bottomLeft = pointForAnchorPointInRect(CGPointMake(0, 1), self.bounds);
CGPoint bottomRight = pointForAnchorPointInRect(CGPointMake(1, 1), self.bounds);
CGPoint topLeftTranslated = [self.superview convertPoint:topLeft fromView:self];
CGPoint topRightTranslated = [self.superview convertPoint:topRight fromView:self];
CGPoint bottomLeftTranslated = [self.superview convertPoint:bottomLeft fromView:self];
CGPoint bottomRightTranslated = [self.superview convertPoint:bottomRight fromView:self];
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:topLeftTranslated];
[path addLineToPoint:topRightTranslated];
[path addLineToPoint:bottomRightTranslated];
[path addLineToPoint:bottomLeftTranslated];
[path closePath];
return path;
}
}

Related

SKShapeNode.Get Radius?

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

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.

Make NSBezierPath to stay relative to the NSView on windowResize

In my program I'm drawing some shapes inside an NSView using NSBezierPath. Everything works and looks great except when I resize the window.
Example
Initial drawing
Window resize
Does anyone know what I should use to prevent this square from being anchored to the initial position, but make it readjust relative the the initial scale.
Any help is appreciated!
If you are doing your drawing in drawRect: then the answer is NO. You will need to rebuild and reposition your path each time. What you can do is something along the following lines:
- (void) drawRect:(NSRect)dirtyRect
{
// Assuming that _relPos with x and y having values bewteen 0 and 1.
// To keep the square in the middle of the view you would set _relPos to
// CGPointMake(0.5, 0.5).
CGRect bounds = self.bounds;
CGRect rect;
rect.size.width = 100;
rect.size.height = 100;
rect.origin.x = bounds.origin.x + bounds.size.width * _relPos.x - rect.size.width /2;
rect.origin.y = bounds.origin.y + bounds.size.height * _relPos.y - rect.size.height/2;
NSBezierPath *path = [NSBezierPath bezierPathWithRect:rect];
[[NSColor redColor] set];
path.lineWidth = 2;
[path stroke];
}

Strange behavior with UIBezierPath addClip

I'm trying to learn CoreGraphics and I have encountered a strange behavior.
I draw a rectangle and in it I draw given amount of diamonds,the shapes can be drawn with different filling (empty, filled and with stripes) my draw rect function looks like this:
- (void)drawRect:(CGRect)rect
{
UIBezierPath* roundedRect = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:9.0];
//don't draw where the round corners "cut" the rectangle
[roundedRect addClip];
//set a white background
[[UIColor whiteColor] setFill];
UIRectFill(self.bounds);
//set a black frame
[[UIColor darkGrayColor] setStroke];
[roundedRect stroke];
self.shade = STRIPED;
self.color = [UIColor greenColor];
self.number = 3;
rectOffset = self.bounds.size.width / (self.number * 2);
[self drawDiamondNumberOfTimes:self.number startOrigin:self.bounds.origin];
}
drawDiamondNumberOfTimes:startOrigin: is a recursive function that calculates the rectangle in which the shape will be drawing and draw the diamond boundaries using stroke
- (void) drawDiamondNumberOfTimes:(int) p_times startOrigin:(CGPoint) p_origin
{
if( p_times > 0)
{
CGRect drawArea;
drawArea.origin = CGPointMake(p_origin.x + rectOffset-shapeSize.width/2, self.bounds.size.height/4);
drawArea.size = shapeSize;
UIBezierPath *diamond = [[UIBezierPath alloc] init];
[diamond moveToPoint:CGPointMake(drawArea.origin.x, drawArea.origin.y+ shapeSize.height/2)];
[diamond addLineToPoint:CGPointMake(drawArea.origin.x+shapeSize.width/2, drawArea.origin.y)];
[diamond addLineToPoint:CGPointMake(drawArea.origin.x+shapeSize.width, drawArea.origin.y+ shapeSize.height/2)];
[diamond addLineToPoint:CGPointMake(drawArea.origin.x+shapeSize.width/2, drawArea.origin.y+ shapeSize.height)];
[diamond closePath];
[self.color setStroke];
[diamond stroke];
[self drawShadeOfDraw:diamond atRect:drawArea];
drawArea.origin.x += rectOffset + shapeSize.width/2;
[self drawDiamondNumberOfTimes:p_times-1 startOrigin:drawArea.origin ];
}
}
drawShadeOfDraw:atRect: set the different filling, where the strange behavior occurs.
With empty and solid fills it works perfect but with stripes, if I write [p_symbol addClip] then I get always one diamond striped even if self.number is set to 2 or 3. Without [p_symbol addClip] I get the correct number of diamonds but, of course, the stripes are all over the rectangle, here is the code for drawShadeOfDraw:atRect:
- (void)drawShadeOfDraw:(UIBezierPath*)p_symbol atRect:(CGRect)p_drawArea
{
switch (self.shade)
{
case STRIPED:
{
[p_symbol addClip];
for( int y = p_drawArea.origin.y; y < p_drawArea.origin.y+ p_drawArea.size.height; y+= 6)
{
[p_symbol moveToPoint:CGPointMake(p_drawArea.origin.x, y)];
[p_symbol addLineToPoint:CGPointMake(p_drawArea.origin.x+shapeSize.width, y)];
[self.color setStroke];
[p_symbol stroke];
}
break;
}
case SOLID:
{
[self.color setFill];
[p_symbol fill];
break;
}
default:
{
break;
}
}
}
Here are some images:
What am I doing wrong?
Adding to the clip is semi-permanent. Once the clipping area has been "reduced", you can't grow it again, per se. You can only restore it to a previous state. To do that you call CGContextSaveGState() before setting up context state (including clipping), do some drawing with that state, and then call CGContextRestoreGState() afterward to restore the context state to what it was before.
So, I suggest that you bracket your two methods with calls to save and restore the context state. Use UIGraphicsGetCurrentContext() to get a reference to the current context.

How to draw triangle using Core Graphics - Mac?

I want something like this: (light part, ignore background)
How can I draw this using Cocoa? In drawRect:? I don't know how to draw.
Use NSBezierPath:
- (void)drawRect:(NSRect)rect
{
NSBezierPath *path = [NSBezierPath bezierPath];
[path moveToPoint:NSMakePoint(0, 0)];
[path lineToPoint:NSMakePoint(50, 100)];
[path lineToPoint:NSMakePoint(100, 0)];
[path closePath];
[[NSColor redColor] set];
[path fill];
}
This should get you started, it draws a red triangle on a 100x100 sized view. You'd usually calculate the coordinates dynamically, based on the view's size, instead of using hard-coded values of course.
iOS + Swift
(1) Create a Swift extension
// Centered, equilateral triangle
extension UIBezierPath {
convenience init(equilateralSide: CGFloat, center: CGPoint) {
self.init()
let altitude = CGFloat(sqrt(3.0) / 2.0 * equilateralSide)
let heightToCenter = altitude / 3
moveToPoint(CGPoint(x:center.x, y:center.y - heightToCenter*2))
addLineToPoint(CGPoint(x:center.x + equilateralSide/2, y:center.y + heightToCenter))
addLineToPoint(CGPoint(x:center.x - equilateralSide/2, y:center.y + heightToCenter))
closePath()
}
}
(2) Override drawRect
override func drawRect(rect: CGRect) {
let path = UIBezierPath(
equilateralSide: self.bounds.size.width,
center: CGPoint(x: self.bounds.size.width/2, y: self.bounds.size.height/2))
self.tintColor.set()
path!.fill()
}
iOS + Swift5 (Updated Context handling, allow for diff widths)
let triangleWidth: CGFloat = 8.0
let heightToCenter: CGFloat = 4.0 // This controls how "narrow" the triangle is
let center: CGPoint = centerPoint
context.setFillColor(UIColor.red.cgColor)
context.move(to: CGPoint(x:center.x, y:center.y - heightToCenter*2))
context.addLine(to: CGPoint(x:center.x + triangleWidth/2, y:center.y + heightToCenter))
context.addLine(to: CGPoint(x:center.x - triangleWidth/2, y:center.y + heightToCenter))
context.closePath()
context.fillPath()