SceneKit CALayer unsharp drawing - ios

I'm trying to draw UIBezierPaths and display them on a SCNPlane, though I'm getting a very unsharp result (See in the image) How could I make it sharper?
let displayLayer = CAShapeLayer()
displayLayer.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
displayLayer.path = path.cgPath
displayLayer.fillColor = UIColor.clear.cgColor
displayLayer.strokeColor = stroke.cgColor
displayLayer.lineWidth = lineWidth
let plane = SCNPlane(width:size.width, height: size.height)
let material = SCNMaterial()
material.diffuse.contents = displayLayer
plane.materials = [material]
let node = SCNNode(geometry: plane)
I've tried to increase the displayLayer.frame's size but it only made things smaller.
Thanks for your answer!
Andras

This has been driving me crazy, so hopefully this answer helps someone else who stumbles across this. I'm mapping a CALayer to a SCNMaterial texture for use in an ARKit scene, and everything was blurry/pixelated. Setting the contentsScale property of the CALayer to UIScreen.main.scale didn't do anything, nor playing with shouldRasterize and rasterizationScale.
The solution is to set the layer's frame to the desired frame, multiplied by the screen's scale, otherwise the material is scale transforming the layer to fit the texture. In other words, the layer's size needs to be 3x its desired size.
let layer = CALayer()
layer.frame = CGRect(x: 0, y: 0, width: 500 * UIScreen.main.scale, height: 750 * UIScreen.main.scale)

Try to change UIBezierPath "flatness" property (0.1, 0.01, 0.001 etc).

Related

How do I make everything outside of a CAShapeLayer black with an opacity of 50% with Swift?

I have the following code which draws a shape:
let screenSize: CGRect = UIScreen.main.bounds
let cardLayer = CAShapeLayer()
let cardWidth = 350.0
let cardHeight = 225.0
let cardXlocation = (Double(screenSize.width) - cardWidth) / 2
let cardYlocation = (Double(screenSize.height) / 2) - (cardHeight / 2) - (Double(screenSize.height) * 0.05)
cardLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: cardWidth, height: 225.0), cornerRadius: 10.0).cgPath
cardLayer.position = CGPoint(x: cardXlocation, y: cardYlocation)
cardLayer.strokeColor = UIColor.white.cgColor
cardLayer.fillColor = UIColor.clear.cgColor
cardLayer.lineWidth = 4.0
self.previewLayer.insertSublayer(cardLayer, above: self.previewLayer)
I want everything outside of the shape to be black with an opacity of 50%. That way you can see the camera view still behind it, but it's dimmed, except where then shape is.
I tried adding a mask to previewLayer.mask but that didn't give me the effect I was looking for.
Your impulse to use a mask is correct, but let's think about what needs to be masked. You are doing three things:
Dimming the whole thing. Let's call that the dimming layer. It needs a dark semi-transparent background.
Drawing the white rounded rect. That's the shape layer.
Making a hole in the entire thing. That's the mask.
Now, the first two layers can be the same layer. That leaves only the mask. This is not trivial to construct: a mask affects its owner in terms entirely of its transparency, so we need a mask that is opaque except for an area shaped like the shape of the shape layer, which needs to be clear. To get that, we start with the shape and clip to that shape as we fill the mask — or we can clip to that shape as we erase the mask, which is the approach I prefer.
In addition, your code has some major flaws, the most important of which is that your shape layer has no size. Without a size, there is nothing to mask.
So here, with corrections and additions, is your code; I made this the entirety of a view controller, for testing purposes, and what I'm covering is the entire view controller's view rather than a particular subview or sublayer:
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .red
}
private var didInitialLayout = false
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if didInitialLayout {
return
}
didInitialLayout = true
let screenSize = UIScreen.main.bounds
let cardLayer = CAShapeLayer()
cardLayer.frame = self.view.bounds
self.view.layer.addSublayer(cardLayer)
let cardWidth = 350.0 as CGFloat
let cardHeight = 225.0 as CGFloat
let cardXlocation = (screenSize.width - cardWidth) / 2
let cardYlocation = (screenSize.height / 2) - (cardHeight / 2) - (screenSize.height * 0.05)
let path = UIBezierPath(roundedRect: CGRect(
x: cardXlocation, y: cardYlocation, width: cardWidth, height: cardHeight),
cornerRadius: 10.0)
cardLayer.path = path.cgPath
cardLayer.strokeColor = UIColor.white.cgColor
cardLayer.lineWidth = 8.0
cardLayer.backgroundColor = UIColor.black.withAlphaComponent(0.5).cgColor
let mask = CALayer()
mask.frame = cardLayer.bounds
cardLayer.mask = mask
let r = UIGraphicsImageRenderer(size: mask.bounds.size)
let im = r.image { ctx in
UIColor.black.setFill()
ctx.fill(mask.bounds)
path.addClip()
ctx.cgContext.clear(mask.bounds)
}
mask.contents = im.cgImage
}
And here's what we get. I didn't have a preview layer but the background is red, and as you see, the red shows through inside the white shape, which is just the effect you are looking for.
The shape layer can only affect what it covers, not the space it doesn't cover. Make a path that covers the entire video and has a hole in it where the card should be.
let areaToDarken = previewLayer.bounds // assumes origin at 0, 0
let areaToLeaveClear = areaToDarken.insetBy(dx: 50, dy: 200)
let shapeLayer = CAShapeLayer()
let path = CGPathCreateMutable()
path.addRect(areaToDarken, ...)
path.addRoundedRect(areaToLeaveClear, ...)
cardLayer.frame = previewLayer.bounds // use frame if shapeLayer is sibling
cardLayer.path = path
cardLayer.fillRule = .evenOdd // allow holes
cardLayer.fillColor = black, 50% opacity

