Single shadow for multiple objects in Swift? - swift

I want to draw a dialog bubble so I need 2 objects:
arrow (can be drawn with UIBezierPath only)
rounded rect (can be drawn with UIBezierPath or UIView+corner radius)
Example of code generated by paintcode:
//// Rectangle Drawing
let rectanglePath = UIBezierPath(roundedRect: CGRect(x: 68, y: 38, width: 101, height: 38), cornerRadius: 5)
UIColor.gray.setFill()
rectanglePath.fill()
//// Polygon Drawing
let polygonPath = UIBezierPath()
polygonPath.move(to: CGPoint(x: 118, y: 25.5))
polygonPath.addLine(to: CGPoint(x: 125.36, y: 38.25))
polygonPath.addLine(to: CGPoint(x: 110.64, y: 38.25))
polygonPath.close()
UIColor.gray.setFill()
polygonPath.fill()
The problem is if I add shadow to this code then it is applied twice and may overlap the objects.
I could replace 2 objects with one but the complexity of code is increased significantly (drawing rect and 3 points will be replaced with ~50 points including "control points").
So is it possible to join rectanglePath and polygonPath to draw single shadow somehow without this complexity?

Related

Cropping An Image (CIImage) To A UIBezierPath

I have seen this question asked in varying ways on Stack Overflow, but I have not seen an implementation that leverages anything but UIKit to develop an approach. For my use case, I have a CIImage and UIBezierPath (rather, a set of points from a machine learning feature detection model) that I would like to use a "mask" for the CIImage.
For example, I may have an image of a person's face. Subsequently, I may have a UIBezierPath that creates a closed shape around one of their eyes. Ideally, I would like to "crop" my CIImage to encompass the UIBezierPath. I recognize that since the final image will need to be square/rectangle, I would like to have any area outside of the UIBezierPath either black or transparent so it can be used as a mask for a future composition.
At present, I have code set up as such;
func doCrop(image: CIImage) -> CIImage {
// The drawing context (here to show I need to use CIContext for another use later on in this function.
let ciContext = CIContext()
// The original source image
let source = image
// Setup the path
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 400, y: 0))
path.addLine(to: CGPoint(x: 600, y: 200))
path.addLine(to: CGPoint(x: 400, y: 400))
path.addLine(to: CGPoint(x: 200, y: 400))
path.addLine(to: CGPoint(x: 100, y: 200))
path.close()
// Test
let cropped = source.cropped(to: path.bounds)
}
To be honest, I'm really unsure where to go from here. From what I have ascertained online, I see solutions that either implement UIKit to create an image context and draw a path, or solutions that only mask a UIImageView (which is not relevant for my needs, as I will need to draw this into a CIContext and pass through a Metal shader at a later point).

Is it possible to create an instance of a particular location on the screen (using auto layout)?

I have 4 playing cards on the screen. At the press of a button, I want one of the cards at random to move to the middle of the top half of the screen. Is it possible to create an instance of a constraint (eg: centerXAnchor with constant 0, and centerYAnchor with constant -200) so that I can use CGAffineTransform and move the random image to this point?
Ive tried creating an instance of a CGRect Frame:
let destination = CGPoint(x: 10, y: 10)
but this does not move evenly across devices.
An affine transformation matrix is used to rotate, scale, translate, or skew the objects you draw in a graphics context.
I don't think CGAffineTransform is the ideal thing to use for this task. You aren't doing any the above things (rotate, scale, translate, or skew).
I think you would likely be best using UIView.animateWithDuration
let cardSize = CGSize(width: 100, height: 100)
let card = UIView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: cardSize))
UIView.animate(withDuration: 1.0) {
card.frame = CGRect(origin: CGPoint(x: 100, y: 100), size: cardSize)
}

UIBezierPath - draw multiple rects for a single path

I can draw a single rect with :
let roundedRect = UIBezierPath(roundedRect: rect, cornerRadius: 50)
with this I can not add a new rect to the same path. (I need multiple rects to a single layer)
I can also just draw a rect by moving the point :
path.move(to: CGPoint(x:point1.x-rectWidth/2.0,y:point1.y) )
path.addLine(to: CGPoint(x: point1.x-rectWidth/2.0, y: point2.y))
path.addLine(to: CGPoint(x: point1.x+rectWidth/2.0, y: point2.y))
path.addLine(to: CGPoint(x: point1.x+rectWidth/2.0, y: point1.y))
path.addLine(to: CGPoint(x:point1.x-rectWidth/2.0,y:point1.y))
which will not be a rounded rect.
Which approach can I use to get a rounded rect ?
Building a rounded rect yourself is somewhat tricky. I suggest creating rounded rect paths using the UIBezierPath initializer init(roundedRect:cornerRadius:)
You can use append(_:) to append rounded rect paths to another path:
var path = UIBezierPath() //Create an empty path
//Add a rounded rect to the path
path.append(UIBezierPath(roundedRect: rect1, cornerRadius: radius1))
//Add another rounded rect to the path
path.append(UIBezierPath(roundedRect: rect2, cornerRadius: radius2))
//Lather, rinse, repeat.
path.append(UIBezierPath(roundedRect: rect3, cornerRadius: radius3))
Edit:
To animate all of your rectangles drawing from the bottom, like an ink-jet printer, create a CAShapeLayer that's the same size as your view, install a zero-height rectangle at the bottom, and make that layer the mask layer for your view's layer. Then create a CABasicAnimation of the rectangle growing to the full height of the view.

