SCNText ignores NSAttributedString attributes - swift

Although the docs claim that SceneKit's SCNText works with NSAttributedString, the following NSAttributedString attributes seem to be ignored completely (and perhaps others, too):
shadow
strokeColor
strokeWidth
foregroundColor
SCNText does obey NSAttributedString's font attribute, however -- and it does display on screen.
What I'm trying to do is add a stroke to my SCNText. I do not want to use a SpriteKit overlay for such a simple use case.
From the docs:
When you create a text geometry from an attributed string, SceneKit styles the text according to the attributes in the string, and the properties of the SCNText object determine the default style for portions of the string that have no style attributes.
Here's what I'm doing in code. This code results in a white-filled text in the correct font, but without shadow or stroke of any kind:
let labelNode = SCNNode()
let label = SCNText(string: "\(targetLevel)", extrusionDepth: 0.0)
let font = UIFont(name: "Whatever", size: 4.0)
let shadow = NSShadow()
shadow.shadowBlurRadius = 1.0
shadow.shadowColor = UIColor.black
let attStr = NSAttributedString(string: "\(targetLevel)", attributes: [.font: font!, .shadow: shadow, .strokeColor: UIColor.black, .strokeWidth: 0.5])
label.string = attStr
label.alignmentMode = "kCAAlignmentCenter"
label.flatness = 0.01
//Using a big font and then scaling the node down to make it look less jagged:
labelNode.scale = SCNVector3(0.25,0.25,0.25)
labelNode.geometry = label
labelNode.castsShadow = false
parent.addChildNode(labelNode)
Questions: Am I doing something wrong -- or is this a limitation of SceneKit? Is there any other way to stroke an SCNText? I saw this question, but it's about fill color, not stroke, and didn't fix my problem.

As you've found (and others have noted), you don't automatically get all of the properties of the attributed string rendered in SceneKit. Think of them in terms of what you are drawing (e.g. the shape of the text), rather than how you draw them (e.g. color, shadow etc). All SceneKit uses is the geometry.
If you want outline-stroked 3D text then you may have to get the path from the font and use that for your geometry instead of SCNText. I wrote this category but the code is pretty ancient now, I have no idea if it still works, but it should be enough to get you there.
You could also perhaps render the text as an image and put it as a texture on a billboard, unless you actually want it to be part of the 3D scene. But by this point, a sprite kit overlay isn't looking that complicated, is it?

Related

NSMenuItem with attributedTitle containing an NSFont object draws the title with baseline shift

I'm tying to create an NSPopUpButton with the list of fonts available in the system. Seemed pretty obvious task but I've failed. I guess, I'm missing something so obvious that I've completely forgot about it.
The code is pretty straight:
let button = NSPopUpButton()
button.menu = NSMenu()
NSFontManager.shared.availableFonts.forEach { fontNameString in
let item = NSMenuItem()
let font = NSFont(name: fontNameString, size: 14)!
let attrs: [NSAttributedString.Key: Any] = [.font: font]
item.attributedTitle = NSAttributedString(string: fontNameString, attributes: attrs)
button.menu?.addItem(item)
}
But that just creates the NSMenu with items having shifted baselines. I've tried to calculate the baseline offset and add it as an attribute but I've failed. Haven't found an algorhythm to satisfy all the font available in the system.
Besides, adding the baseline offset resizes the corresponding NSMenuItem which does not have a fixed size, by the way - the height of an item is different on every font.
To analyse the situation I've added the .backgroundColor attribute and set it to red NSColor. And that brought even more confusing. It appears that some font somehow not drawing in bounds.
How can I center the attributed title vertically? Please, help!
Probably, that is NSAttributedString's issue.
To workaround, I created a custom view and drew a string in it with a trick.
Then set it to NSMenuItem.view.
Get more details, see my codes below.
https://github.com/bluedome/FontSelectionView/blob/main/FontSelectionView.swift
Hope it help, if you're still having the trouble...

Turning a UIBezierPath into a mask?