Add camera layer on irregular shape images IOS

I need to add the camera layer on any irregular shaped image i.e. lets say i have a image which is having irregular shape and inside image there is a circular or any other irregular shape in which i want to embed the live camera.
Any idea how i can achieve this functionality?
You can use UIBezierPath to draw irregular share for a mask CAShapeLayer
let size = 200.0
Create a CAShapeLayer and draw shape in which you wanna embed a cameraPreviewLayer.
let maskLayer = CAShapeLayer()
let maskPath = UIBezierPath()
maskPath.move(to: .zero)
maskPath.addLine(to: CGPoint(x: 10, y: -size))
maskPath.addLine(to: CGPoint(x: size/2, y: -size))
maskPath.addLine(to: CGPoint(x: size*2, y: size))
maskPath.close()
maskLayer.anchorPoint = .zero
Set the mask positon
maskLayer.position = CGPoint(x: 100, y: 400)
maskLayer.path = maskPath.cgPath
self.yourVideoPreviewLayer.mask = maskLayer
self.yourVideoPreviewLayer.masksToBounds = true
Or you can make an image with a shape in which you wanna embed a cameraPreviewLayer. Or if your image's inner shape have an alpha value = 0 you can reverse alpha of your original image and use it as a mask.
let maskLayer = CAShapeLayer()
maskLayer.anchorPoint = .zero
maskLayer.frame = videoPreviewLayer.bounds
maskLayer.contents = YourReversedImage.cgImage
self.videoPreviewLayer.mask = maskLayer
self.videoPreviewLayer.masksToBounds = true
Add additional UIView upon of your UIImageView with same frames(width, height and position). it shouldn't be a subview of UIImageView!
Set background of this UIView to clearColor and create whatever layer you want.
Now you can use this layer as AVCaptureVideoPreviewLayer instead of using UIImageView layers

Why does calculateAccumulatedFrame give an unexpected CGRect?

