In SceneKit, putting a text label onto SCNGeometry or SCNBox - swift

I'm trying to put a simple text label onto one (hopefully all types) of built in SCNGeometry shapes as they move across the screen. The closest I have come to success is adding a CALayer with CATextLayer to a SCNBox via .firstMaterial.diffuse.contents, as described in this thread.
BUT, the text is never readable. With a SCNBox of height 1.0: when the size of the layer.frame and textLayer.fontSize is 1.0, the text does not appear; as the frame and font size increase (not the box) the text appears blotchy, like in the image below; and when very large, the text appears as squiggly lines.
The following code is part of the method that spawns shapes:
var geometry:SCNGeometry
let layer = CALayer()
layer.frame = CGRect(x: 0, y: 0, width: 4, height: 4)
layer.backgroundColor = UIColor.white.cgColor
var textLayer = CATextLayer()
textLayer.frame = layer.bounds
textLayer.fontSize = layer.bounds.size.height
textLayer.string = "Matilda"
textLayer.alignmentMode = kCAAlignmentLeft
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.display()
layer.addSublayer(textLayer)
let geometry = SCNBox(width: 1.0,
height: 1.0,
length: 3.0,
chamferRadius: 0.0)
geometry.firstMaterial?.locksAmbientWithDiffuse = true
geometry.firstMaterial?.diffuse.contents = layer
let geometryNode = SCNNode(geometry: geometry)
geometryNode.position = SCNVector3(x: 0.0, y: 0.0, z: 0.0)
scnScene.rootNode.addChildNode(geometryNode)

As pointed out by David R., the units of the box size is not the same as the units of the frame size.
It worked after adjusting the frame to (and optimising SCNBox size):
layer.frame = CGRect(x: 0, y: 0, width: 200, height: 50)

Related

Add shadow to SCNPlane

I have ARKit artwork app. And I want my artworks to have a shadow just behind it. The shadow mustn't be dynamic. Just static shadow.
I've tried to add a plane behind the artwork. And set a shadow to it. But I discovered that it is not easy.
Can you help me?
let shadow = SCNPlane(width: artwork.size.width * 0.0254 + 0.03, height: artwork.size.height * 0.0254 + 0.015)
let layer = CALayer()
layer.frame = CGRect(origin: .zero, size: CGSize(width: shadow.width, height: shadow.height))
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 1
layer.shadowOffset = .zero
layer.shadowRadius = 10
shadow.firstMaterial?.diffuse.contents = layer
The shadow should be like in UIKit's UIView. But this code just doesn't work.
I need something like
UPDATED.
In order to cast and receive a shadows from 3D lights, you have to use a 3D primitives (like cube, sphere, cylinder, etc), or 3D models in such formats as .usdz, .dae or .obj.
Shadow on a visible plane.
Use the following code to test how light generates shadows for 3D geometry:
let scene = SCNScene()
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = .spot
lightNode.light!.castsShadow = true
lightNode.light!.shadowMode = .deferred
lightNode.rotation = SCNVector4(x: -1, y: 0, z: 0, w: CGFloat.pi/2)
lightNode.position = SCNVector3(x: 0, y: 20, z: 0)
scene.rootNode.addChildNode(lightNode)
let sphereNode = SCNNode()
sphereNode.geometry = SCNSphere(radius: 2)
sphereNode.position = SCNVector3(x: 0, y: -2, z: 0)
scene.rootNode.addChildNode(sphereNode)
let planeNode = SCNNode()
planeNode.geometry = SCNPlane(width: 15, height: 15)
planeNode.position = SCNVector3(x: 0, y: -5, z: 0)
planeNode.rotation = SCNVector4(x: -1, y: 0, z: 0, w: CGFloat.pi/2)
planeNode.geometry?.materials.first?.diffuse.contents = UIColor.red
scene.rootNode.addChildNode(planeNode)
Shadow on invisible plane.
In case you need an invisible plane that receives a shadow, use the following code:
planeNode.geometry?.materials.first?.colorBufferWriteMask = []
planeNode.geometry?.materials.first?.writesToDepthBuffer = true
planeNode.geometry?.materials.first?.lightingModel = .constant
Fake shadow.
And if you want to use a fake shadow written as a .png (png file format can hold 4 channels) texture on geometry (with a premultiplied alpha channel – RGB x A), use the following approach:
planeNode.geometry?.materials.first?.diffuse.contents = UIImage(named: "shadow.png")
lightNode.light!.castsShadow = false
Here's my answer on fake shadows.
And here's your premultiplied shadow in .png format (just drag-and-drop it on your desktop):
You can change its size and transparency and, of course, you can blur it.

LottieAnimationView size won't change/is too small (iOS/Swift)

