SpriteKit draw upside down text - ios

In a playground, I am trying to make a SpriteKit scene with a top left origin going down (ie like a drawing program). Everything works except for text which is flipped. I need to make the text right side up. I am missing something. If I add the translate and scale, the text vanishes.
//: A SpriteKit based Playground
import PlaygroundSupport
import SpriteKit
class GameScene: SKScene
{
static let width = 1000
static let height = 1800
override func didMove(to view: SKView)
{
self.backgroundColor = SKColor.white
let cam = SKCameraNode()
self.camera = cam
cam.yScale = -1
let node = SKSpriteNode(color: SKColor.yellow, size: CGSize(width: 500, height: 300))
node.anchorPoint = CGPoint(x: 0, y: 0)
node.position = CGPoint(x: 10, y: 10)
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 200, height: 200))
let img = renderer.image { ctx in
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let attrs = [NSAttributedStringKey.font: UIFont(name: "Futura", size: 72)!, NSAttributedStringKey.paragraphStyle: paragraphStyle]
//ctx.cgContext.translateBy(x: 0, y:200)
//ctx.cgContext.scaleBy(x: 0, y: -1)
let string = "Test"
string.draw(with: CGRect(x: 32, y: 32, width: 200, height: 200), options: .usesLineFragmentOrigin, attributes: attrs, context: nil)
}
let tex = SKTexture(image: img)
let zzz = SKSpriteNode(texture: tex)
zzz.anchorPoint = CGPoint(x: 0, y: 0)
zzz.position = CGPoint(x: 10, y: 10)
node.addChild(zzz)
// let z = SKSpriteNode(color: SKColor.red, size: CGSize(width: 200, height: 150))
// z.anchorPoint = CGPoint(x: 0, y: 0)
// z.position = CGPoint(x: 10, y: 10)
// node.addChild(z)
//
// let q = SKSpriteNode(color: SKColor.green, size: CGSize(width: 80, height: 70))
// q.anchorPoint = CGPoint(x: 0, y: 0)
// q.position = CGPoint(x: 10, y: 10)
// z.addChild(q)
self.addChild(node)
self.addChild(cam)
cam.position = CGPoint(x: GameScene.width/2, y: GameScene.height/2)
}
}
let sceneView = SKView(frame: CGRect(x:0 , y:0, width: 480, height: 640))
let scene = GameScene(size: CGSize(width: GameScene.width, height: GameScene.height))
scene.scaleMode = .aspectFit
sceneView.presentScene(scene)
PlaygroundSupport.PlaygroundPage.current.liveView = sceneView
Update:
ctx.cgContext.textMatrix = .identity
ctx.cgContext.translateBy(x: 0, y: 200)
ctx.cgContext.scaleBy(x: 1.0, y: -1.0)
now draws the text rightside up, but it is wrapped at 50% of the width
Final Update:
This worked, its rightsize up. The font size is proportional to the scene size (which is a dimension that fits on all iOS devices.
self.camera = cam
cam.yScale = -1
let boxSize = CGSize(width: 500, height: 500)
let node = SKSpriteNode(color: SKColor.yellow, size: boxSize)
node.anchorPoint = CGPoint(x: 0, y: 0)
node.position = CGPoint(x: 10, y: 10)
let renderer = UIGraphicsImageRenderer(size: boxSize)
let img = renderer.image { ctx in
let bounds = CGRect(x: 16, y: 0, width: boxSize.width-32, height: boxSize.height)
let string = "This is a long test with many lines."
let range = NSRange( location: 0, length: string.count)
guard let context = UIGraphicsGetCurrentContext() else { return }
let path = CGMutablePath()
path.addRect(bounds)
let attrString = NSMutableAttributedString(string: string)
attrString.addAttribute(NSAttributedStringKey.font, value: UIFont(name: "Futura", size: 84)!, range: range )
let framesetter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString)
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrString.length), path, nil)
CTFrameDraw(frame, context)
}