I'm trying to determine the right scaling factor for my node tree to make it fit exactly in my presentation rectangle, so I'm trying to find the smallest bounding rectangle around all my nodes. Apple's docs say that calculateAccumulatedFrame "Calculates a rectangle in the parent’s coordinate system that contains the content of the node and all of its descendants." That sounds like what I need, but it's not giving me the tight fit that I expect. My complete playground code is:
import SpriteKit
import PlaygroundSupport
let view:SKView = SKView(frame: CGRect(x: 0, y: 0, width: 800, height: 800))
PlaygroundPage.current.liveView = view
let scene:SKScene = SKScene(size: CGSize(width: 1000, height: 800))
scene.scaleMode = SKSceneScaleMode.aspectFill
view.presentScene(scene)
let yellowBox = SKSpriteNode(color: .yellow, size:CGSize(width: 300, height: 300))
yellowBox.position = CGPoint(x: 400, y: 500)
yellowBox.zRotation = CGFloat.pi / 10
scene.addChild(yellowBox)
let greenCircle = SKShapeNode(circleOfRadius: 100)
greenCircle.fillColor = .green
greenCircle.position = CGPoint(x: 300, y: 50)
greenCircle.frame
yellowBox.addChild(greenCircle)
let uberFrame = yellowBox.calculateAccumulatedFrame()
let blueBox = SKShapeNode(rect: uberFrame)
blueBox.strokeColor = .blue
blueBox.lineWidth = 2
scene.addChild(blueBox)
And the results are:
The left and bottom edges of the blue rectangle look good, but why are there gaps between the blue rectangle and the green circle on the top and right?
The notion "frame" does funny things when you add a transform. The bounding box around the box and circle is a rectangle. You have rotated that rectangle. Therefore its corners stick out. The accumulated frame embraces that rotated rectangle, including the sticking-out corners. It does not magically hug the drawn appearance of the nodes (e.g. the circle).

Combine overlapping UIBezierPaths to create a single mask

I'm looking to create a mask in my view that is shaped like several circles. Here blurView is the view I want to have cut outs in, and frames contains CGRect frame values for these circles/ovals.
let holes = UIBezierPath()
for f in frames {
let p = UIBezierPath(ovalInRect: f)
holes.appendPath(p)
}
let path = UIBezierPath(rect: blurView.bounds)
path.appendPath(holes)
path.usesEvenOddFillRule = true
let mask = CAShapeLayer()
mask.path = path.CGPath
mask.fillRule = kCAFillRuleEvenOdd
blurView.layer.mask = mask
This works quite well, except when circles overlap, the contents of the blurView show through instead of the view below it. How can I fix that?
Edit: Using Rob's answer, I got somewhere, but not quite where I want to be. The edges of the mask are ragged, they lack anti-aliasing:
Any way to fix that?
The basic idea is to fill a bitmap context with an opaque colour and then clear anything we want to clip. Grab an image of that bitmap context and use as a mask.
You can copy this into a playground if you like:
import UIKit
// A couple of overlapping circles to play with
let frames = [
CGRect(x: 100, y: 100, width: 200, height: 200),
CGRect(x: 50, y: 50, width: 200, height: 200)
]
let blurView = UIImageView(image: ...) // <== You can drag an image here. Literals are cool.
let size = blurView.bounds.size
let scale = UIScreen.mainScreen().scale
// First, create a bitmap context to work in.
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.PremultipliedFirst.rawValue | CGBitmapInfo.ByteOrder32Little.rawValue)
let ctx = CGBitmapContextCreate(nil, Int(size.width*scale), Int(size.height*scale),
8, 4 * Int(size.width * scale), CGColorSpaceCreateDeviceRGB(), bitmapInfo.rawValue)
CGContextScaleCTM(ctx, scale, scale)
// iOS draws upsidedown from Core Graphics, so you'll probably want to flip your context:
CGContextConcatCTM(ctx, CGAffineTransformMake(1, 0, 0, -1, 0, size.height))
// Everything we want to draw should be opaque
// Everything we want to clip should be translucent
CGContextSetGrayFillColor(ctx, 1, 1) // white
CGContextFillRect(ctx, blurView.bounds)
// And now we clear all our circles
CGContextSetBlendMode(ctx, .Clear)
CGContextSetGrayFillColor(ctx, 1, 0) // clear
for f in frames {
CGContextFillEllipseInRect(ctx, f)
}
// Make the masking image
let maskImage = CGBitmapContextCreateImage(ctx)
// For your playground amusement (you can quicklook this line)
UIImage(CGImage: maskImage)
// Create the masking layer
let mask = CALayer()
mask.frame = blurView.layer.frame
mask.contents = maskImage
// And apply it
blurView.layer.mask = mask
// And the final so you can quicklook it
blurView