UIBezierPath Quadratic Curve is a Straight Line

I'm trying to create a curved arrow for displaying in ab ARKit scene, however, the curvature of the arrow staff is just rendering as a straight line on both sides.
func createTurnArrow(_ direction: Direction) -> SCNShape {
let path = UIBezierPath()
path.move(to: CGPoint(x: 0.2, y: 0)) // A
path.addLine(to: CGPoint(x: 0, y: 0.2)) // B
path.addLine(to: CGPoint(x: 0, y: 0.1)) // C
path.addQuadCurve(to: CGPoint(x: -0.3, y: -0.3), controlPoint: CGPoint(x: -0.3, y: 0.1)) // Curve 1
path.addLine(to: CGPoint(x: -0.1, y: -0.3)) // D
path.addQuadCurve(to: CGPoint(x: 0, y: -0.1), controlPoint: CGPoint(x: -0.1, y: -0.1)) // Curve 2
path.addLine(to: CGPoint(x: 0, y: -0.2)) // E
path.close()
return direction == .left ?
SCNShape(path: path.reversing(), extrusionDepth: self.defaultDepth) :
SCNShape(path: path, extrusionDepth: self.defaultDepth)
}
My intuition tells me that create a node with this function:
SCNNode(geometry: createTurnArrow(.right))
should produce a shape like this:
but instead renders this without any curves to the tail of the arrow:
I've tried a bunch of other math to get the current control points for the quadratic curves but nothing is worry. Any ideas?
EDIT:
Where is the schematic with plotted points and my assumption of how this should be rendered with the curves.
Read the SCNShape path documentation. It says this:
The path’s flatness (see flatness in NSBezierPath) determines the level of detail SceneKit uses in building a three-dimensional shape from the path—a larger flatness value results in fewer polygons to render, increasing performance.
(Since you're on iOS, substitute UIBezierPath for NSBezierPath.)
What is the default flatness of a UIBezierPath? Here's what the documentation says:
The flatness value measures the largest permissible distance (measured in pixels) between a point on the true curve and a point on the rendered curve. Smaller values result in smoother curves but require more computation time. Larger values result in more jagged curves but are rendered much faster. The default flatness value is 0.6.
Now compare the default flatness (0.6) to the overall size of your shape (0.5 × 0.5). Notice that the flatness is bigger than the size of your shape! So each of your curves is getting flattened to a single straight line.
Change the flatness of your path to something more appropriate for your shape, or change the scale of your shape to something more appropriate for the default flatness.
let path = UIBezierPath()
path.flatness = 0.05 // <----------------------- insert this statement
path.move(to: CGPoint(x: 0.2, y: 0)) // A
path.addLine(to: CGPoint(x: 0, y: 0.2)) // B
// etc.

What is the difference between these two statements in SpriteKit?

this part is a SKShapeNode
var rect = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 30, height: 30))
self.path = CGPathCreateWithRect(rect, nil)
AND
let rect = SKShapeNode(rectOfSize: CGSize(width: 30, height: 30))
They seems to be acting differently in the simulator and I'm wondering if these are different in any way.
The origin, (0, 0) point, of a SKShapeNode is the center of the node and its path is drawn relative to that point. Therefore, a shape node created with the following CGRect
CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 30, height: 30))
will be drawn with its bottom-left corner in the center of the shape node.
The convenience method SKShapeNode(rectOfSize...) centers the rect in the shape node by offsetting the origin argument of the CGRect by half of the rectangle's width and height. In this case, the rect's origin is (-15, -15).
You can create a CGRect that is centered in an SKShapeNode by offsetting its origin:
let height = CGFloat(30)
let width = CGFloat(30)
let anchorPoint = CGPoint(x:0.5, y:0.5)
CGRect(origin: CGPoint(x: -width*anchorPoint.x, y: -height*anchorPoint.y), size: CGSize(width: width, height: height))
where anchorPoint defines the relative "point in the sprite that corresponds to the node’s position."
You can also use SKShapeNode(path:path, centered:true) which will center the path using its bounding box.