You should read https://www.raywenderlich.com/153591/core-text-tutorial-ios-making-magazine-app
CoreText inverts (technically, rotates) text in iOS. This is possibly because it is shared with MacOS which uses a different coordinate system. (Though why they couldn't fix this for iOS I have no idea).
Whatever the cause, you aren't doing anything wrong, iOS is doing the wrong thing. The simple solution (as per the linked article) is to simply rotate the text before drawing it.

Related

Does anyone have a good way to render PKDrawing to PDF clearly?

I'm trying to share my PKDrawing results via PDF file for email or print. I'm creating the PDF using 2 other PDF files plus the PKDrawing on the top layer. The 2 PDF layers look clear. The PKDrawing however is somewhat grainy, especially compared to a printed screen shot.
Here's my current code:
UIGraphicsBeginPDFContextToFile(filePath, CGRect.zero, nil)
var drawing: PKDrawing = PKDrawing.init()
let markup: Data = self.pages![pagenum].value(forKey: "markup") as! Data
let drawingHeight = self.pages![pagenum].value(forKey: "height") as! CGFloat
let drawingWidth = self.pages![pagenum].value(forKey: "width") as! CGFloat
do {
try drawing = PKDrawing.init(data: markup)
} catch {
print("Error loading drawing")
}
let pageSize = CGSize(width: 0, height: 0)
//let scale: CGFloat = 2.0 THIS VALUE DOESN'T SEEM TO MATTER
let scale: CGFloat = 1.0
if landscapeOrientation {
pageSize = CGSize(width:2048, height: 1408)
} else {
pageSize = CGSize(width: 1536, height: 1920)
}
let canvas = PKCanvasView(frame: CGRect(x: 0.0, y: 0.0, width: drawingWidth, height: drawingHeight))
canvas.drawing = drawing
markupImage = drawing.image(from: canvas.frame, scale: scale)
if pageSize.width != drawingWidth {
canvas.drawing.transform(using: CGAffineTransform(scaleX: pageSize.width / drawingWidth, y: pageSize.height / drawingHeight))
}
//markupImage = drawing.image(from: canvas.frame, scale: self.view.contentScaleFactor) THIS CHANGE MADE A BIG DIFFERENCE
markupImage = drawing.image(from: canvas.frame, scale: 4.0)
UIGraphicsBeginImageContext(pageSize)
backgroundView?.draw(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: pageSize))
templateView?.draw(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: pageSize))
markupImage.draw(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: pageSize))
let pdfImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
let pageRect: CGRect = CGRect(origin: CGPoint.zero, size: pageSize)
UIGraphicsBeginPDFPageWithInfo(pageRect, nil)
let pdfContext: CGContext = UIGraphicsGetCurrentContext()!
pdfContext.scaleBy(x: 1.0, y: 1.0)
pdfImage.draw(at: CGPoint(x: 0.0, y: 0.0))
UIGraphicsEndPDFContext()
Changing my scale from 2.0 to 4.0 cleared it up quite a bit, I hadn't thought to try that. Will edit the code with my change and a comment. Definitely open for other alternatives though if there's a cleaner or clearer way to do this. I'm not the best at resolutions / pixel spaces.

Swift 3 draw uiimage programmatically

I don't have experience in Core Graphics but I need to draw a dynamic uiimage that look like these:
left
whole
(Actually I want the grey area to be clear. So the red color will look like floating)
This is the code I tried:
public extension UIImage {
public convenience init?(color: UIColor, size: CGSize = CGSize(width: 27, height: 5), isWhole: Bool = true) {
let totalHeight: CGFloat = 5.0
let topRectHeight: CGFloat = 1.0
//if (isWhole) {
let topRect = CGRect(origin: .zero, size: CGSize(width: size.width, height: topRectHeight))
UIGraphicsBeginImageContextWithOptions(topRect.size, false, 0.0)
color.setFill()
UIRectFill(topRect)
let bottomRect = CGRect(origin: CGPoint(x: 0, y: topRectHeight), size: CGSize(width: size.width, height: totalHeight - topRectHeight))
UIGraphicsBeginImageContextWithOptions(bottomRect.size, false, 0.0)
UIColor.blue.setFill()
UIRectFill(bottomRect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let cgImage = image?.cgImage else { return nil }
self.init(cgImage: cgImage)
}
Here is example you can have first image if you set isWhole property to false and have second image if you set it to true. You can paste this code in viewDidLoad to test and play with it.
var isWhole = false
UIGraphicsBeginImageContextWithOptions(CGSize.init(width: 27, height: 5), false,0.0)
var context = UIGraphicsGetCurrentContext()
context?.setFillColor(UIColor.red.cgColor)
if(context != nil){
if(isWhole){
context?.fill(CGRect(x: 0, y: 0, width: 27, height: 2.5))
}
else{
context?.fill(CGRect(x: 0, y: 0, width: 13.5, height: 2.5))
}
context?.setFillColor(UIColor.gray.cgColor)
context?.fill(CGRect(x: 0, y: 2.5, width: 27, height: 2.5))
}
let newImage = UIGraphicsGetImageFromCurrentImageContext()
var imageView = UIImageView(frame: CGRect(x: 100, y: 200, width: 27, height: 5))
imageView.image = newImage
self.view.addSubview(imageView)
UIGraphicsEndImageContext()
If you need your red rectangle to be with rounder corners just change fill(rect:CGRect) with path like this:
if(isWhole){
context?.addPath(CGPath(roundedRect: CGRect(x: 0, y: 0, width: 27, height: 2.5), cornerWidth: 1, cornerHeight: 1, transform: nil))
context?.fillPath()
}
I'd recommend creating two paths in the first context that you create, i.e.
let topPath = UIBezierPath(rect: topRect)
color.setFill()
topPath.fill()
let bottomPath = UIBezierPath(rect: bottomRect)
UIColor.blue.setFill()
bottomPath.fill()
Then you can get the image from the current context.
I know you said in your comments that you can't use UIViews, but what you want "looks" easily done through views. Why not do that, then simply turn it into a UIImage?
override func viewDidLoad() {
super.viewDidLoad()
let imageContainer = UIView(frame: CGRect(x: 30, y: 40, width: 110, height: 60))
imageContainer.backgroundColor = view.backgroundColor
view.addSubview(imageContainer)
let redView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 30))
redView.backgroundColor = UIColor.red
let grayView = UIView(frame: CGRect(x: 10, y: 30, width: 100, height: 30))
grayView.backgroundColor = UIColor.gray
imageContainer.addSubview(redView)
imageContainer.addSubview(grayView)
let imageView = UIImageView(frame: CGRect(x: 100, y: 140, width: 200, height: 200))
imageView.contentMode = .scaleAspectFit
imageView.image = createImage(imageContainer)
view.addSubview(imageView)
}
func createImage(_ view: UIView) -> UIImage {
UIGraphicsBeginImageContextWithOptions(
CGSize(width: view.frame.width, height: view.frame.height), true, 1)
view.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}

