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
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 want to make this shape in swift .
As you can see, the tabbar has a raised center button. However, this is not the only thing as there should be a real hole in the tabbar so that it is transparent there.
How can I create such a hole inside a tabbar? And then put a raised, round button in that hole?
I would gladly appreciate any help regarding my question.
i am trying but cannot achieve the above result.
import Foundation
import UIKit
#IBDesignable
class AppTabBar: UITabBar {
private var shapeLayer: CALayer?
override func draw(_ rect: CGRect) {
self.addShape()
}
private func addShape() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = createPath()
shapeLayer.strokeColor = UIColor.lightGray.cgColor
shapeLayer.fillColor = #colorLiteral(red: 0.9782002568, green: 0.9782230258, blue: 0.9782107472, alpha: 1)
shapeLayer.lineWidth = 0.5
// The below 4 lines are for shadow above the bar. you can skip them if you do not want a shadow
shapeLayer.shadowOffset = CGSize(width:0, height:0)
shapeLayer.shadowRadius = 10
shapeLayer.shadowColor = UIColor.gray.cgColor
shapeLayer.shadowOpacity = 0.3
if let oldShapeLayer = self.shapeLayer {
self.layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.shapeLayer = shapeLayer
}
func createPath() -> CGPath {
let height2: CGFloat = self.frame.height
let height: CGFloat = 86.0
let path = UIBezierPath()
let centerWidth = self.frame.width / 2
let startXpoint = centerWidth - height + 57
let endXpoint = (centerWidth + height - 45)
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: startXpoint , y: 0))
// path.addCurve(to: CGPoint(x: centerWidth, y: height - 40),
// controlPoint1: CGPoint(x: (centerWidth - 30), y: 0), controlPoint2: CGPoint(x: centerWidth - 35, y: height - 40))
path.addCurve(to: CGPoint(x: centerWidth, y: height / 1.6),
controlPoint1: CGPoint(x: startXpoint - 5, y: height2 / 3), controlPoint2: CGPoint(x: (centerWidth - 10), y: height2 / 1.6))
path.addCurve(to: CGPoint(x: (centerWidth + height / 2.9 ), y: 0),
controlPoint1: CGPoint(x: centerWidth + 35, y: height - 40), controlPoint2: CGPoint(x: (centerWidth + 30), y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
for member in subviews.reversed() {
let subPoint = member.convert(point, from: self)
guard let result = member.hitTest(subPoint, with: event) else { continue }
return result
}
return nil
}
}
extension UITabBar {
override open func sizeThatFits(_ size: CGSize) -> CGSize {
var sizeThatFits = super.sizeThatFits(size)
sizeThatFits.height = 74
return sizeThatFits
}
}
This code works when the frame's x and y are 0, but fails when using different x and y's:
class Triangle: UIView {
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
let startX = self.center.x
let startY: CGFloat = 0
path.move(to: CGPoint(x: startX, y: startY))
path.addLine(to: CGPoint(x: self.bounds.width, y: self.bounds.height))
path.addLine(to: CGPoint(x: 0, y: self.bounds.height))
path.close()
UIColor.green.setStroke()
path.stroke()
}
}
import UIKit
class ViewController: UIViewController {
#IBAction func animate(_ sender: UIButton) {
let triangleView = Triangle(frame: CGRect(x: 50, y: 50, width: 30, height: 30))
triangleView.backgroundColor = .clear
self.view.addSubview(triangleView)
}
}
This works:
let triangleView = Triangle(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
When failing it looks like this:
Well that is one ugly triangle. Why does it works with x: 0 and y:0 and fails when using different floats there? How can I fix this?
replace let startX = self.center.x with let startX = self. bounds.width / 2
I discovered that it's possible to render multiple polygons using a single SKShapeNode object:
class GameScene: SKScene {
override func didMove(to view: SKView) {
let polygons = [
[
CGPoint(x: 0, y: 0),
CGPoint(x: 100, y: 100),
CGPoint(x: 100, y: 0)
],
[
CGPoint(x: 50, y: 50),
CGPoint(x: 50, y: 150),
CGPoint(x: 150, y: 150),
CGPoint(x: 150, y: 50),
],
]
let path = CGMutablePath()
for points in polygons {
path.addLines(between: points)
path.closeSubpath()
}
let node = SKShapeNode(path: path)
node.fillColor = UIColor.red
addChild(node)
}
}
However, any space where polygons overlap is rendered as empty:
Is it possible to fill those empty spaces, while continuing to use a single node?
Here is a kluge workaround if you don't care about the border.
class GameScene: SKScene {
override func didMove(to view: SKView) {
let polygons = [
[
CGPoint(x: 0, y: 0),
CGPoint(x: 100, y: 100),
CGPoint(x: 100, y: 0)
],
[
CGPoint(x: 50, y: 50),
CGPoint(x: 50, y: 150),
CGPoint(x: 150, y: 150),
CGPoint(x: 150, y: 50),
]
]
let path = CGMutablePath()
for points in polygons {
path.addLines(between: points)
path.closeSubpath()
}
let first = CGMutablePath()
first.addLines(between: polygons[0]);
first.closeSubpath()
let second = CGMutablePath()
second.addLines(between: polygons[1]);
second.closeSubpath()
let node = SKShapeNode(path: first)
node.fillColor = .red
node.strokeColor = .red
let child = SKShapeNode(path: second)
child.fillColor = .red
child.strokeColor = .red
node.addChild(child)
addChild(node)
}
}
If you care about the border, then ...
class GameScene: SKScene {
override func didMove(to view: SKView) {
let polygons = [
[
CGPoint(x: 0, y: 0),
CGPoint(x: 100, y: 100),
CGPoint(x: 100, y: 0)
],
[
CGPoint(x: 50, y: 50),
CGPoint(x: 50, y: 150),
CGPoint(x: 150, y: 150),
CGPoint(x: 150, y: 50),
]
]
let path = CGMutablePath()
for points in polygons {
path.addLines(between: points)
path.closeSubpath()
}
let first = CGMutablePath()
first.addLines(between: polygons[0]);
first.closeSubpath()
let second = CGMutablePath()
second.addLines(between: polygons[1]);
second.closeSubpath()
let node = SKShapeNode(path: first)
node.fillColor = .red
node.strokeColor = .white
node.lineWidth = 2
let child = SKShapeNode(path: second)
child.fillColor = .red
child.strokeColor = .white
node.lineWidth = 2
node.addChild(child)
let child2 = SKShapeNode(path: first)
child2.fillColor = .red
child2.strokeColor = .clear
node.lineWidth = 2
node.addChild(child2)
addChild(node)
}
}
For example if you have an image of a trampoline, and a character jumping on it. Then you want to animate how the trampoline bends down in the center.
To do this I would have to take a bitmap and apply it to a densely tessellated OpenGL ES mesh. Then apply texture to it. Then deform the mesh.
Does SpriteKit support this or can it only display textures as-is?
With iOS 10, SKWarpGeometry has been added that allows you to deform sprites. Warps are defined using a source and destination grid containing eight control points, these points define the warp transform; Sprite Kit will take care of tessellation and other low level details.
You can set the wrap geometry for a sprite directly, or animate warping with SKActions.
SKAction.animate(withWarps: [SKWarpGeometry], times: [NSNumber]) -> SKAction?
SKAction.animate(withWarps: [SKWarpGeometry], times: [NSNumber], restore: Bool) -> SKAction?
SKAction.warp(to: SKWarpGeometry, duration: TimeInterval) -> SKAction?
UPDATE Nov 2016
On Xcode 8+, the following code snippet can be used in a playground: Drag the control points and see the resulting SKSpriteNode get deformed accordingly.
// SpriteKit warp geometry example
// Copy the following in an Xcode 8+ playground
// and make sure your Assistant View is open
//
import UIKit
import PlaygroundSupport
import SpriteKit
let view = SKView(frame: CGRect(x: 0, y: 0, width: 480, height: 320))
let scene = SKScene(size: view.frame.size)
view.showsFPS = true
view.presentScene(scene)
PlaygroundSupport.PlaygroundPage.current.liveView = view
func drawCanvas1(frame: CGRect = CGRect(x: 0, y: 0, width: 200, height: 200)) -> UIImage {
UIGraphicsBeginImageContext(frame.size)
//// Star Drawing
let starPath = UIBezierPath()
starPath.move(to: CGPoint(x: frame.minX + 100.5, y: frame.minY + 24))
starPath.addLine(to: CGPoint(x: frame.minX + 130.48, y: frame.minY + 67.74))
starPath.addLine(to: CGPoint(x: frame.minX + 181.34, y: frame.minY + 82.73))
starPath.addLine(to: CGPoint(x: frame.minX + 149, y: frame.minY + 124.76))
starPath.addLine(to: CGPoint(x: frame.minX + 150.46, y: frame.minY + 177.77))
starPath.addLine(to: CGPoint(x: frame.minX + 100.5, y: frame.minY + 160))
starPath.addLine(to: CGPoint(x: frame.minX + 50.54, y: frame.minY + 177.77))
starPath.addLine(to: CGPoint(x: frame.minX + 52, y: frame.minY + 124.76))
starPath.addLine(to: CGPoint(x: frame.minX + 19.66, y: frame.minY + 82.73))
starPath.addLine(to: CGPoint(x: frame.minX + 70.52, y: frame.minY + 67.74))
starPath.close()
UIColor.red.setFill()
starPath.fill()
UIColor.green.setStroke()
starPath.lineWidth = 10
starPath.lineCapStyle = .round
starPath.lineJoinStyle = .round
starPath.stroke()
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
class ControlNode : SKShapeNode {
override init() {
super.init()
self.path = CGPath(ellipseIn: CGRect.init(x: 0, y: 0, width: 10, height: 10), transform: nil)
self.fillColor = UIColor.red
self.isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
typealias UpdateHandler = (ControlNode) -> ()
var positionUpdated: UpdateHandler? = nil
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.fillColor = UIColor.yellow
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first, let parent = self.parent {
let pos = touch.location(in: parent).applying(CGAffineTransform(translationX: -5, y: -5))
self.position = pos
positionUpdated?(self)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.fillColor = UIColor.red
}
var warpPosition: vector_float2 {
if let parent = self.parent {
let size = parent.frame.size
let pos = self.position.applying(CGAffineTransform(translationX: -size.width/2 + 5, y: -size.height/2 + 5))
return vector_float2(
x: Float(1 + pos.x / size.width),
y: Float(1 + pos.y / size.height)
)
}
return vector_float2(x: 0, y: 0)
}
}
extension SKSpriteNode {
func addControlNode(x: CGFloat, y: CGFloat) -> ControlNode {
let node = ControlNode()
node.position = CGPoint(
x: x * frame.width - frame.width / 2 - 5,
y: y * frame.height - frame.height / 2 - 5
)
self.addChild(node)
return node
}
}
let texture = SKTexture(image: drawCanvas1())
let image = SKSpriteNode(texture: texture)
image.position = CGPoint(x: 200, y: 160)
scene.addChild(image)
let controlPoints: [(x:CGFloat, y:CGFloat)] = [
(0, 0),
(0.5, 0),
(1.0, 0),
(0, 0.5),
(0.5, 0.5),
(1.0, 0.5),
(0, 1.0),
(0.5, 1.0),
(1.0, 1.0),
]
let sourcePositions = controlPoints.map {
vector_float2.init(Float($0.x), Float($0.y))
}
var controlNodes: [ControlNode] = []
controlPoints.forEach { controlNodes.append(image.addControlNode(x: $0.x, y: $0.y)) }
for node in controlNodes {
node.positionUpdated = {
[weak image]
_ in
let destinationPoints = controlNodes.map {
$0.warpPosition
}
image?.warpGeometry = SKWarpGeometryGrid(columns: 2, rows: 2, sourcePositions: sourcePositions, destinationPositions: destinationPoints)
}
}
PS: I referred to the SKWarpGeometry documentation.
Edit: See #Benzi's answer for a nice option using the SKWarpGeometry API available in iOS 10 / tvOS 10 / macOS 10.12. Prior to those releases, Sprite Kit could only display textures as billboards — the remainder of this answer addresses that pre-2016 situation, which might still be relevant if you're targeting older devices.
You might be able to get the distortion effect you're going for by using SKEffectNode and one of the geometry distortion Core Image filters, though. (CIBumpDistortion, for example.) Keep a reference to the effect node or its filter property in your game logic code (in your SKScene subclass or wherever else you put it), and you can adjust the filter's parameters using setValue:forKey: for each frame of animation.