Whether the view I'm creating is a LOTAnimatedSwitch or View, the image of the animation always appears very small. The lottie animation doesn't take up the size of the view that I create. Is this an issue with downloading the animation from LottieFiles? The dimensions of the file are 600x600 pixels. I'm using Lottie version 2.5.0 and Swift 4. For example:
let animatedSwitch = LOTAnimatedSwitch(named: "toggle_switch")
animatedSwitch.frame.origin = CGPoint(x: 8, y: separatorLineView.frame.height + separatorLineView.frame.origin.y + 8)
animatedSwitch.frame.size = CGSize(width: dialogViewWidth - 16, height: 40)
animatedSwitch.setProgressRangeForOnState(fromProgress: 0.5, toProgress: 1)
animatedSwitch.setProgressRangeForOffState(fromProgress: 0, toProgress: 0.5)
animatedSwitch.contentMode = .scaleAspectFill
animatedSwitch.clipsToBounds = true
animatedSwitch.backgroundColor = .purple
The problem was with the file I downloaded from LottieFiles. To fix the animation/icon from being small, I scaled the composition size in adobe after effects to fit the preview frame. I exported the .aeb file to .json using the bodymovin plugin.
Hardik's answer was also helpful. The problem was simply that the file I downloaded had a lot of empty space around the actual icon until I scaled the picture up.
Try this code i am not sure this will help in your case
let animatedSwitch = LOTAnimatedSwitch(named: "toggle_switch")
animatedSwitch.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
animatedSwitch.center = self.view.center
animatedSwitch.setProgressRangeForOnState(fromProgress: 0.5, toProgress: 1)
animatedSwitch.setProgressRangeForOffState(fromProgress: 0, toProgress: 0.5)
animatedSwitch.contentMode = .scaleAspectFit
self.view.addSubview(animatedSwitch)
self.view.backgroundColor = UIColor.lightGray
animationView = .init(name: "lf30_editor_fip4qqkq")
animationView!.frame = CGRect(x: 0, y: 0, width: 150, height: 150)
animationView!.center = self.view.center
animationView!.contentMode = .scaleAspectFit
animationView!.loopMode = .loop
animationView!.animationSpeed = 1.0
view.addSubview(animationView!)
animationView!.play()
I had this issue too. I modified the animation view's width and height to my desired size and changed the content mode to scale aspect fill. If you wanted to make the animation larger, just update your width and height. Here's example code.
animationView.animation = Animation.named("loading")
animationView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
animationView.contentMode = .scaleAspectFill
animationView.loopMode = .loop
animationView.play()

Draw a circular transparent hole into UIView for given rect

I am trying to create a circular transparent hole into my UIview with the following code, and this code makes the correct hole based on given rect. In my situation I have few rects which are individual and few of them are overlapped,for all those rect which are getting overlapped and not getting transparent holes over intersection rect area because of fill rule kCAFillRuleEvenOdd.How can I modify the same code so it can work for individual rects and all other overlapped rect and create transparent holes into view?
let overlay = UIView(frame: CGRect(x: 0, y: 0, width: width,height: height))
overlay.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.4)
// Create the path.
let path = UIBezierPath(rect: view.frame)
let maskLayer = CAShapeLayer()
maskLayer.frame = view.frame
for rectFrame in rects {
// Create the frame for the circle.
let rect = CGRect(x: rectFrame.origin.x, y: rectFrame.origin.y, width: 38, height: 38)
// Append the circle to the path so that it is subtracted.
path.append(UIBezierPath(ovalIn: rect))
}
maskLayer.fillRule = kCAFillRuleEvenOdd
maskLayer.path = path.cgPath
// Set the mask of the view.
view.layer.mask = maskLayer
self.overlayLayer.addSubview(overlay)

Rounded rectangle with SKShapeNode