Converting CGPoints between UIKit and SpriteKit woes: UIViews next to SKNodes

There are many posts on this but for whatever reason I can't seem to get the correct sequence of conversions correct. I'm trying to get the UISliders to go directly below the SKLabels.
As you can see in the pictures, one of the sliders doesn't show at all, and the other one is in the completely wrong place (the volume slider is near the seek label):
Here is my current attempt at getting this right, though I've tried a few other configurations that got me nowhere:
class GameScene: SKScene {
let seekSlider = UISlider(frame: CGRect(x: 0, y: 0, width: 200, height: 15))
let volSlider = UISlider(frame: CGRect(x: 0, y: 0, width: 200, height: 15))
override func didMove(to view: SKView) {
removeAllChildren() // Delete this in your actual project.
// Add some labels:
let seekLabel = SKLabelNode(text: "Seek")
seekLabel.setScale(3)
seekLabel.verticalAlignmentMode = .center
seekLabel.position = CGPoint(x: frame.minX + seekLabel.frame.width/2,
y: frame.maxY - seekLabel.frame.height)
let volLabel = SKLabelNode(text: "Volume")
volLabel.setScale(3)
volLabel.verticalAlignmentMode = .center
volLabel.position = CGPoint(x: frame.minX + volLabel.frame.width/2,
y: frame.minY + volLabel.frame.height + volSlider.frame.height)
/* CONVERSION WOES BELOW: */
// Configure sliders:
let seekOrigin = convertPoint(toView: convert(seekLabel.position, from: self))
seekSlider.frame = CGRect(origin: seekOrigin, size: CGSize(width: 200, height: 15))
seekSlider.value = 1
let volOrigin = convertPoint(fromView: CGPoint(x: volLabel.frame.minX, y: volLabel.frame.minY))
seekSlider.frame = CGRect(origin: volOrigin, size: CGSize(width: 200, height: 15))
seekSlider.value = 0
// Scene stuff:
view.addSubview(seekSlider)
view.addSubview(volSlider)
addChild(seekLabel)
addChild(volLabel)
}
}
You're very close! There are two minor issues in your code:
Issue #1
You modify the seekSlider's frame twice instead of modifying the seekSlider then the volSlider.
let volOrigin = convertPoint(fromView: CGPoint(x: volLabel.frame.minX, y: volLabel.frame.minY))
seekSlider.frame = CGRect(origin: volOrigin, size: CGSize(width: 200, height: 15))
seekSlider.value = 0
should be
let volOrigin = convertPoint(fromView: CGPoint(x: volLabel.frame.minX, y: volLabel.frame.minY))
volSlider.frame = CGRect(origin: volOrigin, size: CGSize(width: 200, height: 15))
volSlider.value = 0
Issue #2
The conversion code is not correct.
Using the convertPoint(toView method with the node's position should do the trick:
let seekOrigin = convertPoint(toView: seekLabel.position)
Final Code:
class GameScene: SKScene {
let seekSlider = UISlider(frame: CGRect(x: 0, y: 0, width: 200, height: 15))
let volSlider = UISlider(frame: CGRect(x: 0, y: 0, width: 200, height: 15))
override func didMove(to view: SKView) {
removeAllChildren() // Delete this in your actual project.
// Add some labels:
let seekLabel = SKLabelNode(text: "Seek")
seekLabel.setScale(3)
seekLabel.verticalAlignmentMode = .center
seekLabel.position = CGPoint(x: frame.minX + seekLabel.frame.width/2,
y: frame.maxY - seekLabel.frame.height)
let volLabel = SKLabelNode(text: "Volume")
volLabel.setScale(3)
volLabel.verticalAlignmentMode = .center
volLabel.position = CGPoint(x: frame.minX + volLabel.frame.width/2,
y: frame.minY + volLabel.frame.height + volSlider.frame.height)
// Configure sliders:
let seekOrigin = convertPoint(toView: seekLabel.position)
let offsetSeekOrigin = CGPoint(x: seekOrigin.x, y: seekOrigin.y + 50)
seekSlider.frame = CGRect(origin: offsetSeekOrigin, size: CGSize(width: 200, height: 15))
seekSlider.value = 1
let volOrigin = convertPoint(toView: volLabel.position)
let offsetVolOrigin = CGPoint(x: volOrigin.x, y: volOrigin.y - 50)
volSlider.frame = CGRect(origin: offsetVolOrigin, size: CGSize(width: 200, height: 15))
volSlider.value = 0
// Scene stuff:
view.addSubview(seekSlider)
view.addSubview(volSlider)
addChild(seekLabel)
addChild(volLabel)
}
}
Final Result:

Gap between SKSpriteNodes in SpriteKit collision detection

I've been trying to figure this out for quite a while now -- I have a game with simple platformer physics where a player falls onto a block, which stops him from falling. This works, however there is a noticeable gap between where the player stops, and where the actual object/spritenode is. Here is a screenshot, it should be self-explanatory:
class GameScene: SKScene {
override init(){
super.init(size: UIScreen.mainScreen().bounds.size)
self.physicsWorld.gravity = CGVector(dx: 0, dy: -9.8)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func genBlock(iteration: CGFloat){
let block = SKSpriteNode(color: UIColor.blackColor(), size: CGSizeMake(100,50))
block.position = CGPointMake((iteration*50)-block.size.width/2,0)
block.physicsBody = SKPhysicsBody(edgeLoopFromRect: CGRectMake(0,0,block.size.width,block.size.height))
self.addChild(block)
}
func genPlayer(){
let char = SKSpriteNode(color: UIColor.redColor(), size: CGSizeMake(100,100))
char.position = CGPointMake(0,500)
char.physicsBody = SKPhysicsBody(rectangleOfSize: char.size)
self.addChild(char)
}
override func didMoveToView(view: SKView) {
/* Setup your scene here */
view.backgroundColor = UIColor.whiteColor()
self.view?.backgroundColor = UIColor.whiteColor()
genBlock(0)
genBlock(1)
genBlock(2)
genBlock(3)
genBlock(4)
genPlayer()
}
}
I feel as though this isn't an issue with SpriteKit and is instead an issue with my code. Any advice on how to remove this gap between the nodes would be greatly appreciated.
EDIT: I've submitted this to Apple's bug reporter, but I don't expect to receive anything in reply. If anyone is coming here long after this was posted, it would be great if you could submit your suggestions here.
I believe it has to do with physicsBodies built from a CGPath. For instance, using the code below to create 6 squares:
override func didMoveToView(view: SKView) {
self.physicsBody = SKPhysicsBody(edgeLoopFromRect: self.frame)
self.physicsWorld.gravity = CGVector(dx: 0, dy: -9.8)
self.backgroundColor = UIColor.whiteColor()
let square1 = SKSpriteNode(color: UIColor.blackColor(), size: CGSize(width: 50, height: 50))
square1.physicsBody = SKPhysicsBody(edgeLoopFromRect: CGRect(x: -25, y: -25, width: square1.size.width, height: square1.size.height))
square1.position = CGPoint(x: self.size.width/2 - 75, y: 25)
self.addChild(square1)
let square2 = SKSpriteNode(color: UIColor.greenColor(), size: CGSize(width: 50, height: 50))
square2.physicsBody = SKPhysicsBody(rectangleOfSize: square2.size)
square2.position = CGPoint(x: self.size.width/2 - 75, y: 200)
self.addChild(square2)
let square3 = SKSpriteNode(color: UIColor.blueColor(), size: CGSize(width: 50, height: 50))
square3.physicsBody = SKPhysicsBody(rectangleOfSize: square3.size)
square3.position = CGPoint(x: self.size.width/2 + 75, y: 200)
self.addChild(square3)
let squarePath = getSquarePath()
let square4 = SKSpriteNode(color: UIColor.redColor(), size: CGSize(width: 50, height: 50))
square4.physicsBody = SKPhysicsBody(polygonFromPath: squarePath)
square4.position = CGPoint(x: self.size.width/2 + 75, y: 400)
self.addChild(square4)
let square5 = SKSpriteNode(color: UIColor.orangeColor(), size: CGSize(width: 50, height: 50))
square5.physicsBody = SKPhysicsBody(rectangleOfSize: square5.size)
square5.position = CGPoint(x: self.size.width/2, y: 200)
self.addChild(square5)
let square6 = SKSpriteNode(color: UIColor.purpleColor(), size: CGSize(width: 50, height: 50))
square6.physicsBody = SKPhysicsBody(rectangleOfSize: square6.size)
square6.position = CGPoint(x: self.size.width/2, y: 400)
self.addChild(square6)
}
func getSquarePath() -> CGPath {
let path = CGPathCreateMutable()
CGPathMoveToPoint(path, nil, -25, -25)
CGPathAddLineToPoint(path, nil, -25, 25)
CGPathAddLineToPoint(path, nil, 25, 25)
CGPathAddLineToPoint(path, nil, 25, -25)
CGPathCloseSubpath(path)
return path
}
you can see that the squares with physicsBodies created from CGPath (this includes edgeLoopFromRect) have that gap issue. Even the physicsBody for the scene's edge loop has this issue. This also shows up for me when I build the physicsBody using the texture constructors which appears to build a CGPath under the hood.
I haven't found a fix for it other than to not use those types of physicsBodies for long term (visible) collisions.

How to get the intersection of two CGPath?

I am using CAShapeLayer.path and CALayer.mask to set up image mask, i can achieve "difference set" and "union" effect for CGPath by setting
maskLayer.fillRule = kCAFillRuleEvenOdd/kCAFillRuleZero
However, how can i get the intersection of two paths? I cannot achieve it with even-odd rule
Here is an example:
let view = UIImageView(frame: CGRectMake(0, 0, 400, 400))
view.image = UIImage(named: "scene.jpg")
let maskLayer = CAShapeLayer()
let maskPath = CGPathCreateMutable()
CGPathAddEllipseInRect(maskPath, nil, CGRectOffset(CGRectInset(view.bounds, 50, 50), 50, 0))
CGPathAddEllipseInRect(maskPath, nil, CGRectOffset(CGRectInset(view.bounds, 50, 50), -50, 0))
maskLayer.path = maskPath
maskLayer.fillRule = kCAFillRuleEvenOdd
maskLayer.path = maskPath
view.layer.mask = maskLayer
how can i get the middle intersection area to be shown?
extension UIImage {
func doubleCircleMask(diameter: CGFloat)-> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, 0)
defer { UIGraphicsEndImageContext() }
let x = size.width/2 - diameter/2
let y = size.height/2 - diameter/2
let offset = diameter/4
let bezierPath: UIBezierPath = .init(ovalIn: .init(origin: .init(x: x-offset, y: y), size: .init(width: diameter, height: diameter)))
bezierPath.append(.init(ovalIn: .init(origin: .init(x: x+offset, y: y), size: .init(width: diameter, height: diameter))))
bezierPath.addClip()
draw(in: .init(origin: .zero, size: size))
return UIGraphicsGetImageFromCurrentImageContext()
}
func doubleCircleIntersectionMask(diameter: CGFloat)-> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, 0)
defer { UIGraphicsEndImageContext() }
let x = size.width/2 - diameter/2
let y = size.height/2 - diameter/2
let offset = diameter/4
UIBezierPath(ovalIn: .init(origin: .init(x: x-offset, y: y), size: .init(width: diameter, height: diameter))).addClip()
UIBezierPath(ovalIn: .init(origin: .init(x: x+offset, y: y), size: .init(width: diameter, height: diameter))).addClip()
draw(in: .init(origin: .zero, size: size))
return UIGraphicsGetImageFromCurrentImageContext()
}
}
Testing
let image = UIImage(data: try! Data(contentsOf: URL(string: "http://i.stack.imgur.com/Xs4RX.jpg")!))!
let imageUnion = image.doubleCircleMask(diameter: 300)
let imageIntersection = image.doubleCircleIntersectionMask(diameter: 300)

Resources