#21 運用 UIBezierPath 繪製可愛圖案-皮卡丘

Sam
彼得潘的 Swift iOS / Flutter App 開發教室
19 min readSep 6, 2020

右邊是原圖,左邊是用程式畫出來的。

線條的描繪方式主要有三個,UIBezierPath() 的 addLine、addQuadCurve、addCurve。

眼睛的部分用 init(ovalIn:) 來繪製圓形以及橢圓形來組成。

所有程式碼:

let view = UIView()
view.backgroundColor = UIColor(red: 254/255, green: 218/255, blue: 37/255, alpha: 1)

//整體輪廓
var path = UIBezierPath()
path.move(to: CGPoint(x: 82, y: 84))
path.addQuadCurve(to: CGPoint(x: 17, y: 12), controlPoint: CGPoint(x: 27, y: 45))//左耳
path.addQuadCurve(to: CGPoint(x: 97, y: 69), controlPoint: CGPoint(x: 65, y: 21))
path.addQuadCurve(to: CGPoint(x: 169, y: 68), controlPoint: CGPoint(x: 132, y: 54))//頭頂
path.addQuadCurve(to: CGPoint(x: 253, y: 20), controlPoint: CGPoint(x: 201, y: 23))//右耳
path.addQuadCurve(to: CGPoint(x: 186, y: 83), controlPoint: CGPoint(x: 236, y: 60))
//右邊臉
path.move(to: CGPoint(x: 184, y: 80))
path.addCurve(to: CGPoint(x: 197, y: 132), controlPoint1: CGPoint(x: 200, y: 96), controlPoint2: CGPoint(x: 190, y: 119))
path.addQuadCurve(to: CGPoint(x: 157, y: 182), controlPoint: CGPoint(x: 209, y: 160))
//右邊身體
path.move(to: CGPoint(x: 188, y: 165))
path.addCurve(to: CGPoint(x: 210, y: 232), controlPoint1: CGPoint(x: 186, y: 186), controlPoint2: CGPoint(x: 215, y: 217))
//右腳
path.move(to: CGPoint(x: 199, y: 241))
path.addQuadCurve(to: CGPoint(x: 222, y: 226), controlPoint: CGPoint(x: 220, y: 222))//腳趾一
path.addQuadCurve(to: CGPoint(x: 214, y: 238), controlPoint: CGPoint(x: 224, y: 227))//腳趾一
path.move(to: CGPoint(x: 222, y: 226))
path.addQuadCurve(to: CGPoint(x: 226, y: 229), controlPoint: CGPoint(x: 228, y: 225))//腳趾二
path.addQuadCurve(to: CGPoint(x: 215, y: 243), controlPoint: CGPoint(x: 226, y: 231))//腳趾二
path.move(to: CGPoint(x: 227, y: 228))
path.addQuadCurve(to: CGPoint(x: 218, y: 245), controlPoint: CGPoint(x: 230, y: 233))//腳趾三
path.addQuadCurve(to: CGPoint(x: 180, y: 268), controlPoint: CGPoint(x: 184, y: 279))//後腳跟
path.addQuadCurve(to: CGPoint(x: 182, y: 261), controlPoint: CGPoint(x: 179, y: 264))//後腳跟
path.move(to: CGPoint(x: 180, y: 268))
path.addQuadCurve(to: CGPoint(x: 167, y: 264), controlPoint: CGPoint(x: 173, y: 268))
path.move(to: CGPoint(x: 192, y: 211))//右邊手
path.addQuadCurve(to: CGPoint(x: 165, y: 266), controlPoint: CGPoint(x: 189, y: 231))
path.addLine(to: CGPoint(x: 165, y: 270))
path.addLine(to: CGPoint(x: 161, y: 269))
path.addLine(to: CGPoint(x: 159, y: 270))
path.addLine(to: CGPoint(x: 156, y: 267))
path.addLine(to: CGPoint(x: 154, y: 269))
path.addLine(to: CGPoint(x: 151, y: 267))
path.addLine(to: CGPoint(x: 147, y: 267))
path.addLine(to: CGPoint(x: 146, y: 265))
path.addLine(to: CGPoint(x: 142, y: 262))
path.addLine(to: CGPoint(x: 144, y: 259))
path.addCurve(to: CGPoint(x: 151, y: 192), controlPoint1: CGPoint(x: 145, y: 255), controlPoint2: CGPoint(x: 139, y: 231))
path.move(to: CGPoint(x: 144, y: 259))
path.addLine(to: CGPoint(x: 126, y: 259))//屁股
path.move(to: CGPoint(x: 122, y: 194))//左邊手
path.addCurve(to: CGPoint(x: 126, y: 264), controlPoint1: CGPoint(x: 131, y: 236), controlPoint2: CGPoint(x: 126, y: 255))
path.addLine(to: CGPoint(x: 123, y: 263))
path.addLine(to: CGPoint(x: 121, y: 266))
path.addLine(to: CGPoint(x: 118, y: 265))
path.addLine(to: CGPoint(x: 115, y: 268))
path.addLine(to: CGPoint(x: 112, y: 266))
path.addLine(to: CGPoint(x: 109, y: 269))
path.addLine(to: CGPoint(x: 106, y: 267))
path.addLine(to: CGPoint(x: 101, y: 268))
path.addLine(to: CGPoint(x: 102, y: 264))
path.addQuadCurve(to: CGPoint(x: 78, y: 218), controlPoint: CGPoint(x: 79, y: 235))
path.move(to: CGPoint(x: 101, y: 264))
path.addQuadCurve(to: CGPoint(x: 84, y: 264), controlPoint: CGPoint(x: 87, y: 267))
//左邊腳
path.move(to: CGPoint(x: 84, y: 256))
path.addQuadCurve(to: CGPoint(x: 61, y: 256), controlPoint: CGPoint(x: 85, y: 280))
path.addQuadCurve(to: CGPoint(x: 42, y: 226), controlPoint: CGPoint(x: 38, y: 230))
path.move(to: CGPoint(x: 51, y: 239))
path.addQuadCurve(to: CGPoint(x: 42, y: 226), controlPoint: CGPoint(x: 44, y: 231))
path.addQuadCurve(to: CGPoint(x: 47, y: 223), controlPoint: CGPoint(x: 42, y: 221))
path.move(to: CGPoint(x: 56, y: 238))
path.addQuadCurve(to: CGPoint(x: 47, y: 223), controlPoint: CGPoint(x: 45, y: 226))
path.addQuadCurve(to: CGPoint(x: 68, y: 235), controlPoint: CGPoint(x: 49, y: 217))
//左邊身體
path.move(to: CGPoint(x: 60, y: 227))
path.addCurve(to: CGPoint(x: 83, y: 161), controlPoint1: CGPoint(x: 56, y: 201), controlPoint2: CGPoint(x: 84, y: 197))
//左邊臉
path.move(to: CGPoint(x: 110, y: 178))
path.addQuadCurve(to: CGPoint(x: 74, y: 134), controlPoint: CGPoint(x: 65, y: 157))
path.addCurve(to: CGPoint(x: 88, y: 77), controlPoint1: CGPoint(x: 83, y: 117), controlPoint2: CGPoint(x: 66, y: 97))
//尾巴
path.move(to: CGPoint(x: 190, y: 175))
path.addLine(to: CGPoint(x: 208, y: 164))
path.addLine(to: CGPoint(x: 195, y: 155))
path.move(to: CGPoint(x: 191, y: 89))
path.addQuadCurve(to: CGPoint(x: 279, y: 56), controlPoint: CGPoint(x: 245, y: 62))
path.addQuadCurve(to: CGPoint(x: 267, y: 117), controlPoint: CGPoint(x: 277, y: 84))
path.addLine(to: CGPoint(x: 213, y: 132))
path.addLine(to: CGPoint(x: 242, y: 165))
path.addLine(to: CGPoint(x: 206, y: 181))
path.addLine(to: CGPoint(x: 215, y: 193))
path.addLine(to: CGPoint(x: 200, y: 200))
//嘴
path.move(to: CGPoint(x: 115, y: 138))
path.addQuadCurve(to: CGPoint(x: 135, y: 137), controlPoint: CGPoint(x: 122, y: 145))
path.addQuadCurve(to: CGPoint(x: 156, y: 137), controlPoint: CGPoint(x: 152, y: 146))

