How to get the intersection of two CGPath? - ios

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)

Related

Saving a UIImage cropped to a CGPath

I am trying to mask a UIImage and then save the masked image. So far, I have got this working when displayed in a UIImageView preview as follows:
let maskLayer = CAShapeLayer()
let maskPath = shape.cgPath
maskLayer.path = maskPath.resized(to: imageView.frame)
maskLayer.fillRule = .evenOdd
imageView.layer.mask = maskLayer
let picture = UIImage(named: "1")!
imageView.contentMode = .scaleAspectFit
imageView.image = picture
where shape is a UIBezierPath().
The resized function is:
extension CGPath {
func resized(to rect: CGRect) -> CGPath {
let boundingBox = self.boundingBox
let boundingBoxAspectRatio = boundingBox.width / boundingBox.height
let viewAspectRatio = rect.width / rect.height
let scaleFactor = boundingBoxAspectRatio > viewAspectRatio ?
rect.width / boundingBox.width :
rect.height / boundingBox.height
let scaledSize = boundingBox.size.applying(CGAffineTransform(scaleX: scaleFactor, y: scaleFactor))
let centerOffset = CGSize(
width: (rect.width - scaledSize.width) / (scaleFactor * 2),
height: (rect.height - scaledSize.height) / (scaleFactor * 2)
)
var transform = CGAffineTransform.identity
.scaledBy(x: scaleFactor, y: scaleFactor)
.translatedBy(x: -boundingBox.minX + centerOffset.width, y: -boundingBox.minY + centerOffset.height)
return copy(using: &transform)!
}
}
So this works in terms of previewing the outcome I'd like. I'd now like to save this modified UIImage to the user's photo album, in it's original size (so basically generate it again but don't resize the image to fit a UIImageView - keep it as-is and apply the mask over it).
I have tried this, but it just saves the original image - no path/mask applied:
func getMaskedImage(path: CGPath) {
let picture = UIImage(named: "1")!
UIGraphicsBeginImageContext(picture.size)
if let context = UIGraphicsGetCurrentContext() {
let pathNew = path.resized(to: CGRect(x: 0, y: 0, width: picture.size.width, height: picture.size.height))
context.addPath(pathNew)
context.clip()
picture.draw(in: CGRect(x: 0.0, y: 0.0, width: picture.size.width, height: picture.size.height))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
UIImageWriteToSavedPhotosAlbum(newImage!, nil, nil, nil)
}
}
What am I doing wrong? Thanks.
Don't throw away your layer-based approach! You can still use that when drawing in a graphics context.
Example:
func getMaskedImage(path: CGPath) -> UIImage? {
let picture = UIImage(named: "my_image")!
let imageLayer = CALayer()
imageLayer.frame = CGRect(origin: .zero, size: picture.size)
imageLayer.contents = picture.cgImage
let maskLayer = CAShapeLayer()
let maskPath = path.resized(to: CGRect(origin: .zero, size: picture.size))
maskLayer.path = maskPath
maskLayer.fillRule = .evenOdd
imageLayer.mask = maskLayer
UIGraphicsBeginImageContext(picture.size)
defer { UIGraphicsEndImageContext() }
if let context = UIGraphicsGetCurrentContext() {
imageLayer.render(in: context)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
return newImage
}
return nil
}
Note that this doesn't actually resize the image. If you want the image resized, you should get the boundingBox of the resized path. Then do this instead:
// create a context as big as the bounding box
UIGraphicsBeginImageContext(boundingBox.size)
defer { UIGraphicsEndImageContext() }
if let context = UIGraphicsGetCurrentContext() {
// move the context to the top left of the path
context.translateBy(x: -boundingBox.origin.x, y: -boundingBox.origin.y)
imageLayer.render(in: context)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
return newImage
}

Trying to create a colored circle UIImage, but it always ends up square. Why?

I have the following code. I want a blue circle created:
class func circleFromColor(_ color: UIColor, size: CGSize = CGSize(width: 1.0, height: 1.0)) -> UIImage? {
let rect = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
context.setFillColor(color.cgColor)
context.fill(rect)
let radius: CGFloat = 8.0 * UIScreen.main.scale
let maskPath = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: radius, height: radius))
maskPath.addClip()
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
However every single time it returns the image is a blue SQUARE. Not a circle, what gives?
The newer way is to use UIGraphicsImageRenderer which gives you the correct point scale automatically. Also a path can fill itself so there is no need for a clipping mask:
func circleFromColor(_ color: UIColor, size: CGSize = CGSize(width: 1.0, height: 1.0)) -> UIImage? {
UIGraphicsImageRenderer(size: size).image { context in
color.setFill()
UIBezierPath(ovalIn: .init(origin: .zero, size: size)).fill()
}
}
Here is how you do it the old way:
func circleFromColor(_ color: UIColor, size: CGSize = CGSize(width: 1.0, height: 1.0)) -> UIImage? {
let rect = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
context.setFillColor(color.cgColor)
let radius: CGFloat = 8.0 * UIScreen.main.scale
let maskPath = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: radius, height: radius))
maskPath.addClip()
maskPath.fill()
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}

