Related
I simply do the following in code:
let path = UIBezierPath(rect: blurView.bounds)
path.usesEvenOddFillRule = true
path.append(UIBezierPath(rect: CGRect(x: 100, y: 100, width: 100, height: 100)))
path.append(UIBezierPath(rect: CGRect(x: 150, y: 150, width: 100, height: 100)))
//here you can add more paths, but the number is not known
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.fillRule = .evenOdd
blurView.layer.mask = layer
and the effect is following:
Two rectangles overlapping one another. But all I need is to combine area from both rectanges, not to exclude everlapping area. Is it possible?
Using the "even-odd" fill rule is great for "cutting a hole" in a path. However, this code:
// create a big rect
let path = UIBezierPath(rect: blurView.bounds)
// cut a hole in it
path.append(UIBezierPath(rect: CGRect(x: 100, y: 100, width: 100, height: 100)))
// cut a hole overlapping a hole?
path.append(UIBezierPath(rect: CGRect(x: 150, y: 150, width: 100, height: 100)))
will be, as you've seen, problematic.
Depending on what all you are wanting to do, you could use a library such as ClippingBezier which allows you to manipulate paths with boolean actions.
Or, you can use a custom CALayer like this to "invert" multiple paths to use as a "cutout mask":
class BasicCutoutLayer: CALayer {
var rects: [CGRect] = []
func addRect(_ newRect: CGRect) {
rects.append(newRect)
setNeedsDisplay()
}
func reset() {
rects = []
setNeedsDisplay()
}
override func draw(in ctx: CGContext) {
// fill entire layer with solid color
ctx.setFillColor(UIColor.gray.cgColor)
ctx.fill(self.bounds);
rects.forEach { r in
ctx.addPath(UIBezierPath(rect: r).cgPath)
}
// draw clear "cutouts"
ctx.setFillColor(UIColor.clear.cgColor)
ctx.setBlendMode(.sourceIn)
ctx.drawPath(using: .fill)
}
}
To show it in use, we'll use this image:
In a standard UIImageView, overlaid with a blur UIVisualEffectView, and then use the BasicCutoutLayer class with two overlapping rects as the blur view's layer mask:
class BasicCutoutVC: UIViewController {
let myBlurView = UIVisualEffectView()
let myCutoutLayer = BasicCutoutLayer()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
let imgView = UIImageView()
if let img = UIImage(named: "sampleBG") {
imgView.image = img
}
[imgView, myBlurView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
imgView.topAnchor.constraint(equalTo: g.topAnchor),
imgView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
imgView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
imgView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
myBlurView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
myBlurView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
myBlurView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
myBlurView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
myBlurView.effect = UIBlurEffect(style: .extraLight)
// set mask for blur view
myBlurView.layer.mask = myCutoutLayer
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// set mask layer frame
myCutoutLayer.frame = myBlurView.bounds
// add two overlapping rects
let v: CGFloat = 160
let c: CGPoint = CGPoint(x: myBlurView.bounds.midX, y: myBlurView.bounds.midY)
var r: CGRect = CGRect(origin: c, size: CGSize(width: v, height: v))
r.origin.x -= v * 0.75
r.origin.y -= v * 0.75
myCutoutLayer.addRect(r)
r.origin.x += v * 0.5
r.origin.y += v * 0.5
myCutoutLayer.addRect(r)
}
}
Before applying the mask, it looks like this:
after applying the mask we get:
As we see, the "overlap" displays as we want.
That was a very simple, basic example. For a more advanced example, take a look at this:
struct MyPath {
var lineWidth: CGFloat = 0
var lineCap: CGLineCap = .butt
var lineJoin: CGLineJoin = .bevel
var isStroked: Bool = true
var isFilled: Bool = true
var pth: UIBezierPath = UIBezierPath()
}
class AdvancedCutoutLayer: CALayer {
var myPaths: [MyPath] = []
func addPath(_ newPath: MyPath) {
myPaths.append(newPath)
setNeedsDisplay()
}
func reset() {
myPaths = []
setNeedsDisplay()
}
override func draw(in ctx: CGContext) {
// fill entire layer with solid color
ctx.setFillColor(UIColor.gray.cgColor)
ctx.fill(self.bounds);
ctx.setBlendMode(.sourceIn)
myPaths.forEach { thisPath in
ctx.setStrokeColor(thisPath.isStroked ? UIColor.clear.cgColor : UIColor.black.cgColor)
ctx.setFillColor(thisPath.isFilled ? UIColor.clear.cgColor : UIColor.black.cgColor)
ctx.setLineWidth(thisPath.isStroked ? thisPath.lineWidth : 0.0)
ctx.setLineCap(thisPath.lineCap)
ctx.setLineJoin(thisPath.lineJoin)
ctx.addPath(thisPath.pth.cgPath)
ctx.drawPath(using: .fillStroke)
}
}
}
along with a subclassed UIVisualEffectView for convenience:
class CutoutBlurView: UIVisualEffectView {
let sl = AdvancedCutoutLayer()
override init(effect: UIVisualEffect?) {
super.init(effect: effect)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
sl.isOpaque = false
layer.mask = sl
}
override func layoutSubviews() {
super.layoutSubviews()
sl.frame = bounds
sl.setNeedsDisplay()
}
func addPath(_ newPath: MyPath) {
sl.addPath(newPath)
}
func reset() {
sl.reset()
}
}
and an example controller:
class AdvancedCutoutVC: UIViewController {
let myView = CutoutBlurView()
var idx: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
let imgView = UIImageView()
if let img = UIImage(named: "sampleBG") {
imgView.image = img
}
[imgView, myView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
imgView.topAnchor.constraint(equalTo: g.topAnchor),
imgView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
imgView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
imgView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
myView.topAnchor.constraint(equalTo: g.topAnchor),
myView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
myView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
myView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
myView.effect = UIBlurEffect(style: .extraLight)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true, block: { _ in
switch self.idx % 4 {
case 1:
self.addSomeOvals()
case 2:
self.addSomeLines()
case 3:
self.addSomeShapes()
default:
self.addSomeRects()
}
self.idx += 1
})
}
func addSomeRects() {
myView.reset()
let w: CGFloat = myView.frame.width / 4.0
let h: CGFloat = myView.frame.height / 4.0
var x: CGFloat = ((myView.frame.width - (w * 5.0 * 0.5)) * 0.5) - (w * 0.25)
var y: CGFloat = ((myView.frame.height - (h * 5.0 * 0.5)) * 0.5) - (h * 0.25)
for _ in 1...5 {
let bz = UIBezierPath(rect: CGRect(x: x, y: y, width: w, height: h))
myView.addPath(MyPath(lineWidth: 0, isStroked: false, isFilled: true, pth: bz))
x += w * 0.5
y += h * 0.5
}
}
func addSomeOvals() {
myView.reset()
let w: CGFloat = myView.frame.width / 4.0
let h: CGFloat = myView.frame.height / 4.0
var x: CGFloat = ((myView.frame.width - (w * 5.0 * 0.5)) * 0.5) - (w * 0.25)
var y: CGFloat = ((myView.frame.height - (h * 5.0 * 0.5)) * 0.5) - (h * 0.25)
for _ in 1...5 {
let bz = UIBezierPath(ovalIn: CGRect(x: x, y: y, width: w, height: h))
myView.addPath(MyPath(lineWidth: 0, isStroked: false, isFilled: true, pth: bz))
x += w * 0.5
y += h * 0.5
}
}
func addSomeLines() {
myView.reset()
let w: CGFloat = myView.frame.width / 2.0
let h: CGFloat = myView.frame.height / 4.0
let x: CGFloat = 80
var y: CGFloat = 80
var lw: CGFloat = 4
for _ in 1...5 {
let bz = UIBezierPath()
bz.move(to: CGPoint(x: x, y: y))
bz.addLine(to: CGPoint(x: x + w, y: y + 20))
myView.addPath(MyPath(lineWidth: lw, lineCap: .round, isStroked: true, isFilled: false, pth: bz))
y += h * 0.5
lw += 10
}
}
func addSomeShapes() {
myView.reset()
var bz: UIBezierPath!
bz = UIBezierPath(rect: CGRect(x: 80, y: 80, width: 80, height: 120))
myView.addPath(MyPath(isStroked: false, isFilled: true, pth: bz))
bz = UIBezierPath(rect: CGRect(x: 120, y: 120, width: 120, height: 60))
myView.addPath(MyPath(isStroked: false, isFilled: true, pth: bz))
bz = UIBezierPath(rect: CGRect(x: 80, y: 220, width: 220, height: 60))
myView.addPath(MyPath(lineWidth: 12, isStroked: true, isFilled: false, pth: bz))
bz = UIBezierPath(ovalIn: CGRect(x: 100, y: 240, width: 220, height: 60))
myView.addPath(MyPath(lineWidth: 12, isStroked: true, isFilled: false, pth: bz))
var r: CGRect = CGRect(x: 40, y: 320, width: myView.frame.width - 80, height: 200)
for _ in 1...4 {
bz = UIBezierPath(rect: r)
myView.addPath(MyPath(lineWidth: 8, isStroked: true, isFilled: false, pth: bz))
r = r.insetBy(dx: 20, dy: 20)
}
}
}
When run, this example will cycle through overlapping rect, overlapping ovals, some varying width lines, and some assorted shapes (just to give an idea):
I would go with ClippingBezier because it is fast, easy to use and neat. It'll be something like this:
let rect1 = CGRect(x: 100, y: 100, width: 200, height: 200)
let rect2 = CGRect(x: 150, y: 200, width: 200, height: 200)
let path0 = UIBezierPath(rect: blurView.bounds)
let path1 = UIBezierPath(rect: rect1)
let path2 = UIBezierPath(rect: rect2)
let unionPathArray = path1.union(with: path2)
let unionPath = UIBezierPath()
if let array = unionPathArray {
array.forEach(unionPath.append)
path0.append(unionPath.reversing())
let layerUnion = CAShapeLayer()
layerUnion.path = path0.cgPath
blurView.layer.mask = layerUnion
}
Output:
EDIT
It appears that this method doesn't work properly when using UIBezierPath(roundedRect:cornerRadius:). To overcome that, here is how we can construct our own func to do that:
extension UIBezierPath {
convenience init(rectangleIn rect: CGRect, cornerRadius: CGFloat) {
self.init()
move(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius))
addArc(withCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + cornerRadius), radius: cornerRadius, startAngle: .pi, endAngle: 3.0 * .pi / 2.0, clockwise: true)
addLine(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY))
addArc(withCenter: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY + cornerRadius), radius: cornerRadius, startAngle: 3.0 * .pi / 2.0, endAngle: 2 * .pi, clockwise: true)
addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadius))
addArc(withCenter: CGPoint(x: rect.maxX - cornerRadius, y: rect.maxY - cornerRadius), radius: cornerRadius, startAngle: 0.0, endAngle: .pi / 2.0, clockwise: true)
addLine(to: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY))
addArc(withCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY - cornerRadius), radius: cornerRadius, startAngle: .pi / 2.0, endAngle: .pi, clockwise: true)
//addLine(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius))
close()
}
}
We can also extend the above-mentioned solution to multiple paths. Here is one way to create the union of multiple paths:
extension UIBezierPath {
class func getUnion(of paths: [UIBezierPath]) -> UIBezierPath {
var result = UIBezierPath()
paths.forEach { subPath in
guard let union = result.union(with: subPath) else { return }
let unionCombined = UIBezierPath()
union.forEach(unionCombined.append)
result = unionCombined
}
return result
}
}
Here is an example:
let rect1 = CGRect(x: 100, y: 100, width: 200, height: 180)
let rect2 = CGRect(x: 150, y: 200, width: 200, height: 200)
let rect3 = CGRect(x: 150, y: 500, width: 100, height: 100)
let rect4 = CGRect(x: 150, y: 800, width: 300, height: 100)
let pathBase = UIBezierPath(rect: blurView.bounds)
let path1 = UIBezierPath(rectangleIn: rect1, cornerRadius: 20.0)
let path2 = UIBezierPath(rect: rect2)
let path3 = UIBezierPath(ovalIn: rect3)
let path4 = UIBezierPath(ovalIn: rect4)
let union = UIBezierPath.getUnion(of: [path1, path2, path3, path4])
pathBase.append(union.reversing())
let layerUnion = CAShapeLayer()
layerUnion.path = pathBase.cgPath
blurView.layer.mask = layerUnion
And the output:
I am trying to understand how to achieve kind of circle with more than ten control points like in this video, which can be adjusted to any shape and implemented in swift language.
I have found javascript similar effects, but I don’t know how to start. I also tried to use the Bezier path implementation, the code is as follows, but I don't know how to complete it.
class MyBezierPathView: UIView {
private var path: UIBezierPath?
// start point
var startP = CGPoint.zero
// end point
var endP = CGPoint.zero
// control point
var controlP = CGPoint.zero
var pathColor: UIColor?
var pathWidth: CGFloat = 0.0
// current touch point
private var currentTouchP = 0
// init
override init(frame: CGRect) {
super.init(frame: frame)
}
// draw BezierPath
override func draw(_ rect: CGRect) {
path = UIBezierPath()
path?.move(to: startP)
path?.addQuadCurve(to: endP, controlPoint: controlP)
path?.lineWidth = pathWidth
pathColor?.setStroke()
path?.stroke()
path = UIBezierPath()
path?.lineWidth = 1
UIColor.gray.setStroke()
let lengths: [CGFloat] = [5]
path?.setLineDash(lengths, count: 1, phase: 1)
path?.move(to: controlP)
path?.addLine(to: startP)
path?.stroke()
path?.move(to: controlP)
path?.addLine(to: endP)
path?.stroke()
path = UIBezierPath(arcCenter: startP, radius: 4, startAngle: 0, endAngle: .pi * 2, clockwise: true)
UIColor.black.setStroke()
path?.fill()
path = UIBezierPath(arcCenter: endP, radius: 4, startAngle: 0, endAngle: .pi * 2, clockwise: true)
UIColor.black.setStroke()
path?.fill()
path = UIBezierPath(arcCenter: controlP, radius: 3, startAngle: 0, endAngle: .pi * 2, clockwise: true)
path?.lineWidth = 2
UIColor.black.setStroke()
path?.stroke()
let startMsgRect = CGRect(x: startP.x + 8, y: startP.y - 7, width: 50, height: 20)
"start point".draw(in: startMsgRect, withAttributes: nil)
let endMsgRect = CGRect(x: endP.x + 8, y: endP.y - 7, width: 50, height: 20)
"end point".draw(in: endMsgRect, withAttributes: nil)
let control1MsgRect = CGRect(x: controlP.x + 8, y: controlP.y - 7, width: 50, height: 20)
"control point".draw(in: control1MsgRect, withAttributes: nil)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let startPoint = touches.first?.location(in: self)
let startR = CGRect(x: startP.x - 4, y: startP.y - 4, width: 80, height: 80)
let endR = CGRect(x: endP.x - 4, y: endP.y - 4, width: 80, height: 80)
let controlR = CGRect(x: controlP.x - 4, y: controlP.y - 4, width: 80, height: 80)
guard let startPoint = startPoint else {
print("startPoint is nil.")
return
}
if startR.contains(startPoint) {
currentTouchP = 1
} else if endR.contains(startPoint) {
currentTouchP = 2
} else if controlR.contains(startPoint) {
currentTouchP = 3
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
var touchPoint = touches.first?.location(in: self)
if touchPoint!.x < 0 {
touchPoint!.x = 0
}
if touchPoint!.x > bounds.size.width {
touchPoint!.x = bounds.size.width
}
if touchPoint!.y < 0 {
touchPoint!.y = 0
}
if touchPoint!.y > bounds.size.height {
touchPoint!.y = bounds.size.height
}
switch currentTouchP {
case 1:
startP = touchPoint!
case 2:
endP = touchPoint!
case 3:
controlP = touchPoint!
default:
break
}
setNeedsDisplay()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
currentTouchP = 0
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
touchesEnded(touches, with: event)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let frame = CGRect(x: 0, y: 0, width: view.bounds.size.width, height: view.bounds.size.height)
let pathView = MyBezierPathView(frame: frame)
pathView.startP = CGPoint(x: 110, y: 150)
pathView.endP = CGPoint(x: 258.47, y: 211.53)
pathView.controlP = CGPoint(x: 196.94, y: 150)
pathView.pathColor = #colorLiteral(red: 1, green: 0.1491314173, blue: 0, alpha: 1)
pathView.pathWidth = 2
pathView.backgroundColor = UIColor.white
pathView.layer.borderWidth = 1
view.addSubview(pathView)
}
}
One way you could approach this problem
create a circle from several curve segments (using addQuadCurve() or addCurve() of UIBezierPath class)
addQuadCurve() adds a curve with one control point while addCurve() adds a curve with 2 control points (the video you showed seems using paths with 2 control points, so it would be better using addCurve())
Then user needs to be able to move any of start/end and control points of these curves.
For each these change, you have to redraw the curves
I have created a sample playground with this idea. In this playground, I have created a red circle (not a perfect circle) by four curves using addQuadCurve(). This circle has 8 points you could use to alter the shape. If you use 4 curves with addCurve(), then you will have 12 points to alter the shape.
Then I changed a single point of the red circle and added the updated shape in green color below the original red circle.
import UIKit
import PlaygroundSupport
let container = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 700))
let view1 = UIView(frame: CGRect(x: 50, y: 50, width: 500, height: 350))
let layer1 = CAShapeLayer()
layer1.fillColor = UIColor.clear.cgColor
layer1.lineWidth = 5
layer1.strokeColor = UIColor.red.cgColor
//create a circle wich has 8 points to change it's shape (4 control points and 4 start/end points of curves)
let originalPath = UIBezierPath()
originalPath.move(to: CGPoint(x: 100, y: 0))
originalPath.addQuadCurve(to: CGPoint(x: 200, y: 100), controlPoint: CGPoint(x: 190, y: 10))
originalPath.addQuadCurve(to: CGPoint(x: 100, y: 200), controlPoint: CGPoint(x: 190, y: 190))
originalPath.addQuadCurve(to: CGPoint(x: 0, y: 100), controlPoint: CGPoint(x: 10, y: 190))
originalPath.addQuadCurve(to: CGPoint(x: 100, y: 0), controlPoint: CGPoint(x: 10, y: 10))
//add this path to the layer1
layer1.path = originalPath.cgPath
//suppose user move the CGPoint(x: 200, y: 100) to CGPoint(x: 220, y: 100)
//then we can redraw the 4 curves again
let view2 = UIView(frame: CGRect(x: 50, y: 350, width: 500, height: 350))
let layer2 = CAShapeLayer()
layer2.fillColor = UIColor.clear.cgColor
layer2.lineWidth = 5
layer2.strokeColor = UIColor.green.cgColor
//changedPath is almost same as originalPath except CGPoint(x: 250, y: 100)
let changedPath = UIBezierPath()
changedPath.move(to: CGPoint(x: 100, y: 0))
changedPath.addQuadCurve(to: CGPoint(x: 250, y: 100), controlPoint: CGPoint(x: 190, y: 10)) // <---- user has moved point CGPoint(x: 200, y: 100) to CGPoint(x: 250, y: 100). So add this curve to the new point
changedPath.addQuadCurve(to: CGPoint(x: 100, y: 200), controlPoint: CGPoint(x: 190, y: 190))
changedPath.addQuadCurve(to: CGPoint(x: 0, y: 100), controlPoint: CGPoint(x: 10, y: 190))
changedPath.addQuadCurve(to: CGPoint(x: 100, y: 0), controlPoint: CGPoint(x: 10, y: 10))
//adding changed path to layer2
layer2.path = changedPath.cgPath
view1.layer.addSublayer(layer1)
view2.layer.addSublayer(layer2)
container.addSubview(view1)
container.addSubview(view2)
PlaygroundPage.current.liveView = container
I have created UIBezierPath with custom shape then I need to make it mask for image always I got empty image
here is my code
First I created the path, then create image and last create my mask but it is not working
here is image I need to mask it dropbox.com/s/tnxgx7g1uvb1zj7/TeethMask.png?dl=0 here is UIBazier path dropbox.com/s/nz93n1vgvj6c6y0/… I need to mask this image in this path
The output is something like this
https://www.dropbox.com/s/gueyhdmmdcfvyiq/image.png?dl=0
Here is ViewController class
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let tapGR = UITapGestureRecognizer(target: self, action: #selector(didTap))
self.view.addGestureRecognizer(tapGR)
}
#objc func didTap(tapGR: UITapGestureRecognizer) {
let tapPoint = tapGR.location(in: self.view)
if #available(iOS 11.0, *) {
let shapeView = ShapeView(origin: tapPoint)
self.view.addSubview(shapeView)
} else {
// Fallback on earlier versions
}
}
}
Here is ShapeView class
import UIKit
#available(iOS 11.0, *)
class ShapeView: UIView {
let size: CGFloat = 150
let lineWidth: CGFloat = 3
var fillColor: UIColor!
var path: UIBezierPath!
init(origin: CGPoint) {
super.init(frame: CGRect(x: 0.0, y: 0.0, width: size, height: size))
self.fillColor = randomColor()
self.path = mouthPath()
self.center = origin
self.backgroundColor = UIColor.clear
}
func randomColor() -> UIColor {
let hue:CGFloat = CGFloat(Float(arc4random()) / Float(UINT32_MAX))
return UIColor(hue: hue, saturation: 0.8, brightness: 1.0, alpha: 0.8)
}
func mouthPath() -> UIBezierPath{
let pointsArray = [CGPoint(x:36 , y:36 ),CGPoint(x:41 , y:36 ),CGPoint(x:45 , y:36 ),CGPoint(x:49 , y:36 ),CGPoint(x:53 , y:36 ),CGPoint(x:58 , y: 37),CGPoint(x:64 , y:37 ),CGPoint(x:69 , y:36 ),CGPoint(x:65 , y:29 ),CGPoint(x:58 , y:24 ),CGPoint(x:50 , y:22 ),CGPoint(x:42 , y:23 ),CGPoint(x:36 , y:28 ),CGPoint(x:32 , y:35 )]
let newPath = UIBezierPath()
let factor:CGFloat = 10
for i in 0...pointsArray.count - 1 { // last point is 0,0
let point = pointsArray[i]
let currentPoint1 = CGPoint(x: point.x * factor , y: point.y * factor)
if i == 0 {
newPath.move(to: currentPoint1)
} else {
newPath.addLine(to: currentPoint1)
}
}
newPath.addLine(to: CGPoint(x: pointsArray[0].x * factor, y: pointsArray[0].y * factor))
newPath.close()
let imageTemplate = UIImageView()
imageTemplate.image = UIImage(named: "TeethMask")
self.addSubview(imageTemplate)
self.bringSubviewToFront(imageTemplate)
imageTemplate.frame = self.frame
let mask = CAShapeLayer(layer: self.layer)
mask.frame = newPath.bounds
mask.fillColor = UIColor.clear.cgColor
mask.strokeColor = UIColor.black.cgColor
mask.path = newPath.cgPath
mask.shouldRasterize = true
imageTemplate.layer.mask = mask
imageTemplate.layer.addSublayer(mask)
}
}
Well, you're doing a few things wrong...
The "teeth" image you linked:
has a native size of 461 x 259. So, I'm going to use a proportional "target" size of 200 x 112.
First, shape layers use 0,0 at upper-left. Your original points array:
let pointsArray = [
CGPoint(x: 36, y: 36),
CGPoint(x: 41, y: 36),
CGPoint(x: 45, y: 36),
CGPoint(x: 49, y: 36),
CGPoint(x: 53, y: 36),
CGPoint(x: 58, y: 37),
CGPoint(x: 64, y: 37),
CGPoint(x: 69, y: 36),
CGPoint(x: 65, y: 29),
CGPoint(x: 58, y: 24),
CGPoint(x: 50, y: 22),
CGPoint(x: 42, y: 23),
CGPoint(x: 36, y: 28),
CGPoint(x: 32, y: 35),
]
gives this shape:
If we invert the y-coordinates:
let pointsArray = [
CGPoint(x: 36.0, y: 23.0),
CGPoint(x: 41.0, y: 23.0),
CGPoint(x: 45.0, y: 23.0),
CGPoint(x: 49.0, y: 23.0),
CGPoint(x: 53.0, y: 23.0),
CGPoint(x: 58.0, y: 22.0),
CGPoint(x: 64.0, y: 22.0),
CGPoint(x: 69.0, y: 23.0),
CGPoint(x: 65.0, y: 30.0),
CGPoint(x: 58.0, y: 35.0),
CGPoint(x: 50.0, y: 37.0),
CGPoint(x: 42.0, y: 36.0),
CGPoint(x: 36.0, y: 31.0),
CGPoint(x: 32.0, y: 24.0),
]
we get this shape:
It will be difficult to get things to "line up" correctly if your shape is offset like that, so we can "normalize" the points to start at top-left:
let pointsArray: [CGPoint] = [
CGPoint(x: 4.0, y: 1.0),
CGPoint(x: 9.0, y: 1.0),
CGPoint(x: 13.0, y: 1.0),
CGPoint(x: 17.0, y: 1.0),
CGPoint(x: 21.0, y: 1.0),
CGPoint(x: 26.0, y: 0.0),
CGPoint(x: 32.0, y: 0.0),
CGPoint(x: 37.0, y: 1.0),
CGPoint(x: 33.0, y: 8.0),
CGPoint(x: 26.0, y: 13.0),
CGPoint(x: 18.0, y: 15.0),
CGPoint(x: 10.0, y: 14.0),
CGPoint(x: 4.0, y: 9.0),
CGPoint(x: 0.0, y: 2.0),
]
resulting in:
However, we want the shape to fit the image, so we can scale the UIBezierPath to the bounds of the imageView:
// need to scale the path to self.bounds
let scaleW = bounds.width / pth.bounds.width
let scaleH = bounds.height / pth.bounds.height
let trans = CGAffineTransform(scaleX: scaleW, y: scaleH)
pth.apply(trans)
and we're here:
The only thing left is to use that as a mask for the image.
I'm going to suggest subclassing UIImageView instead of UIView ... that way you can set the .image property without needing to add another view as a subview. Also, I think you'll find it much easier to manage the size of the custom, masked image in your controller code, rather than inside the custom class.
Here is a demo view controller and a custom MouthShapeView:
class TeethViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let tapGR = UITapGestureRecognizer(target: self, action: #selector(didTap))
self.view.addGestureRecognizer(tapGR)
}
#objc func didTap(tapGR: UITapGestureRecognizer) {
let tapPoint = tapGR.location(in: self.view)
if #available(iOS 11.0, *) {
// make sure you can load the image
if let img = UIImage(named: "TeethMask") {
// create custom ShapeView with image
let shapeView = MouthShapeView(image: img)
// if you want to use original image proportions
// set the width you want and calculate a proportional height
// based on the original image size
let targetWidth: CGFloat = 200.0
let targetHeight: CGFloat = img.size.height / img.size.width * targetWidth
// set the frame size
shapeView.frame.size = CGSize(width: targetWidth, height: targetHeight)
// set the frame center
shapeView.center = tapPoint
// add it
self.view.addSubview(shapeView)
}
} else {
// Fallback on earlier versions
}
}
}
#available(iOS 11.0, *) class MouthShapeView: UIImageView {
let pointsArray: [CGPoint] = [
CGPoint(x: 4.0, y: 1.0),
CGPoint(x: 9.0, y: 1.0),
CGPoint(x: 13.0, y: 1.0),
CGPoint(x: 17.0, y: 1.0),
CGPoint(x: 21.0, y: 1.0),
CGPoint(x: 26.0, y: 0.0),
CGPoint(x: 32.0, y: 0.0),
CGPoint(x: 37.0, y: 1.0),
CGPoint(x: 33.0, y: 8.0),
CGPoint(x: 26.0, y: 13.0),
CGPoint(x: 18.0, y: 15.0),
CGPoint(x: 10.0, y: 14.0),
CGPoint(x: 4.0, y: 9.0),
CGPoint(x: 0.0, y: 2.0),
]
let maskLayer = CAShapeLayer()
override init(image: UIImage?) {
super.init(image: image)
maskLayer.fillColor = UIColor.black.cgColor
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let newPath = UIBezierPath()
pointsArray.forEach { p in
if p == pointsArray.first {
newPath.move(to: p)
} else {
newPath.addLine(to: p)
}
}
newPath.close()
// need to scale the path to self.bounds
let scaleW = bounds.width / newPath.bounds.width
let scaleH = bounds.height / newPath.bounds.height
let trans = CGAffineTransform(scaleX: scaleW, y: scaleH)
newPath.apply(trans)
maskLayer.path = newPath.cgPath
layer.mask = maskLayer
}
}
When you run that code, and tap on the view, you'll get this:
I have created multiple circles (points) using CAShapeLayer and multiple lines connecting to those circles. Then i animate those circles. what i want to do is animate connected lines along with dots. But only circles are animating not the lines. How can i achieve this? is there a way to update lines position?`
`
var points: [CAShapeLayer] = []
let positions: [CGPoint] = [CGPoint(x: 70, y: 100),
CGPoint(x: 140, y: 100),
CGPoint(x: 210, y: 100),
CGPoint(x: 70, y: 200),
CGPoint(x: 140, y: 200),
CGPoint(x: 210, y: 200)]
var lines:[CAShapeLayer] = []
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
createPoints()
}
override func viewDidLoad() {
super.viewDidLoad()
}
func createPoints(){
for (index,position) in positions.enumerated() {
let point = CAShapeLayer()
point.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 10, height: 10), cornerRadius: 5).cgPath
point.frame = CGRect(x: position.x, y: position.y, width: 10, height: 10)
point.fillColor = UIColor.red.cgColor
points.append(point)
view.layer.addSublayer(points[index])
}
animatePosition(point: points[0])
drawLines()
}
func drawLines(){
for (index,_) in positions.enumerated() {
let path = UIBezierPath()
//Not completed
//set line's start position to dots position
path.move(to: points[index].position)
//set line's end position to dots position
path.addLine(to: points[index+1].position)
let line = CAShapeLayer()
line.fillColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0).cgColor
line.strokeColor = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1).cgColor
line.lineWidth = 1
line.strokeStart = 0
line.path = path.cgPath
lines.append(line)
view.layer.addSublayer(line)
}
}
func animatePosition(point: CAShapeLayer) {
let animation = CABasicAnimation(keyPath: "position")
animation.fromValue = [10, 10 ]
animation.toValue = [100, 100]
animation.autoreverses = true
animation.repeatCount = .infinity
animation.isAdditive = true
point.add(animation, forKey: "pointStart")
// line.add(animation, forKey: "positionLine")
self.view.setNeedsLayout()
}
What i want is some thing like this:
I am trying to give some mask at bottom view using UIBezierPath with addCurve() method.
Expected Result
Current Output
What I have tried is:
viewHeader.backgroundColor = headerColor
let path = UIBezierPath()
let cpoint1 = CGPoint(x: 80, y: 160)
let cpoint2 = CGPoint(x: 280, y: viewHeader.Height + 50)
path.move(to:.init(x: 0, y: viewHeader.Height + 10))
path.addCurve(to: .init(x: viewHeader.Width, y: 205), controlPoint1: cpoint1, controlPoint2: cpoint2)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.reversing().cgPath
shapeLayer.fillColor = headerColor?.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 1.0
viewHeader.layer.addSublayer(shapeLayer)
I hope it ll work for you, enjoy.
func drawLine() {
let headerColor = UIColor.red
let path = UIBezierPath()
let cpoint1 = CGPoint(x: viewHeader.frame.width/4, y: 160)
let cpoint2 = CGPoint(x: viewHeader.frame.width*3/4, y: viewHeader.frame.height + 50)
path.move(to:.init(x: 0, y: 0))
path.addLine(to:.init(x: 0, y: viewHeader.frame.height - 50))
path.addCurve(to: .init(x: viewHeader.frame.width, y: viewHeader.frame.height - 50), controlPoint1: cpoint1, controlPoint2: cpoint2)
path.addLine(to: CGPoint(x: viewHeader.frame.width, y: 0))
path.addLine(to: CGPoint(x: 0, y: 0))
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.reversing().cgPath
shapeLayer.fillColor = headerColor.cgColor
shapeLayer.strokeColor = headerColor.cgColor
shapeLayer.lineWidth = 1.0
viewHeader.layer.addSublayer(shapeLayer)
}