Not sure if I am asking this question correctly, but I have two components; a CIImage and a UIBezierPath. Ideally, I want to create a CGRect that encapsulates my UIBezierPath; everything inside of the path would be white, everything outside of the path would be black. This way, I can then render this CGRect to some sort of an image, which I could then use as a mask for other purposes.
I am struggling to figure out how to do this with a focus on performance. My tests, as noted below, leverage using UIGraphicsImageRenderer which is far too slow for my needs (I will be doing this on sample buffers from a camera). Therefore, I would like to stick within CoreImage. This is my attempt;
// Path
let path = UIBezierPath()
// ... define the path's shape and close it
// My source image
let image = CIImage(cgImage: UIImage(named: "test.jpg")!.cgImage!)
// Renderer
let renderer = UIGraphicsImageRenderer(size: image.extent.size)
// Render path as mask
let img = renderer.image { ctx in
ctx.cgContext.setFillColor(UIColor.black.cgColor)
ctx.cgContext.fill(CGRect(x: 0, y: 0, width: image.extent.size.width, height: image.extent.size.height))
ctx.cgContext.setFillColor(UIColor.white.cgColor)
ctx.cgContext.addPath(path.cgPath)
ctx.cgContext.drawPath(using: .fill)
}
// Put a filter on the image
let imageFiltered = image.applyingFilter("CIPhotoEffectNoir")
// Blend with mask
let maskFilter = CIFilter.blendWithMask()
maskFilter.inputImage = imageFiltered
maskFilter.backgroundImage = image
maskFilter.maskImage = CIImage(cgImage: img.cgImage!)
// Output
if let output = maskFilter.outputImage {
... use CIContext() to render back to CVPixelBuffer for preview on MTKView.
}
Overall, the goal is to have a defined portion of an image (which will not conform to a traditional shape like a square or circle) which will be filtered with a CIFilter, then composited back over the original. If there is a better approach (such as somehow taking the original image, filtering it, cropping it to the path (leaving everything outside of the path transparent) and composing, that would likely be better performant.
To note, the above sample code results in a crash as the UIGraphicsImageRenderer cannot render the mask fast enough.
Your approach looks good so far. I assume the slow part is the generation of the mask image with Core Graphics. Unfortunately, there is no direct way to do the same with Core Image directly (on the GPU). However, you can try the following:
(Assuming from your previous question that the path always has a certain shape,) you can generate a mask image containing the path once for a certain reference size of your choice. Make sure that the path doesn't "touch" the border.
Then, when you want to use it as a mask, move and scale the shape image to the correct place using transformations and let its edges extend infinitely (to cover the whole underlying image; that's why the shape shouldn't touch the edges). Something like this:
let pathImage = CIImage(cgImage: img.cgImage!)
// scale path to the size of the area you want to mask
var mask = pathImage.transformed(by: CGAffineTransform(scaleX: scaleX, y: scaleY))
// move path to the place you want to cover
mask = mask.transformed(by: CGAffineTransform(translationX: offsetX, y: offsetY))
// let mask fill the rest of the area
mask = mask.clampedToExtent()
// use mask as maskImage...
You should be able to recycle the pathImage for every frame and thereby avoiding Core Graphics and CPU-GPU-synchronization.

textSize property of SCNText is missing

I want to add a SCNPlane around the SCNText in ARSCNView. therefore I need the textSize of a text which I have created with SCNText like:
as you can see from the image, there is an error which indicate that the
SCNText has no member 'textSize'
but I can see the related documentation in apple website for both SCNText and textSize.
does anybody know what is the problem and how can I access the textSize property?
I'm using xCode 10.0 beta 4, swift 4.2, macOS Mojave 10.14 Beta.
The textSize property is only available on macOS 10.8+ which is why you can't see it (assuming of course you are building for IOS). By this, I mean that if you are building a MacOS app then the variable is accessible. Whereas if you are building for IOS it is not.
If you want to get the size of your SCNText you can use it's boundingBox property to get its width and height for example e.g:
//1. Get The Bounding Box Of The Text Node
let boundingBox = textNode.boundingBox
//2. Get The Width & Height Of Our Node
let width = boundingBox.max.x - boundingBox.min.x
let height = boundingBox.max.y - boundingBox.min.y
If you just want to set the fontSize you can do something like so:
//1. Create An SCNNode With An SCNText Geometry
let textNode = SCNNode()
let textGeometry = SCNText(string: "StackOverflow", extrusionDepth: 1)
textGeometry.font = UIFont(name: "Skia-Regular_Black", size: 20)
Remembering of course that when you specify the font size in ARKit, this in meters.
Hope it helps...

How to Convert Text With Attributes Directly Into a CIImage (Without Drawing to Screen)

I would like to convert text with attributes (i.e., a specific font and size) directly into a CIImage—that is, without drawing it to screen first—so that I can use a custom CIFilter to dynamically alter the text's appearance. How can I do this?
Here's how to draw a NSAttributedString onto a NSImage. I didn't test the conversion to CIImage though (the last line), but it shouldn't be too difficult:
let string = NSAttributedString(string: "Hello World!", attributes: [NSFontAttributeName: NSFont.labelFontOfSize(10)])
let image = NSImage(size: string.size())
image.lockFocus()
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.currentContext()!.shouldAntialias = true
string.drawAtPoint(NSZeroPoint)
NSGraphicsContext.restoreGraphicsState()
image.unlockFocus()
let ciimage = CIImage(data: image.TIFFRepresentation!)!

uibezierpath with multiple line width and a background image (swift)

I'm trying to draw several shapes (rectangle, triangle, circle, ...) in swift and for that, I use uibezierpath but I am not able to draw exactly what I want.
I need to draw for example a rectangle, but the borders of this rectangle need to have different line width.
To do that, I create different path then use the "appendpath" to merge them in one path. So that works, BUT I also need to have a background image in this rectangle.
For that, I create a layer and set it an image. The issue is that, no background image are displayed when I use "appendpath", certainly because it doesn't recognize my drawing as a rectangle.
I hope it is clear enough, but is there a way to draw a shape with a background image, and have different border width ?
Thanks for your help !!
There are two solutions I'd suggest you try:
1) Masking
Create a normal CALayer and set the image as its contents. Then create a CAShapeLayer with the path you like and use it as the first layer's mask.
E.g.:
let imageLayer = CALayer()
imageLayer.contents = UIImage(named: "yourImage")?.CGImage // Your image here
imageLayer.frame = ... // Define a frame
let maskPath = UIBezierPath(...) // Create your path here
let maskLayer = CAShapeLayer()
maskLayer.path = maskPath.CGPath
imageLayer.mask = maskLayer
Don't forget to set the right frames and paths, and you should be able to achieve the effect you wanted.
2) Fill color
Create a CAShapeLayer with the path you like, then use your image as its fillColor.
E.g.:
let path = UIBezierPath(...) // Create your path here
let layer = CAShapeLayer()
layer.path = path.CGPath
let image = UIImage(named: "yourImage") // Your image here
layer.fillColor = UIColor(patternImage: image!).CGColor
You may find this approach easier at first, but controlling the way the image fills your shape is not trivial at all.
I hope this will help.
If you'd like more details, please provide an image or a sketch of what you're trying to achieve and / or the code you've written so far. Thanks!