SpriteKit draw upside down text

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.

UIImage masking doesn't work (Swift, iOS 10)

Trying to mask an image with my custom mask. I think I follow the ideas correctly, but for some reason, image isn't get masked. Instead, masked image, created after masking, contains original cropped image as the mask wasn't applied.
Here's the Swift playground code which one can use in order to test my code (image and mask are attached, just drop them to the resources folder):
import UIKit
extension UIImage {
static func resizeImage(image: UIImage, width: CGFloat) -> UIImage {
let scale = width / image.size.width
let newHeight = round(image.size.height * scale)
UIGraphicsBeginImageContextWithOptions(CGSize(width:width, height:newHeight), false, image.scale)
image.draw(in: CGRect(origin: CGPoint(x:0, y:0), size: CGSize(width: width, height: newHeight)))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
static func resizeImage(image: UIImage, height: CGFloat) -> UIImage {
let scale = height / image.size.height
let newWidth = round(image.size.width * scale)
UIGraphicsBeginImageContextWithOptions(CGSize(width:newWidth, height:height), false, image.scale)
image.draw(in: CGRect(origin: CGPoint(x:0, y:0), size: CGSize(width: newWidth, height: height)))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
}
let image = UIImage(named: "image.jpg")!
var mask = UIImage(named: "mask.jpg")!
let k1 = image.size.width / image.size.height
let k2 = mask.size.width / mask.size.height
if k1 >= k2
{
mask = UIImage.resizeImage(image: mask, height: image.size.height)
}
else
{
mask = UIImage.resizeImage(image: mask, width: image.size.width)
}
image
mask
let center = CGPoint(x: image.size.width/2, y: image.size.height/2)
let croppingRect = CGRect(x: abs(image.size.width-mask.size.width)/2*image.scale,
y: abs(image.size.height-mask.size.height)/2*image.scale,
width: mask.size.width*image.scale,
height: mask.size.height*image.scale).integral
let maskReference = mask.cgImage!
let imageReference = image.cgImage!.cropping(to: croppingRect)!
let imageMask = CGImage(maskWidth: maskReference.width,
height: maskReference.height,
bitsPerComponent: maskReference.bitsPerComponent,
bitsPerPixel: maskReference.bitsPerPixel,
bytesPerRow: maskReference.bytesPerRow,
provider: maskReference.dataProvider!, decode: nil, shouldInterpolate: true)
imageMask?.colorSpace
imageMask?.alphaInfo
let maskedReference = imageReference.masking(imageMask!)
let maskedImage = UIImage(cgImage:maskedReference!, scale: image.scale, orientation: image.imageOrientation)
Swift 4+
let icon = UIImageView(image: YOURIMAGE)
icon.frame = CGRect(x:100, y: 100, width: 100, height: 100)
icon.layer.masksToBounds = true
let maskView = UIImageView()
maskView.image = YOURMASKIMAGE
maskView.frame = icon.bounds
icon.mask = maskView
icon.contentMode = .scaleToFill
icon.clipsToBounds = true
view.addSubview(icon)

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!
}

Resources