var pikaLayer = CAShapeLayer()
pikaLayer.path = path.cgPath
pikaLayer.fillColor = UIColor.clear.cgColor
pikaLayer.strokeColor = UIColor.black.cgColor
pikaLayer.lineWidth = 2
view.layer.addSublayer(pikaLayer)

//尾毛
path = UIBezierPath()
path.move(to: CGPoint(x: 200, y: 200))
path.addLine(to: CGPoint(x: 215, y: 193))
path.addLine(to: CGPoint(x: 209, y: 185))
path.addLine(to: CGPoint(x: 208, y: 186))
path.addLine(to: CGPoint(x: 200, y: 182))
path.addLine(to: CGPoint(x: 202, y: 187))
path.addLine(to: CGPoint(x: 198, y: 185))
path.addLine(to: CGPoint(x: 199, y: 189))
path.addLine(to: CGPoint(x: 195, y: 189))
path.close()

pikaLayer = CAShapeLayer()
pikaLayer.path = path.cgPath
pikaLayer.fillColor = UIColor(red: 205/255, green: 68/255, blue: 19/255, alpha: 1).cgColor
view.layer.addSublayer(pikaLayer)

//左耳黑色部分
path = UIBezierPath()
path.move(to: CGPoint(x: 17, y: 12))
path.addQuadCurve(to: CGPoint(x: 36, y: 17), controlPoint: CGPoint(x: 25, y: 13))
path.addQuadCurve(to: CGPoint(x: 45, y: 53), controlPoint: CGPoint(x: 36, y: 31))
path.addQuadCurve(to: CGPoint(x: 17, y: 12), controlPoint: CGPoint(x: 21, y: 27))
//右耳黑色部分
path.move(to: CGPoint(x: 253, y: 20))
path.addQuadCurve(to: CGPoint(x: 229, y: 24), controlPoint: CGPoint(x: 242, y: 20))
path.addQuadCurve(to: CGPoint(x: 223, y: 60), controlPoint: CGPoint(x: 229, y: 37))
path.addQuadCurve(to: CGPoint(x: 253, y: 20), controlPoint: CGPoint(x: 242, y: 42))
//鼻子
path.move(to: CGPoint(x: 133, y: 124))
path.addLine(to: CGPoint(x: 138, y: 124))
path.addQuadCurve(to: CGPoint(x: 137, y: 125), controlPoint: CGPoint(x: 138, y: 125))
path.addLine(to: CGPoint(x: 134, y: 125))
path.addQuadCurve(to: CGPoint(x: 133, y: 124), controlPoint: CGPoint(x: 134, y: 125))

