1

Is it possible give a circular mask/crop to an image node without jagged edges?

Following this example from Apple (https://developer.apple.com/reference/spritekit/skcropnode), the result is not ideal. You can click on the link to see.

    let shapeNode = SKShapeNode()
    shapeNode.physicsBody = SKPhysicsBody(circleOfRadius: radius)
    shapeNode.physicsBody?.allowsRotation = false
    shapeNode.strokeColor = SKColor.clearColor()

    // Add a crop node to mask the profile image
    // profile images (start off with place holder)
    let scale = 1.0
    let profileImageNode = SKSpriteNode(imageNamed: "PlaceholderUser")
    profileImageNode.setScale(CGFloat(scale))

    let circlePath = CGPathCreateWithEllipseInRect(CGRectMake(-radius, -radius, radius*2, radius*2), nil)

    let circleMaskNode = SKShapeNode()
    circleMaskNode.path = circlePath
    circleMaskNode.zPosition = 12
    circleMaskNode.name = "connection_node"
    circleMaskNode.fillColor = SKColor.whiteColor()
    circleMaskNode.strokeColor = SKColor.clearColor()

    let zoom = SKAction.fadeInWithDuration(0.25)
    circleMaskNode.runAction(zoom)

    let cropNode = SKCropNode()
    cropNode.maskNode = circleMaskNode
    cropNode.addChild(profileImageNode)
    cropNode.position = shapeNode.position

    shapeNode.addChild(cropNode)
    self.addChild(shapeNode)
Tuan Anh Vu
  • 760
  • 1
  • 7
  • 26

4 Answers4

2

UPDATE:

Ok, so here's one solution I came up. Not super ideal but it works perfectly. Essentially, I have a to size/scale, and cut the image exactly the way it would go on the SKSpriteNode so I would not have to use SKCropNode or some variation of SKShapeNode.

  1. I used these UIImage extensions by Leo Dabus to resize/shape the image exactly as needed. Cut a UIImage into a circle Swift(iOS)

    var circle: UIImage? {
         let square = CGSize(width: min(size.width, size.height), height: min(size.width, size.height))
         let imageView = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: square))
         imageView.contentMode = .ScaleAspectFill
         imageView.image = self
         imageView.layer.cornerRadius = square.width/2
         imageView.layer.masksToBounds = true
         UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, false, scale)
         guard let context = UIGraphicsGetCurrentContext() else { return nil }
         imageView.layer.renderInContext(context)
         let result = UIGraphicsGetImageFromCurrentImageContext()
         UIGraphicsEndImageContext()
         return result
    }
    
    func resizedImageWithinRect(rectSize: CGSize) -> UIImage {
         let widthFactor = size.width / rectSize.width
         let heightFactor = size.height / rectSize.height
    
         var resizeFactor = widthFactor
         if size.height > size.width {
         resizeFactor = heightFactor
         }
    
         let newSize = CGSizeMake(size.width/resizeFactor, size.height/resizeFactor)
         let resized = resizedImage(newSize)
         return resized
    }
    
  2. The final codes look like this:

    //create/shape image
    let image = UIImage(named: "TestImage")
    let scaledImage = image?.resizedImageWithinRect(CGSize(width: 100, height: 100))
    let circleImage = scaledImage?.circle
    
    //create sprite
    let sprite = SKSpriteNode(texture: SKTexture(image: circleImage!))
    sprite.position = CGPoint(x: view.frame.width/2, y: view.frame.height/2)
    
    //set texture/image
    sprite.texture = SKTexture(image: circleImage!)
    sprite.physicsBody = SKPhysicsBody(texture: SKTexture(image: circleImage!), size: CGSizeMake(100, 100))
    if let physics = sprite.physicsBody {
        //add the physic properties
    }
    
    //scale node
    sprite.setScale(1.0)
    addChild(sprite)
    

So if you have a perfectly scaled asset/image, then you probably dont need to do all this work, but I'm getting images from the backend that could come in any sizes.

Community
  • 1
  • 1
Tuan Anh Vu
  • 760
  • 1
  • 7
  • 26
  • You're passing in "scale" to UIGraphicsBeginImageContextWithOptions, but it's never set beforehand. – CodyMace Jan 13 '17 at 16:12
0

There are two different techniques that can be combined to reduce the aliasing of edges created from cropping.

  1. Create bigger images than you need, and then scale them down. Both the target (to be cropped) and the mask. Perform the cropping action, then scale down to required size.

  2. Use very subtle blurring of the cropping shape, to soften its edges. This is best done in Photoshop or a similar editing program, to taste and need.

When these two techniques are combined, the results can be very good.

Confused
  • 6,048
  • 6
  • 34
  • 75
  • Ah, yup! I ended up going with #1 because we have to get images from the server which we don't really know the size of until we get it. – Tuan Anh Vu Oct 11 '16 at 17:44
  • Cool. I just remembered, having done your sizing, you could apply a very gentle blur with Core Image, try this, at very low numbers, on the mask image: https://developer.apple.com/library/content/documentation/GraphicsImaging/Reference/CoreImageFilterReference/#//apple_ref/doc/filter/ci/CIGaussianBlur – Confused Oct 11 '16 at 17:48
  • nice! that would give it a more pro look. ;) – Tuan Anh Vu Oct 11 '16 at 18:09
  • Yeah, sorry, I had a mental blank when writing the answer, completely forgot about the Core Image effects. – Confused Oct 11 '16 at 18:54
  • btw, not sure if you know this... but applying Blur effects at very small values, repeatedly, gives better results than doing it once with a larger number. Obviously more expensive in terms of time, but if you have that time available, this is the best way to get very smooth edges. – Confused Oct 11 '16 at 18:55
  • No problem! What I'm learning from your code is going to be very useful to me, shortly, so THANK YOU!!! – Confused Oct 11 '16 at 20:21
0

Let the stroke color to be displayed. Also, you can make line width a little thicker and the jagged edges will dissapear.

 circleMaskNode.strokeColor = SKColor.whiteColor()
Alexandru Vasiliu
  • 534
  • 1
  • 4
  • 18
0

All you have to do is change the SKShapeNode's lineWidth property to be twice the radius of the circle:

func circularCropNode(radius: CGFloat, add: SKNode) {
    let cropper = SKCropNode.init()
    cropper.addChild(add)
    addChild(cropper)
        
    let circleMask = SKShapeNode.init(circleOfRadius: radius/2)
    circleMask.lineWidth = radius
    cropper.maskNode = circleMask
}
0-1
  • 702
  • 1
  • 10
  • 30