I have the following code that rounds an already existing rectangle from the scene builder...
btOk.path = UIBezierPath(roundedRect: CGRect(x: -62.5,y:-25,width: 125, height: 50), cornerRadius: 10).cgPath
However, if i try to use the same init to round the corners of another rectangle that is much larger, it does not even come close to working. It just makes the width HUUUUGE (imagine Trump).
scene.enumerateChildNodes(withName: "//*"){
node,stop in
if let name = node.name{
if name.contains("round"){
if let shapeNode = node as? SKShapeNode{
print(shapeNode.frame.width) //500
shapeNode.path = UIBezierPath(roundedRect: CGRect(x: -250,y:-100,width: 500, height: 200), cornerRadius: 50).cgPath
print(shapeNode.frame.width) //5735
}
}
}
}
btOk is a SKShapeNode as well. What am i missing between the two that is so different? One thing to note is i am enumerating through the children of the scene like this because this scene is in a SKReferenceNode. Perhaps that has something to do with it?
EDIT
Taking direction from #Ron Myschuk, i've solved this and since it's such a PITA, i also created an extension. So now i can round the corners of any SKShapeNode very easily when needed. Even if it was created in the scene editor. Note, this should only be used if there are no children of the shape node. Otherwise those children will be removed also.
extension SKShapeNode {
func roundCorners(topLeft:Bool,topRight:Bool,bottomLeft:Bool,bottomRight:Bool,radius: CGFloat,parent: SKNode){
let newNode = SKShapeNode(rect: self.frame)
newNode.fillColor = self.fillColor
newNode.lineWidth = self.lineWidth
newNode.position = self.position
newNode.name = self.name
self.removeFromParent()
parent.addChild(newNode)
var corners = UIRectCorner()
if topLeft { corners = corners.union(.bottomLeft) }
if topRight { corners = corners.union(.bottomRight) }
if bottomLeft { corners = corners.union(.topLeft) }
if bottomRight { corners = corners.union(.topRight) }
newNode.path = UIBezierPath(roundedRect: CGRect(x: -(newNode.frame.width / 2),y:-(newNode.frame.height / 2),width: newNode.frame.width, height: newNode.frame.height),byRoundingCorners: corners, cornerRadii: CGSize(width:radius,height:radius)).cgPath
}
}
And use it like so...
aShapeNode.roundCorners(topLeft: true, topRight: true, bottomLeft: false, bottomRight: false, radius: 5,parent: popup)
Not what you're going to want to hear but it's because you cannot set the width of an SKShapeNode in the Scene editor (To my knowledge). In order to get that ShapeNode to have a width of 500 you would have had to adjust the xScale. The xScale then reapplies itself to the path when you adjust it (kind of growing exponentially). If you create the SKShapeNode in code there is no problem adjust the rounded corners
let round = SKShapeNode(rect: CGRect(x: 0, y: -150, width: 500, height: 200))
round.fillColor = .red
addChild(round)
print(round.frame.width)
round.path = UIBezierPath(roundedRect: CGRect(x: -250, y: -100, width: 500, height: 200), cornerRadius: 50).cgPath
print(round.frame.width)
Edit
If you have your heart set on using the Scene editor you can place your ShapeNode and stretch it to where you want it then you could just do a small conversion in code to get the results that you want
if let round = self.childNode(withName: "biground") as? SKShapeNode {
let biground = SKShapeNode(rect: round.frame)
biground.fillColor = round.fillColor
biground.position = round.position
addChild(biground)
round.removeFromParent()
print(biground.frame.width)
biground.path = UIBezierPath(roundedRect: CGRect(x: -250, y: -100, width: 500, height: 200), cornerRadius: 50).cgPath
print(biground.frame.width)
}
this just recreates the shape in code based on what you outlined in the Scene editor and rounds the edges perfectly
edit 2
I've always been under the impression that SKShapeNodes are really inefficient (i'm pretty sure they leaked memory as well). So i always setup my round rectangles as so.
let outsideTexture = SKTexture(imageNamed: "round_square_white")
let outside = SKSpriteNode(texture: outsideTexture)
let insetX: CGFloat = 20
let insetY: CGFloat = 20
let cellSize = CGSize(width: 500.0, height: 500.0)
outside.centerRect = CGRect(x: CGFloat(insetX / outside.size.width), y: CGFloat(insetY / outside.size.height), width: CGFloat((outside.size.width - insetX * 2) / outside.size.width), height: CGFloat((outside.size.height - insetY * 2) / outside.size.height))
//outside.position = CGPointMake(gameModel.gameWidth / 2, (outside.size.height) / 2);
outside.xScale = cellSize.width / outside.size.width
outside.yScale = cellSize.height / outside.size.height
outside.zPosition = 10
outside.position = CGPoint(x: CGFloat(0), y: CGFloat(-0.35 * self.size.height / 2))
self.addChild(outside)
worth noting that this lays out a rounded square/rectangle perfectly however similar to the scale issues from the scene editor you have to place an empty cell over this to add children to, otherwise they scale to the rounded square.

Adding horizontal gradient to borderline

I created simple borderline into my view. However I am struggling giving horizontal gradient background color to it. How should I do it?
I tried like this but it just sets the color to white:
let border = CAGradientLayer()
border.frame = CGRect(x: 0, y: self.mainView.frame.height - 2, width: self.mainView.frame.width, height: 2)
border.backgroundColor = UIColor.gray.cgColor
let color1 = UIColor.black.withAlphaComponent(0.1).cgColor as CGColor
let color2 = UIColor.white.withAlphaComponent(0.9).cgColor as CGColor
border.locations = [0.60, 1.0]// Are these right coordinates?
border.colors = [color2, color1]
I got it working like this:
let gradientLayer = CAGradientLayer()
gradientLayer.frame = CGRect(x: 0, y: self.mainView.frame.height - 2, width: self.mainView.frame.width, height: 2)
gradientLayer.colors = [UIColor.green,UIColor.blue ].map{$0.cgColor}
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
Instead of using CALayer() I had to use CAGradientLayer()