CALayer does not respect zero shadowRadius while drawing?

It seems that CALayer's property shadowRadius is non-zero while drawing even if explicitelly set to zero when following conditions are met:
shadowPath property is also set on the same CALayer, and
shadowPath set is something else than plain rectangular path
To reproduce the issue I created 2 rounded CALayers with shadow that are identical in everything but shadow path - first one has shadowPath set to nil, second one has shadowPath set to its own shape. I would expect those 2 to render into exactly the same picture but they do not. Here is the result (magnified):
As you can see second rectangle obviously has some shadow radius higher then 0 even though it was set to 0. Here is code used to produce the picture above:
override func viewDidLoad() {
super.viewDidLoad()
let layerWithoutShadowPath = CALayer() //shadow of this rectangle will be drawn correctly
let layerWithShadowPath = CALayer() //shadow of this one will be drawn with radius higher then 0
layerWithoutShadowPath.frame = CGRect(x: 30, y: 30, width: 30, height: 30)
layerWithShadowPath.frame = CGRect(x: 70, y: 30, width: 30, height: 30)
layerWithShadowPath.shadowPath = UIBezierPath(roundedRect: layerWithShadowPath.bounds, cornerRadius: 4.0).CGPath
setupLayer(layerWithoutShadowPath)
setupLayer(layerWithShadowPath)
}
private func setupLayer(layer: CALayer) {
layer.backgroundColor = UIColor.yellowColor().colorWithAlphaComponent(1).CGColor
layer.cornerRadius = 4.0
layer.shadowColor = UIColor.blackColor().CGColor
layer.shadowOffset = CGSize(width: 0, height: 3)
layer.shadowRadius = 0 //SHADOW RADIUS IS SET TO 0 FOR BOTH RECTANGLES
layer.shadowOpacity = 1
view.layer.addSublayer(layer)
}
This might be a Cocoa bug or I am just missing something... Anyway, the question is: How can add rounded rectangle shadow with exactly zero radius to CALayer while shadowPath is set? (if you are wondering why I must set shadowPath the reason is performance)
Well, you might be able to fix it by setting the layer's edgeAntialiasingMask. However, it seems to me that you are needlessly trapped in a world of layer inefficiency - as if the only thing you knew how to do was make the layer draw itself with rounded corners and a shadow. What I would do is draw the yellow rounded square and its shadow as an actual drawing, and make that drawing the content of a layer:
let layer = CALayer()
layer.contentsScale = UIScreen.mainScreen().scale
layer.frame = CGRect(x: 30, y: 30, width: 30, height: 35)
UIGraphicsBeginImageContextWithOptions(layer.bounds.size, false, 0)
let con = UIGraphicsGetCurrentContext()
CGContextSetShadowWithColor(con, CGSizeMake(0,3), 1, UIColor.blackColor().CGColor)
let path = UIBezierPath(roundedRect: CGRectMake(0,0,30,30), cornerRadius: 4)
UIColor.yellowColor().setFill()
path.fill()
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
layer.contents = im.CGImage
view.layer.addSublayer(layer)
This has the advantage that the shadow is part of the drawing, and so there is no inefficiency - the very inefficiency you say you are trying to avoid - as there would be if you give the layer itself a shadow and thus compelled the render tree to do the work. Indeed, the same thing is true for the rounded rectangle: it is much more efficient to draw a rounded rectangle, as I am doing here, than to force the render tree to round the layer's corners for you, as you are doing.

Resources