pikaLayer = CAShapeLayer()
pikaLayer.path = path.cgPath
pikaLayer.strokeColor = UIColor.black.cgColor
pikaLayer.lineWidth = 1
view.layer.addSublayer(pikaLayer)

//左電氣囊
path = UIBezierPath()
path.move(to: CGPoint(x: 74, y: 135))
path.addQuadCurve(to: CGPoint(x: 88, y: 134), controlPoint: CGPoint(x: 79, y: 131))
path.addQuadCurve(to: CGPoint(x: 94, y: 153), controlPoint: CGPoint(x: 98, y: 140))
path.addQuadCurve(to: CGPoint(x: 83, y: 161), controlPoint: CGPoint(x: 92, y: 159))
path.addQuadCurve(to: CGPoint(x: 74, y: 135), controlPoint: CGPoint(x: 68, y: 147))
//右電氣囊
path.move(to: CGPoint(x: 192, y: 160))
path.addQuadCurve(to: CGPoint(x: 177, y: 154), controlPoint: CGPoint(x: 180, y: 164))
path.addQuadCurve(to: CGPoint(x: 184, y: 138), controlPoint: CGPoint(x: 176, y: 144))
path.addQuadCurve(to: CGPoint(x: 199, y: 139), controlPoint: CGPoint(x: 194, y: 132))
path.addQuadCurve(to: CGPoint(x: 192, y: 160), controlPoint: CGPoint(x: 200, y: 151))

pikaLayer = CAShapeLayer()
pikaLayer.path = path.cgPath
pikaLayer.fillColor = UIColor(red: 252/255, green: 52/255, blue: 17/255, alpha: 1).cgColor
pikaLayer.strokeColor = UIColor.black.cgColor
pikaLayer.lineWidth = 1
view.layer.addSublayer(pikaLayer)

//左眼
path = UIBezierPath(ovalIn: CGRect(x: 88, y: 105, width: 22, height: 22))
pikaLayer = CAShapeLayer()
pikaLayer.path = path.cgPath
pikaLayer.fillColor = UIColor(red: 86/255, green: 59/255, blue: 10/255, alpha: 1).cgColor
view.layer.addSublayer(pikaLayer)

//左眼黑
path = UIBezierPath(ovalIn: CGRect(x: 90, y: 106, width: 19, height: 16))
pikaLayer = CAShapeLayer()
pikaLayer.path = path.cgPath
view.layer.addSublayer(pikaLayer)

//左眼白
path = UIBezierPath(ovalIn: CGRect(x: 98, y: 107, width: 9, height: 9))
pikaLayer = CAShapeLayer()
pikaLayer.path = path.cgPath
pikaLayer.fillColor = UIColor.white.cgColor
view.layer.addSublayer(pikaLayer)

//右眼
path = UIBezierPath(ovalIn: CGRect(x: 159, y: 105, width: 22, height: 22))
pikaLayer = CAShapeLayer()
pikaLayer.path = path.cgPath
pikaLayer.fillColor = UIColor(red: 86/255, green: 59/255, blue: 10/255, alpha: 1).cgColor
view.layer.addSublayer(pikaLayer)

//右眼黑
path = UIBezierPath(ovalIn: CGRect(x:160, y: 106, width: 19, height: 16))
pikaLayer = CAShapeLayer()
pikaLayer.path = path.cgPath
view.layer.addSublayer(pikaLayer)

//右眼白
path = UIBezierPath(ovalIn: CGRect(x: 162, y: 107, width: 9, height: 9))
pikaLayer = CAShapeLayer()
pikaLayer.path = path.cgPath
pikaLayer.fillColor = UIColor.white.cgColor
view.layer.addSublayer(pikaLayer)

return view

心得:

不規則形狀真的不好畫,只能以好幾個曲線來畫出,整個畫完還滿花時間的,主要的時間都花在曲線座標的微調,好險可以用 SwiftUI 來預覽畫面。

在開始畫的時候還沒有什麼圖形概念,直接就是用畫線條的方式畫出整個輪廓,導致畫完之後它不是一個完整的圖形,就沒辦法設定一致的顏色(也沒辦法用 mask 的方法)。但頭已經洗一半了,就把它設透明,讓它跟背景一樣顏色。

在畫細節部分的時候才以圖形的概念下去畫,所以像是耳朵、眼睛、臉頰等,才能夠正常上色。

--

--