I have an app where I take a UIBezierPath and use it as a brush by a series of appendPath: calls. After a few goes and with really complex brush shapes the memory runs out and the app grinds to a halt. What I really want to do is a full on union exactly like Paint Code does but I can't find any way of doing this.
How would I go about unioning two or more UIBezierPaths?
EDIT:
Here is a visual of what I want to achieve dynamically.
In Paint Code you take two paths and overlap them like this:
BUT I want to merge / union them into one new single path like:
Note that in the bottom panel of Paint Code there is code for now one single shape and this is what I want to be able to get to programatically with maybe 1000 original paths.
You can get desired result easily by following 2 concepts of Core Graphics :-
i)CGBlendMode
ii)OverLap2Layer
Blend modes tell a context how to apply new content to itself. They determine how pixel data is digitally blended.
class UnionUIBezierPaths : UIView {
var firstBeizerPath:UIImage!
var secondBeizerPath:UIImage!
override func draw(_ rect: CGRect) {
super.draw(rect)
firstBeizerPath = drawOverLapPath(firstBeizerpath: drawCircle(), secondBeizerPath: polygon())
secondBeizerPath = drawOverLapPath(firstBeizerpath: polygon(), secondBeizerPath: drawCircle())
let image = UIImage().overLap2Layer(firstLayer:firstBeizerPath , secondLayer:secondBeizerPath)
}
func drawCircle() -> UIBezierPath {
let path = UIBezierPath(ovalIn: CGRect(x: 40, y: 120, width: 100, height: 100) )
return path
}
func polygon() -> UIBezierPath {
let beizerPath = UIBezierPath()
beizerPath.move(to: CGPoint(x: 100, y: 10) )
beizerPath.addLine(to: CGPoint(x: 200.0, y: 40.0) )
beizerPath.addLine(to: CGPoint(x: 160, y: 140) )
beizerPath.addLine(to: CGPoint(x: 40, y: 140) )
beizerPath.addLine(to: CGPoint(x: 0, y: 40) )
beizerPath.close()
return beizerPath
}
func drawOverLapPath(firstBeizerpath:UIBezierPath ,secondBeizerPath:UIBezierPath ) -> UIImage {
UIGraphicsBeginImageContext(self.frame.size)
let firstpath = firstBeizerpath
UIColor.white.setFill()
UIColor.black.setStroke()
firstpath.stroke()
firstpath.fill()
// sourceAtop = 20
let mode = CGBlendMode(rawValue:20)
UIGraphicsGetCurrentContext()!.setBlendMode(mode!)
let secondPath = secondBeizerPath
UIColor.white.setFill()
UIColor.white.setStroke()
secondPath.fill()
secondPath.stroke()
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
func drawImage(image1:UIImage , secondImage:UIImage ) ->UIImage
{
UIGraphicsBeginImageContext(self.frame.size)
image1.draw(in: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height) )
secondImage.draw(in: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height) )
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
}
//OverLap2Layer
extension UIImage {
func overLap2Layer(firstLayer:UIImage , secondLayer:UIImage ) -> UIImage {
UIGraphicsBeginImageContext(firstLayer.size)
firstLayer.draw(in: CGRect(x: 0, y: 0, width: firstLayer.size.width, height: firstLayer.size.height) )
secondLayer.draw(in: CGRect(x: 0, y: 0, width: firstLayer.size.width, height: firstLayer.size.height) )
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
}
First Path :-
Second Path :-
Final Result :-
Reference:-
Blend in Core Graphics ,
Creating Image
GitHub Demo
Finally a solution!!
Using https://github.com/adamwulf/ClippingBezier you can find the intersecting points. Then you can walk through the path, turning left if clockwise or vice-versa to stay on the outside. Then you can generate a new path using the sequence of points.
You can use the GPCPolygon, an Objective-C wrapper for GPC
-GPCPolygonSet*) initWithPolygons:(NSMutableArray*)points;
or
- (GPCPolygonSet*) unionWithPolygonSet:(GPCPolygonSet*)p2;
Related
Trying to add a magnifying-glass effect to a uiview, but the magnfifying glass is always black.
My code for the Magnifing glass view:
public override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
context.translateBy(x: self.frame.size.width/2, y: self.frame.size.height/2)
context.scaleBy(x: self.scale, y: self.scale)
self.viewToMagnify.layer.render(in: context)
}
And in the View-to-Magnify:
self.magnifiedView = MagnifiedView.init(frame: CGRect(x: 200, y: 100, width: 100, height: 100))
self.magnifiedView.viewToMagnify = self.view
self.view.addSubview(magnifiedView)
Edit:
I want to use it in an ARSession on the camera output. The code above works fine for a UIImageView. Updating the View of the camera output in real-time seems to make some problems.
How can I create a triangle UIImage? Here's how I'm doing it now, but it's not producing any image at all.
extension UIImage {
static func triangle(side: CGFloat, color: UIColor)->UIImage {
UIGraphicsBeginImageContextWithOptions(CGSize(width: side, height: side), false, 0)
let ctx = UIGraphicsGetCurrentContext()!
ctx.saveGState()
ctx.beginPath()
ctx.move(to: CGPoint(x: side / 2, y: 0))
ctx.move(to: CGPoint(x: side, y: side))
ctx.move(to: CGPoint(x: 0, y: side))
ctx.move(to: CGPoint(x: side / 2, y: 0))
ctx.closePath()
ctx.setFillColor(color.cgColor)
ctx.restoreGState()
let img = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return img
}
}
Your path does not contain any lines, so there's no region to fill. In addition you are not drawing the path.
Try something like this:
static func triangle(side: CGFloat, color: UIColor)->UIImage {
UIGraphicsBeginImageContextWithOptions(CGSize(width: side, height: side), false, 0)
let ctx = UIGraphicsGetCurrentContext()!
ctx.saveGState()
ctx.beginPath()
ctx.move(to: CGPoint(x: side / 2, y: 0))
//### Add lines
ctx.addLine(to: CGPoint(x: side, y: side))
ctx.addLine(to: CGPoint(x: 0, y: side))
//ctx.addLine(to: CGPoint(x: side / 2, y: 0)) //### path is automatically closed
ctx.closePath()
ctx.setFillColor(color.cgColor)
ctx.drawPath(using: .fill) //### draw the path
ctx.restoreGState()
let img = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return img
}
You might just use UIBezierPath and not use CoreGraphics at all. Furthermore, nowadays we’d use UIGraphicsImageRenderer:
extension UIImage {
static func triangle(side: CGFloat, color: UIColor) -> UIImage {
return UIGraphicsImageRenderer(size: CGSize(width: side, height: side)).image { _ in
let path = UIBezierPath()
path.move(to: CGPoint(x: side / 2, y: 0))
path.addLine(to: CGPoint(x: side, y: side))
path.addLine(to: CGPoint(x: 0, y: side))
path.close()
color.setFill()
path.fill()
}
}
}
For UIBezierPath version that still uses UIGraphicsBeginImageContextWithOptions, see previous revision of this answer.
OOPer told you about your lack of lines, and failure to actually draw the triangle.
In addition to that, I would suggest avoiding mucking around with graphics contexts. UIBezierPath makes it cleaner and easier to draw without having to worry about contexts. Also, you don't need to save and restore contexts since you are creating a temporary context and then immediately discarding it.
Here is a complete playground that draws your triangle (and fills and strokes the image rectangle so it's easier to see - you'd remove the stroke and probably fill the shape with UIColor.clear instead.)
import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
extension UIImage {
static func triangle(side: CGFloat, color: UIColor)->UIImage {
UIGraphicsBeginImageContextWithOptions(CGSize(width: side, height: side), false, 0)
//First fill the bounds with white
let rectPath = UIBezierPath(rect: CGRect(x:0, y: 0, width: side, height: side))
UIColor.white.setFill() //Use clear if you want your image to only contain the triangle.
rectPath.fill()
//Now build the triangle path
let path = UIBezierPath()
path.move(to: CGPoint(x: side / 2, y: 0))
path.addLine(to: CGPoint(x: side, y: side))
path.addLine(to: CGPoint(x: 0, y: side))
path.close() //Closing the path draws the final line
color.setFill()
path.fill()
//Stroke the outer path in gray so you can see the bounds - remove if desired
UIColor.gray.setStroke()
rectPath.stroke()
let img = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return img
}
}
let imageView = UIImageView(frame: CGRect(x:0, y: 0, width: 200, height: 200))
PlaygroundPage.current.liveView = imageView
imageView.image = UIImage.triangle(side: 200.0, color: UIColor.yellow)
I am using this code clear part of image when user drag touch on screen. I have multiple images in overwriting each other. Now clear to top most layer is working fine, so part of lower image is visible.
Now what i want to achieve is user select a close path and area of selected close path should be clear. User can select multiple layers and select any portion to cut.
eg if there are 8 images and user selects layer 6 to 8, then visible portion will be from layer 5 where user clear with touch.
func drawBrushOnLayer(fromPoint: CGPoint, toPoint: CGPoint , selected:[Int]) {
UIGraphicsBeginImageContext(DrawImage.frame.size)
var context = UIGraphicsGetCurrentContext()
DrawImage.image?.draw(in: CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height - 50))
context?.move(to: fromPoint)
context?.addLine(to: toPoint)
context?.setLineCap(.butt)
context?.setLineWidth(BrushSize)
context?.setBlendMode(.clear)
context?.setShouldAntialias(false)
UIColor.clear.set()
context?.strokePath()
DrawImage.image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
}
For now i am drawing white color on image but i need to clear slected area so lower image can be visible.
func drawFill(point : CGPoint) {
autoreleasepool{
UIGraphicsBeginImageContext(CGSize(width: self.view.frame.size.width, height: self.view.frame.size.height - 50))
DrawImage .draw(in: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height - 50))
UIColor.white.set()
BezierPath.addLine(to: point)
BezierPath.lineWidth = 2.0
BezierPath .close()
BezierPath.fill()
let context = UIGraphicsGetCurrentContext()
context?.addPath(lassoBezier.cgPath)
newImage = UIGraphicsGetImageFromCurrentImageContext()!
DrawImage.image = newImage
UIGraphicsEndImageContext()
}
}
func drawFill(point : CGPoint) {
autoreleasepool{
UIGraphicsBeginImageContext(CGSize(width: self.view.frame.size.width, height: self.view.frame.size.height - 50))
DrawImage .draw(in: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height - 50))
BezierPath.addLine(to: point)
BezierPath .close()
let context = UIGraphicsGetCurrentContext()
UIColor.clear.set()
context?.addPath(lassoBezier.cgPath)
context?.setLineCap(.square)
context?.setBlendMode(.clear)
context?.setShouldAntialias(false)
context?.fillPath()
newImage = UIGraphicsGetImageFromCurrentImageContext()!
DrawImage.image = newImage
UIGraphicsEndImageContext()
}
}
I'm currently working with custom markers on a scatter plot and found myself with an issue that results in CPTPlotSymbol created from a CGPath upside down and distorted.
I've tested the path-creating code in a playground and it works without issues, drawing the path with the correct shape and orientation.
Here's the path drawing code:
private func getOuterPathInRect(rect: CGRect) -> CGPath {
let circlePath: CGPath = {
let p = CGMutablePath()
let topHundred = CGRect(x: 0, y: 0, width: 100, height: 100)
p.addEllipse(in: topHundred)
return p
}()
let arrowPath: CGPath = {
let p = CGMutablePath()
p.move(to: CGPoint(x: rect.midX, y: rect.maxY - 5.0))
p.addLine(to: CGPoint(x: rect.midX - 7.5, y: rect.maxY - 15.0))
p.addLine(to: CGPoint(x: rect.midX + 7.5, y: rect.maxY - 15.0))
return p
}()
let path = CGMutablePath()
path.addPath(circlePath)
path.addPath(arrowPath)
return path
}
And the code that creates the CPTPlotSymbol is:
func symbol(for plot: CPTScatterPlot, record idx: UInt) -> CPTPlotSymbol? {
let index = Int(idx)
guard items[index].requiresMarker else { return nil }
let rect = CGRect(x: 0, y: 0, width: 120, height: 120)
let marker = BallMarkerView()
marker.contentMode = .center
let path = marker.pathIn(rect: rect)
let symbol = CPTPlotSymbol.customPlotSymbol(with: path)
symbol.size = rect.size
symbol.fill = CPTFill(color: CPTColor.red())
return symbol
}
My goal was to use a custom UIView as a marker, but I couldn't find an API to do so, so I resorted to providing a path-based marker and fill it with an image representation of the marker.
Is this the proper way of doing it?
Why is my path being drawn distorted and upside down? The path being upside down could be explained by the difference in the coordinate system between UIKit and CoreGraphics, but that doesn't explain the distorsion.
Thanks!
Because Core Plot shares drawing code between the Mac and iOS, it uses the same drawing coordinate system on both platforms where (0, 0) is the lower-left corner of the drawing canvas. This is flipped from the normal drawing coordinate system on iOS.
I am trying to port an artificial horizon app I wrote for a PC in c# to swift. It has a bezel image which does not move and behind it is a horizon image which can move up and down behind the bezel. The "window" part of the bezel is yellow so in c# I just made the yellow opaque.
In swift I stated with the horizon inside of a UIScrollView but I'm not sure how to get that to work with a second image that should not scroll.
Not all that up to speed on this swift stuff, can someone point me in the right direction?
let view: UIView = UIView.init(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
let scrollView = UIScrollView.init(frame: view.bounds)
view.addSubview(scrollView)
let backImage: UIImage = fromColor(UIColor.redColor(), size: CGSize(width: 1000, height: 1000))
let backImageView: UIImageView = UIImageView.init(image: backImage)
scrollView.addSubview(backImageView)
scrollView.contentSize = CGSize.init(width: backImage.size.width, height: backImage.size.height)
let frontImage: UIImage = fromColor(UIColor.blueColor(), size: CGSize(width: 100, height: 100))
let layer: CALayer = CALayer.init()
layer.frame = CGRect.init(x: view.center.x - 50, y: view.center.y - 50, width: 100, height: 100)
layer.contents = frontImage.CGImage
view.layer.addSublayer(layer)
func fromColor(color: UIColor, size: CGSize) -> UIImage {
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()
CGContextSetFillColorWithColor(context, color.CGColor)
CGContextFillRect(context, rect)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}
fromColor is a helper method.
Result of the code