iOS 核心图形动画
iOS Animation with Core Graphics
我目前正在 iOS 使用 Swift.
创建 Hangman 游戏应用程序
我已经完成了所有的游戏机制,想用 Core Graphics 来画刽子手和绞刑架。我可以使用 UIBezierPath 绘制绞架和刽子手,并将视图的每个部分(绞架、头部、body、左臂...)的绘制分解为单独的函数,调用这些函数来创建整个图像.所有这些都在自定义 UIView class 中。
我似乎无法弄清楚的是如何在用户做出错误猜测时添加 Hangman 图像的连续部分。我试图避免在一系列图像之间循环。相反,我想使用相同的自定义视图使用 Core Graphics 逐个绘制火柴人。
如何实现绘制图形的每个连续部分的函数?
类似于 "self.customView.updateDrawing()",其中 customView 中的 updateDrawing 方法将从模型中获取有关错误猜测次数的信息,并调用适当的方法来绘制简笔画所需的部分。
更新
这是我尝试将此 post https://codereview.stackexchange.com/questions/97424/hangman-in-swift 的视图应用到我自己的刽子手游戏中的尝试。它们非常适合绘制绞架,但实际上我无法让图像自行更新并绘制连续的 body 部分来添加刽子手。到目前为止,这是我的代码:
GameViewTwo:这是上面的 GameView post,我尝试将其拆分并将不同的部分作为层添加到视图中。
import UIKit
enum BodyPart: Int {
case Head = 1
case Body = 2
case LeftArm = 5
case RightArm = 6
case LeftLeg = 3
case RightLeg = 4
case LeftEye = 7
case RightEye = 8
case Mouth = 9
case Unknown = 10
}
class GameViewTwo: UIView {
var path = UIBezierPath()
var newPartLayer = CAShapeLayer()
private var bodyStart: CGPoint = CGPoint.zero
private var bodyEnd: CGPoint = CGPoint.zero
private var headMiddle: CGPoint = CGPoint.zero
struct DrawingConstants {
static let gallowBaseStartScale: CGFloat = 0.15
static let gallowBaseEndScale: CGFloat = 0.85
static let gallowBaseHeight: CGFloat = 10
static let gallowHeight: CGFloat = 0.05 //static let gallowHeight: CGFloat = 0.15
static let gallowHeightStart: CGFloat = 0.175
static let gallowHeightWidth: CGFloat = 10
static let gallowAcrossScale: CGFloat = 0.5
static let gallowTipHeight: CGFloat = 17.5
static let headRadius: CGFloat = 16
static let bodyLength: CGFloat = 25
static let bodyHeight: CGFloat = 25
static let legLength: CGFloat = 50
static let grassHeightScale: CGFloat = 0.68
static let armBack: CGFloat = 5
}
struct ScaleConstants {
static let bodyLength: CGFloat = 50
static let limbLength: CGFloat = 25
static let handHeightScale: CGFloat = 0.4
static let headRadius: CGFloat = 20
static let eyeRadius = CGFloat(0.15 * ScaleConstants.headRadius)
static let eyeOffset = CGFloat(0.3 * ScaleConstants.headRadius)
static let mouthOffSet = CGFloat(0.3 * ScaleConstants.headRadius)
static let mouthRadius = CGFloat(0.25 * ScaleConstants.headRadius)
}
var part : BodyPart?
func setPart(part: BodyPart){
self.part = part
}
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
drawGallow()
}
func add(part: BodyPart){
let partPath = path(forPart: part)
newPartLayer.frame = bounds
newPartLayer.path = partPath.cgPath
newPartLayer.strokeColor = UIColor.black.cgColor
let strokeAnim = CABasicAnimation(keyPath: "strokeEnd")
strokeAnim.fromValue = 0
strokeAnim.toValue = 1
strokeAnim.duration = 1
layer.addSublayer(newPartLayer)
newPartLayer.add(strokeAnim, forKey: "path")
}
func path(forPart: BodyPart)-> UIBezierPath {
switch forPart {
case BodyPart.Head :
let centerX = CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - (DrawingConstants.gallowHeightWidth / 2))
let centerY = CGFloat(bounds.size.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight + DrawingConstants.gallowTipHeight + ScaleConstants.headRadius)
let center = CGPoint(x: centerX, y: centerY)
headMiddle = center
path = UIBezierPath(arcCenter: center, radius: ScaleConstants.headRadius, startAngle: CGFloat(0), endAngle: CGFloat(2 * M_PI), clockwise: true)
path.lineWidth = CGFloat(2)
return path
case BodyPart.Body :
let add = CGFloat(DrawingConstants.gallowBaseHeight + DrawingConstants.gallowTipHeight + 2 * ScaleConstants.headRadius)
let startPointY = CGFloat(bounds.size.height * DrawingConstants.gallowHeight + add)
let startPointX = CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - (DrawingConstants.gallowHeightWidth / 2))
let startPoint = CGPoint(x: startPointX, y: startPointY)
let endPoint = CGPoint(x: startPoint.x, y: startPoint.y + ScaleConstants.bodyLength)
bodyStart = startPoint
bodyEnd = endPoint
path.lineWidth = CGFloat(2)
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case BodyPart.LeftLeg :
let startPoint = CGPoint(x: bodyEnd.x, y: bodyEnd.y)
let endPoint = CGPoint(x: startPoint.x - ScaleConstants.limbLength, y: startPoint.y + ScaleConstants.limbLength)
path.lineWidth = CGFloat(2)
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case BodyPart.RightLeg :
let startPoint = CGPoint(x: bodyEnd.x, y: bodyEnd.y)
let endPoint = CGPoint(x: startPoint.x + ScaleConstants.limbLength, y: startPoint.y + ScaleConstants.limbLength)
path.lineWidth = CGFloat(2)
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case BodyPart.LeftArm :
let startPoint = CGPoint(x: bodyStart.x, y: bodyStart.y + ScaleConstants.handHeightScale * ScaleConstants.bodyLength)
let endPoint = CGPoint(x: startPoint.x - ScaleConstants.limbLength, y: startPoint.y - ScaleConstants.limbLength * ScaleConstants.handHeightScale)
path.lineWidth = CGFloat(2)
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case BodyPart.RightArm :
let startPoint = CGPoint(x: bodyStart.x, y: bodyStart.y + ScaleConstants.handHeightScale * ScaleConstants.bodyLength)
let endPoint = CGPoint(x: startPoint.x + ScaleConstants.limbLength, y: startPoint.y - ScaleConstants.limbLength * ScaleConstants.handHeightScale)
path.lineWidth = CGFloat(2)
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case BodyPart.LeftEye :
UIColor.black.set()
let eyeMiddle = CGPoint(x: headMiddle.x - ScaleConstants.eyeOffset, y: headMiddle.y - ScaleConstants.eyeOffset)
let path = UIBezierPath(arcCenter: eyeMiddle, radius: ScaleConstants.eyeRadius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)
path.lineWidth = CGFloat(1)
return path
case BodyPart.RightEye :
UIColor.black.set()
let eyeMiddle = CGPoint(x: headMiddle.x + ScaleConstants.eyeOffset, y: headMiddle.y - ScaleConstants.eyeOffset)
let path = UIBezierPath(arcCenter: eyeMiddle, radius: ScaleConstants.eyeRadius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)
path.lineWidth = CGFloat(1)
return path
default:
return path
}
}
/******************************************************************************************/
func connectPoints(bottomLeftPoint: CGPoint, bottomRightPoint: CGPoint, topLeftPoint: CGPoint, topRightPoint: CGPoint, color: UIColor) {
color.set()
let path = UIBezierPath()
path.move(to: bottomLeftPoint)
path.addLine(to: topLeftPoint)
path.addLine(to: topRightPoint)
path.addLine(to: bottomRightPoint)
path.close()
path.fill()
path.stroke()
}
func calculateMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint {
return CGPoint(x: (point1.x + point2.x) / 2, y: (point1.y + point2.y) / 2)
}
/*****************************************************************************************/
func drawGrass() {
let topStartPoint = CGPoint(x: CGFloat(0), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale))
let topRightPoint = CGPoint(x: CGFloat(bounds.size.width), y: topStartPoint.y)
let bottomRightPoint = CGPoint(x: topRightPoint.x, y: CGFloat(bounds.size.height))
let bottomLeftPoint = CGPoint(x: CGFloat(0), y: bottomRightPoint.y)
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topStartPoint, topRightPoint: topRightPoint, color: UIColor.green)
}
func drawSky() {
let bottomLeftPoint = CGPoint(x: CGFloat(0), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale))
let topLeftPoint = CGPoint(x: CGFloat(0), y: CGFloat(0))
let topRightPoint = CGPoint(x: CGFloat(bounds.size.width), y: CGFloat(0))
let bottomRightPoint = CGPoint(x: CGFloat(bounds.size.width), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale))
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.cyan)
}
func drawGallow() {
drawGallowBase()
drawGallowHeight()
drawGallowAcross()
drawGallowTip()
}
func drawGallowBase() {
let bottomLeftPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowBaseStartScale), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale))
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: bottomLeftPoint.y - DrawingConstants.gallowBaseHeight)
let topRightPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowBaseEndScale), y: topLeftPoint.y)
let bottomRightPoint = CGPoint(x: topRightPoint.x, y: bottomLeftPoint.y)
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.brown)
}
func drawGallowHeight() {
let bottomLeftPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowHeightStart), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale - DrawingConstants.gallowBaseHeight))
let bottomRightPoint = CGPoint(x: bottomLeftPoint.x + DrawingConstants.gallowHeightWidth, y: bottomLeftPoint.y)
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: bounds.size.height * DrawingConstants.gallowHeight)
let topRightPoint = CGPoint(x: bottomRightPoint.x, y: topLeftPoint.y)
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.brown)
}
func drawGallowAcross() {
let bottomLeftPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowHeightStart) + DrawingConstants.gallowHeightWidth, y: CGFloat(bounds.size.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight))
let bottomRightPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale), y: bottomLeftPoint.y)
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: CGFloat(bounds.size.height * DrawingConstants.gallowHeight))
let topRightPoint = CGPoint(x: CGFloat(bottomRightPoint.x), y: topLeftPoint.y)
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.brown)
}
func drawGallowTip() {
let topLeftPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - DrawingConstants.gallowHeightWidth), y: CGFloat(bounds.size.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight))
let topRightPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale), y: topLeftPoint.y)
let bottomLeftPoint = CGPoint(x: topLeftPoint.x, y: topLeftPoint.y + DrawingConstants.gallowTipHeight)
let bottomRightPoint = CGPoint(x: topRightPoint.x, y: bottomLeftPoint.y)
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.brown)
}
/*******************************************************************************************/
}
然后像这样从我的 GameViewController 中调用它
let newPart = BodyPart(rawValue: (model?.attempts)!)
self.hangmanView.add(part: newPart!)
其中 hangmanView 是一个 GameViewTwo
像这样的方法会奏效。有一个 属性 的自定义 HangmanView
class 叫做 incorrectGuesses
。更改 属性 将通过调用自身 setNeedsDisplay
来触发视图的重绘。
使用 switch
语句和 fallthough
将允许在 incorrectGuesses
增加时显示更多绘图。
class HangmanView: UIView {
var incorrectGuesses = 0 {
didSet {
self.setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
switch incorrectGuesses {
case let x where x > 5:
drawRightLeg()
fallthrough
case 5:
drawLeftLeg()
fallthrough
case 4:
drawRightArm()
fallthrough
case 3:
drawLeftArm()
fallthrough
case 2:
drawBody()
fallthrough
case 1:
drawHead()
fallthrough
case 0:
drawGallows()
default:
break
}
}
func drawGallows() {
// code to draw gallows
}
func drawHead() {
// code to draw head
}
func drawBody() {
// code to draw body
}
func drawLeftArm() {
// code to draw left arm
}
func drawRightArm() {
// code to draw right arm
}
func drawLeftLeg() {
// code to draw left leg
}
func drawRightLeg() {
// code to draw right leg
}
}
要使用它,您可以在 ViewController
中为 HangmanView
设置一个出口,您只需设置 incorrectGuesses
来更新视图:
class ViewController: UIViewController {
@IBOutlet weak var hangmanView: HangManView!
var incorrectGuesses = 0
func gameplay() {
...
if letter not in word {
incorrectGuesses += 1
// Update the HangmanView
hangmanView.incorrectGuesses = incorrectGuesses
}
}
}
在你的 Storyboard 中,添加一个 UIView
并在 Identity Inspector[ 中将 Class 设置为 HangmanView
然后连接到插座上。
如果您想为 HangmanView
的绘图设置动画,您可以将 animationCount
属性 添加到 HangmanView
。当它被设置为一个值时,例如 3
,它将每隔 0.5
秒一点一点地动画绘图。要使用它,您需要设置 hangmanView.animationCount = 3
.
var animationCount = 0 {
didSet {
incorrectGuesses = 0
if animationCount > incorrectGuesses {
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
self.incorrectGuesses += 1
if self.incorrectGuesses >= self.animationCount {
timer.invalidate()
}
}
}
}
}
为每个 body 部分创建一个枚举。使它成为一个整数,这样你就可以用错误答案的数量来实例化它。
enum BodyPart: Int {
case head = 0
case leftArm
case rightArm
...
}
制作新的 body 部分
let newPart = BodyPart(rawValue: incorrectGuesses - 1)
给你的视图几个函数...
func add(part: BodyPart){
// first get a drawing path according to that part
let partPath = path(for: part)
// lets add a new layer so we can animate it seperately
let newPartLayer = CAShapeLayer()
newPartLayer.frame = bounds
newPartLayer.path = partPath
// linewidth, linecap you might want to play with
let strokeAnim = CABasicAnimation(keyPath: "strokeEnd")
strokeAnim.fromValue = 0
strokeAnim.toValue = 1
strokeAnim.duration = 0.5 // play with it, see what you like
layer.addSublayer(layer: newPartLayer)
newPartLayer.add(anim: strokeAnim)
}
func path(for: BodyPart) -> CGPath {
// draw your part path, return it as a CGPath
}
然后从您的控制器中实例化一个新部件并将其添加到您的 hangmanview 中,它将按照您在绘图代码中绘制的方式绘制图层。 UIBezierPath 有一个 .cgPath 变量可以转换为核心图形路径。
let newPart = BodyPart(rawValue: incorrectGuesses - 1)
hangmanView.add(part: newPart)
当你想要一个新游戏时,只需从 hangmanView 中删除所有子层。 CAShapeLayers 对于简单的小形状非常有用,您可以动画更改路径(keyPath:"path")或在任一方向上抚摸路径。会好看的如果保留对所有零件层的引用,您甚至可以独立地移动零件或使用它们做其他有趣的事情。这样,您就有点遵守 model-view-controller 范例。您让游戏状态远离您的视图,您的所有视图都会添加 body 部分,并且您的控制器提供这样做的部分。简单的事情没什么大不了的,但是当你变得更好,事情变得更复杂时,记住这一点是很好的。
快速编辑:您可能想尝试 keyPath: "path",给它一个开始路径(可能是 body 路径)和一个结束路径,它看起来就像部分从body。祝你好运!
*** 已编辑答案。
我在这里清理了你的代码。我不完全确定为什么你在那里有某些变量或小 class 私有结构,但没关系,我把它们留在里面。我移动了一些东西但大部分保持不变,除了你很清楚的地方正在绘制一个矩形而不是其他形状。我没试过运行。让我知道进展如何。
enum BodyPart: Int {
case noose = 0
case head
case body
case leftLeg
case rightLeg
case leftArm
case rightArm
case leftEye
case rightEye
case mouth
case unknown
}
class HangmanView: UIView {
var partLayers = [CAShapeLayer]()
private var bodyStart: CGPoint = CGPoint.zero
private var bodyEnd: CGPoint = CGPoint.zero
private var headMiddle: CGPoint = CGPoint.zero
var currentPart: BodyPart?
func clear() {
for i in partLayers {
i.removeFromSuperlayer()
}
partLayers.removeAll()
}
func add(part: BodyPart){
currentPart = part
let newPartLayer = CAShapeLayer()
let partPath = path(forPart: part)
newPartLayer.frame = bounds
newPartLayer.path = partPath.cgPath
newPartLayer.strokeColor = UIColor.black.cgColor
newPartLayer.fillColor = UIColor.clear.cgColor
newPartLayer.lineCap = kCALineCapRound
newPartLayer.lineWidth = part == .rightEye || part == .leftEye ? 1 : 2
let strokeAnim = CABasicAnimation(keyPath: "strokeEnd")
strokeAnim.fromValue = 0
strokeAnim.toValue = 1
strokeAnim.duration = 1
layer.addSublayer(newPartLayer)
newPartLayer.add(strokeAnim, forKey: "path")
partLayers.append(newPartLayer)
}
func path(forPart: BodyPart) -> UIBezierPath {
switch forPart {
case .noose:
return UIBezierPath()
case .head :
let centerX = CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - (DrawingConstants.gallowHeightWidth / 2))
let centerY = CGFloat(bounds.size.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight + DrawingConstants.gallowTipHeight + ScaleConstants.headRadius)
let center = CGPoint(x: centerX, y: centerY)
headMiddle = center
let path = UIBezierPath(arcCenter: center, radius: ScaleConstants.headRadius, startAngle: CGFloat(0), endAngle: CGFloat(2 * M_PI), clockwise: true)
return path
case .body :
let add = CGFloat(DrawingConstants.gallowBaseHeight + DrawingConstants.gallowTipHeight + 2 * ScaleConstants.headRadius)
let startPointY = CGFloat(bounds.size.height * DrawingConstants.gallowHeight + add)
let startPointX = CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - (DrawingConstants.gallowHeightWidth / 2))
let startPoint = CGPoint(x: startPointX, y: startPointY)
let endPoint = CGPoint(x: startPoint.x, y: startPoint.y + ScaleConstants.bodyLength)
bodyStart = startPoint
bodyEnd = endPoint
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case .leftLeg :
let startPoint = CGPoint(x: bodyEnd.x, y: bodyEnd.y)
let endPoint = CGPoint(x: startPoint.x - ScaleConstants.limbLength, y: startPoint.y + ScaleConstants.limbLength)
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case .rightLeg :
let startPoint = CGPoint(x: bodyEnd.x, y: bodyEnd.y)
let endPoint = CGPoint(x: startPoint.x + ScaleConstants.limbLength, y: startPoint.y + ScaleConstants.limbLength)
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case .leftArm :
let startPoint = CGPoint(x: bodyStart.x, y: bodyStart.y + ScaleConstants.handHeightScale * ScaleConstants.bodyLength)
let endPoint = CGPoint(x: startPoint.x - ScaleConstants.limbLength, y: startPoint.y - ScaleConstants.limbLength * ScaleConstants.handHeightScale)
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case .rightArm :
let startPoint = CGPoint(x: bodyStart.x, y: bodyStart.y + ScaleConstants.handHeightScale * ScaleConstants.bodyLength)
let endPoint = CGPoint(x: startPoint.x + ScaleConstants.limbLength, y: startPoint.y - ScaleConstants.limbLength * ScaleConstants.handHeightScale)
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case .leftEye :
let eyeMiddle = CGPoint(x: headMiddle.x - ScaleConstants.eyeOffset, y: headMiddle.y - ScaleConstants.eyeOffset)
let path = UIBezierPath(arcCenter: eyeMiddle, radius: ScaleConstants.eyeRadius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)
path.lineWidth = CGFloat(1)
return path
case .rightEye :
let eyeMiddle = CGPoint(x: headMiddle.x + ScaleConstants.eyeOffset, y: headMiddle.y - ScaleConstants.eyeOffset)
let path = UIBezierPath(arcCenter: eyeMiddle, radius: ScaleConstants.eyeRadius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)
path.lineWidth = CGFloat(1)
return path
default:
return UIBezierPath()
}
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// get the drawing context and save the ground state
context.saveGState()
// add sky and grass, now they are just rectangles
context.setFillColor(UIColor.cyan.cgColor)
context.fill(sky(rect))
context.setFillColor(UIColor.green.cgColor)
context.fill(grass(rect))
context.setFillColor(UIColor.brown.cgColor)
context.addPath(gallowBase(rect))
context.addPath(gallowHeight(rect))
context.addPath(gallowAcross(rect))
context.addPath(gallowTip(rect))
context.fillPath()
context.restoreGState()
}
func gallowBase(_ rect: CGRect) -> CGPath {
let bottomLeftPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowBaseStartScale), y: CGFloat(rect.height * DrawingConstants.grassHeightScale))
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: bottomLeftPoint.y - DrawingConstants.gallowBaseHeight)
let topRightPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowBaseEndScale), y: topLeftPoint.y)
let bottomRightPoint = CGPoint(x: topRightPoint.x, y: bottomLeftPoint.y)
let path = CGMutablePath()
path.addLines(between: [bottomLeftPoint,topLeftPoint, topRightPoint,bottomRightPoint])
path.closeSubpath()
return path
}
func gallowHeight(_ rect: CGRect) -> CGPath {
let bottomLeftPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowHeightStart), y: CGFloat(rect.height * DrawingConstants.grassHeightScale - DrawingConstants.gallowBaseHeight))
let bottomRightPoint = CGPoint(x: bottomLeftPoint.x + DrawingConstants.gallowHeightWidth, y: bottomLeftPoint.y)
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: rect.height * DrawingConstants.gallowHeight)
let topRightPoint = CGPoint(x: bottomRightPoint.x, y: topLeftPoint.y)
let path = CGMutablePath()
path.addLines(between: [bottomLeftPoint,topLeftPoint, topRightPoint,bottomRightPoint])
path.closeSubpath()
return path
}
func gallowAcross(_ rect: CGRect) -> CGPath {
let bottomLeftPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowHeightStart) + DrawingConstants.gallowHeightWidth, y: CGFloat(rect.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight))
let bottomRightPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowAcrossScale), y: bottomLeftPoint.y)
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: CGFloat(rect.height * DrawingConstants.gallowHeight))
let topRightPoint = CGPoint(x: CGFloat(bottomRightPoint.x), y: topLeftPoint.y)
let path = CGMutablePath()
path.addLines(between: [bottomLeftPoint,topLeftPoint, topRightPoint,bottomRightPoint])
path.closeSubpath()
return path
}
func gallowTip(_ rect: CGRect) -> CGPath {
let topLeftPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowAcrossScale - DrawingConstants.gallowHeightWidth), y: CGFloat(rect.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight))
let topRightPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowAcrossScale), y: topLeftPoint.y)
let bottomLeftPoint = CGPoint(x: topLeftPoint.x, y: topLeftPoint.y + DrawingConstants.gallowTipHeight)
let bottomRightPoint = CGPoint(x: topRightPoint.x, y: bottomLeftPoint.y)
let path = CGMutablePath()
path.addLines(between: [bottomLeftPoint,topLeftPoint, topRightPoint,bottomRightPoint])
path.closeSubpath()
return path
}
func grass(_ rect: CGRect) -> CGRect {
let grassRect = CGRect(x: 0, y: rect.height * DrawingConstants.grassHeightScale, width: rect.width, height: (1 - DrawingConstants.grassHeightScale) * rect.height)
return grassRect
}
func sky(_ rect: CGRect) -> CGRect {
let skyRect = CGRect(x: 0, y: 0, width: rect.width, height: DrawingConstants.grassHeightScale * rect.height)
return skyRect
}
func calculateMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint {
return CGPoint(x: (point1.x + point2.x) / 2, y: (point1.y + point2.y) / 2)
}
struct DrawingConstants {
static let gallowBaseStartScale: CGFloat = 0.15
static let gallowBaseEndScale: CGFloat = 0.85
static let gallowBaseHeight: CGFloat = 10
static let gallowHeight: CGFloat = 0.05 //static let gallowHeight: CGFloat = 0.15
static let gallowHeightStart: CGFloat = 0.175
static let gallowHeightWidth: CGFloat = 10
static let gallowAcrossScale: CGFloat = 0.5
static let gallowTipHeight: CGFloat = 17.5
static let headRadius: CGFloat = 16
static let bodyLength: CGFloat = 25
static let bodyHeight: CGFloat = 25
static let legLength: CGFloat = 50
static let grassHeightScale: CGFloat = 0.68
static let armBack: CGFloat = 5
}
struct ScaleConstants {
static let bodyLength: CGFloat = 50
static let limbLength: CGFloat = 25
static let handHeightScale: CGFloat = 0.4
static let headRadius: CGFloat = 20
static let eyeRadius = 0.15 * headRadius
static let eyeOffset = 0.3 * headRadius
static let mouthOffSet = 0.3 * headRadius
static let mouthRadius = 0.25 * headRadius
}
}
我目前正在 iOS 使用 Swift.
创建 Hangman 游戏应用程序我已经完成了所有的游戏机制,想用 Core Graphics 来画刽子手和绞刑架。我可以使用 UIBezierPath 绘制绞架和刽子手,并将视图的每个部分(绞架、头部、body、左臂...)的绘制分解为单独的函数,调用这些函数来创建整个图像.所有这些都在自定义 UIView class 中。
我似乎无法弄清楚的是如何在用户做出错误猜测时添加 Hangman 图像的连续部分。我试图避免在一系列图像之间循环。相反,我想使用相同的自定义视图使用 Core Graphics 逐个绘制火柴人。
如何实现绘制图形的每个连续部分的函数?
类似于 "self.customView.updateDrawing()",其中 customView 中的 updateDrawing 方法将从模型中获取有关错误猜测次数的信息,并调用适当的方法来绘制简笔画所需的部分。
更新
这是我尝试将此 post https://codereview.stackexchange.com/questions/97424/hangman-in-swift 的视图应用到我自己的刽子手游戏中的尝试。它们非常适合绘制绞架,但实际上我无法让图像自行更新并绘制连续的 body 部分来添加刽子手。到目前为止,这是我的代码:
GameViewTwo:这是上面的 GameView post,我尝试将其拆分并将不同的部分作为层添加到视图中。
import UIKit
enum BodyPart: Int {
case Head = 1
case Body = 2
case LeftArm = 5
case RightArm = 6
case LeftLeg = 3
case RightLeg = 4
case LeftEye = 7
case RightEye = 8
case Mouth = 9
case Unknown = 10
}
class GameViewTwo: UIView {
var path = UIBezierPath()
var newPartLayer = CAShapeLayer()
private var bodyStart: CGPoint = CGPoint.zero
private var bodyEnd: CGPoint = CGPoint.zero
private var headMiddle: CGPoint = CGPoint.zero
struct DrawingConstants {
static let gallowBaseStartScale: CGFloat = 0.15
static let gallowBaseEndScale: CGFloat = 0.85
static let gallowBaseHeight: CGFloat = 10
static let gallowHeight: CGFloat = 0.05 //static let gallowHeight: CGFloat = 0.15
static let gallowHeightStart: CGFloat = 0.175
static let gallowHeightWidth: CGFloat = 10
static let gallowAcrossScale: CGFloat = 0.5
static let gallowTipHeight: CGFloat = 17.5
static let headRadius: CGFloat = 16
static let bodyLength: CGFloat = 25
static let bodyHeight: CGFloat = 25
static let legLength: CGFloat = 50
static let grassHeightScale: CGFloat = 0.68
static let armBack: CGFloat = 5
}
struct ScaleConstants {
static let bodyLength: CGFloat = 50
static let limbLength: CGFloat = 25
static let handHeightScale: CGFloat = 0.4
static let headRadius: CGFloat = 20
static let eyeRadius = CGFloat(0.15 * ScaleConstants.headRadius)
static let eyeOffset = CGFloat(0.3 * ScaleConstants.headRadius)
static let mouthOffSet = CGFloat(0.3 * ScaleConstants.headRadius)
static let mouthRadius = CGFloat(0.25 * ScaleConstants.headRadius)
}
var part : BodyPart?
func setPart(part: BodyPart){
self.part = part
}
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
drawGallow()
}
func add(part: BodyPart){
let partPath = path(forPart: part)
newPartLayer.frame = bounds
newPartLayer.path = partPath.cgPath
newPartLayer.strokeColor = UIColor.black.cgColor
let strokeAnim = CABasicAnimation(keyPath: "strokeEnd")
strokeAnim.fromValue = 0
strokeAnim.toValue = 1
strokeAnim.duration = 1
layer.addSublayer(newPartLayer)
newPartLayer.add(strokeAnim, forKey: "path")
}
func path(forPart: BodyPart)-> UIBezierPath {
switch forPart {
case BodyPart.Head :
let centerX = CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - (DrawingConstants.gallowHeightWidth / 2))
let centerY = CGFloat(bounds.size.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight + DrawingConstants.gallowTipHeight + ScaleConstants.headRadius)
let center = CGPoint(x: centerX, y: centerY)
headMiddle = center
path = UIBezierPath(arcCenter: center, radius: ScaleConstants.headRadius, startAngle: CGFloat(0), endAngle: CGFloat(2 * M_PI), clockwise: true)
path.lineWidth = CGFloat(2)
return path
case BodyPart.Body :
let add = CGFloat(DrawingConstants.gallowBaseHeight + DrawingConstants.gallowTipHeight + 2 * ScaleConstants.headRadius)
let startPointY = CGFloat(bounds.size.height * DrawingConstants.gallowHeight + add)
let startPointX = CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - (DrawingConstants.gallowHeightWidth / 2))
let startPoint = CGPoint(x: startPointX, y: startPointY)
let endPoint = CGPoint(x: startPoint.x, y: startPoint.y + ScaleConstants.bodyLength)
bodyStart = startPoint
bodyEnd = endPoint
path.lineWidth = CGFloat(2)
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case BodyPart.LeftLeg :
let startPoint = CGPoint(x: bodyEnd.x, y: bodyEnd.y)
let endPoint = CGPoint(x: startPoint.x - ScaleConstants.limbLength, y: startPoint.y + ScaleConstants.limbLength)
path.lineWidth = CGFloat(2)
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case BodyPart.RightLeg :
let startPoint = CGPoint(x: bodyEnd.x, y: bodyEnd.y)
let endPoint = CGPoint(x: startPoint.x + ScaleConstants.limbLength, y: startPoint.y + ScaleConstants.limbLength)
path.lineWidth = CGFloat(2)
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case BodyPart.LeftArm :
let startPoint = CGPoint(x: bodyStart.x, y: bodyStart.y + ScaleConstants.handHeightScale * ScaleConstants.bodyLength)
let endPoint = CGPoint(x: startPoint.x - ScaleConstants.limbLength, y: startPoint.y - ScaleConstants.limbLength * ScaleConstants.handHeightScale)
path.lineWidth = CGFloat(2)
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case BodyPart.RightArm :
let startPoint = CGPoint(x: bodyStart.x, y: bodyStart.y + ScaleConstants.handHeightScale * ScaleConstants.bodyLength)
let endPoint = CGPoint(x: startPoint.x + ScaleConstants.limbLength, y: startPoint.y - ScaleConstants.limbLength * ScaleConstants.handHeightScale)
path.lineWidth = CGFloat(2)
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case BodyPart.LeftEye :
UIColor.black.set()
let eyeMiddle = CGPoint(x: headMiddle.x - ScaleConstants.eyeOffset, y: headMiddle.y - ScaleConstants.eyeOffset)
let path = UIBezierPath(arcCenter: eyeMiddle, radius: ScaleConstants.eyeRadius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)
path.lineWidth = CGFloat(1)
return path
case BodyPart.RightEye :
UIColor.black.set()
let eyeMiddle = CGPoint(x: headMiddle.x + ScaleConstants.eyeOffset, y: headMiddle.y - ScaleConstants.eyeOffset)
let path = UIBezierPath(arcCenter: eyeMiddle, radius: ScaleConstants.eyeRadius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)
path.lineWidth = CGFloat(1)
return path
default:
return path
}
}
/******************************************************************************************/
func connectPoints(bottomLeftPoint: CGPoint, bottomRightPoint: CGPoint, topLeftPoint: CGPoint, topRightPoint: CGPoint, color: UIColor) {
color.set()
let path = UIBezierPath()
path.move(to: bottomLeftPoint)
path.addLine(to: topLeftPoint)
path.addLine(to: topRightPoint)
path.addLine(to: bottomRightPoint)
path.close()
path.fill()
path.stroke()
}
func calculateMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint {
return CGPoint(x: (point1.x + point2.x) / 2, y: (point1.y + point2.y) / 2)
}
/*****************************************************************************************/
func drawGrass() {
let topStartPoint = CGPoint(x: CGFloat(0), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale))
let topRightPoint = CGPoint(x: CGFloat(bounds.size.width), y: topStartPoint.y)
let bottomRightPoint = CGPoint(x: topRightPoint.x, y: CGFloat(bounds.size.height))
let bottomLeftPoint = CGPoint(x: CGFloat(0), y: bottomRightPoint.y)
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topStartPoint, topRightPoint: topRightPoint, color: UIColor.green)
}
func drawSky() {
let bottomLeftPoint = CGPoint(x: CGFloat(0), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale))
let topLeftPoint = CGPoint(x: CGFloat(0), y: CGFloat(0))
let topRightPoint = CGPoint(x: CGFloat(bounds.size.width), y: CGFloat(0))
let bottomRightPoint = CGPoint(x: CGFloat(bounds.size.width), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale))
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.cyan)
}
func drawGallow() {
drawGallowBase()
drawGallowHeight()
drawGallowAcross()
drawGallowTip()
}
func drawGallowBase() {
let bottomLeftPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowBaseStartScale), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale))
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: bottomLeftPoint.y - DrawingConstants.gallowBaseHeight)
let topRightPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowBaseEndScale), y: topLeftPoint.y)
let bottomRightPoint = CGPoint(x: topRightPoint.x, y: bottomLeftPoint.y)
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.brown)
}
func drawGallowHeight() {
let bottomLeftPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowHeightStart), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale - DrawingConstants.gallowBaseHeight))
let bottomRightPoint = CGPoint(x: bottomLeftPoint.x + DrawingConstants.gallowHeightWidth, y: bottomLeftPoint.y)
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: bounds.size.height * DrawingConstants.gallowHeight)
let topRightPoint = CGPoint(x: bottomRightPoint.x, y: topLeftPoint.y)
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.brown)
}
func drawGallowAcross() {
let bottomLeftPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowHeightStart) + DrawingConstants.gallowHeightWidth, y: CGFloat(bounds.size.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight))
let bottomRightPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale), y: bottomLeftPoint.y)
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: CGFloat(bounds.size.height * DrawingConstants.gallowHeight))
let topRightPoint = CGPoint(x: CGFloat(bottomRightPoint.x), y: topLeftPoint.y)
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.brown)
}
func drawGallowTip() {
let topLeftPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - DrawingConstants.gallowHeightWidth), y: CGFloat(bounds.size.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight))
let topRightPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale), y: topLeftPoint.y)
let bottomLeftPoint = CGPoint(x: topLeftPoint.x, y: topLeftPoint.y + DrawingConstants.gallowTipHeight)
let bottomRightPoint = CGPoint(x: topRightPoint.x, y: bottomLeftPoint.y)
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.brown)
}
/*******************************************************************************************/
}
然后像这样从我的 GameViewController 中调用它
let newPart = BodyPart(rawValue: (model?.attempts)!)
self.hangmanView.add(part: newPart!)
其中 hangmanView 是一个 GameViewTwo
像这样的方法会奏效。有一个 属性 的自定义 HangmanView
class 叫做 incorrectGuesses
。更改 属性 将通过调用自身 setNeedsDisplay
来触发视图的重绘。
使用 switch
语句和 fallthough
将允许在 incorrectGuesses
增加时显示更多绘图。
class HangmanView: UIView {
var incorrectGuesses = 0 {
didSet {
self.setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
switch incorrectGuesses {
case let x where x > 5:
drawRightLeg()
fallthrough
case 5:
drawLeftLeg()
fallthrough
case 4:
drawRightArm()
fallthrough
case 3:
drawLeftArm()
fallthrough
case 2:
drawBody()
fallthrough
case 1:
drawHead()
fallthrough
case 0:
drawGallows()
default:
break
}
}
func drawGallows() {
// code to draw gallows
}
func drawHead() {
// code to draw head
}
func drawBody() {
// code to draw body
}
func drawLeftArm() {
// code to draw left arm
}
func drawRightArm() {
// code to draw right arm
}
func drawLeftLeg() {
// code to draw left leg
}
func drawRightLeg() {
// code to draw right leg
}
}
要使用它,您可以在 ViewController
中为 HangmanView
设置一个出口,您只需设置 incorrectGuesses
来更新视图:
class ViewController: UIViewController {
@IBOutlet weak var hangmanView: HangManView!
var incorrectGuesses = 0
func gameplay() {
...
if letter not in word {
incorrectGuesses += 1
// Update the HangmanView
hangmanView.incorrectGuesses = incorrectGuesses
}
}
}
在你的 Storyboard 中,添加一个 UIView
并在 Identity Inspector[ 中将 Class 设置为 HangmanView
然后连接到插座上。
如果您想为 HangmanView
的绘图设置动画,您可以将 animationCount
属性 添加到 HangmanView
。当它被设置为一个值时,例如 3
,它将每隔 0.5
秒一点一点地动画绘图。要使用它,您需要设置 hangmanView.animationCount = 3
.
var animationCount = 0 {
didSet {
incorrectGuesses = 0
if animationCount > incorrectGuesses {
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
self.incorrectGuesses += 1
if self.incorrectGuesses >= self.animationCount {
timer.invalidate()
}
}
}
}
}
为每个 body 部分创建一个枚举。使它成为一个整数,这样你就可以用错误答案的数量来实例化它。
enum BodyPart: Int {
case head = 0
case leftArm
case rightArm
...
}
制作新的 body 部分
let newPart = BodyPart(rawValue: incorrectGuesses - 1)
给你的视图几个函数...
func add(part: BodyPart){
// first get a drawing path according to that part
let partPath = path(for: part)
// lets add a new layer so we can animate it seperately
let newPartLayer = CAShapeLayer()
newPartLayer.frame = bounds
newPartLayer.path = partPath
// linewidth, linecap you might want to play with
let strokeAnim = CABasicAnimation(keyPath: "strokeEnd")
strokeAnim.fromValue = 0
strokeAnim.toValue = 1
strokeAnim.duration = 0.5 // play with it, see what you like
layer.addSublayer(layer: newPartLayer)
newPartLayer.add(anim: strokeAnim)
}
func path(for: BodyPart) -> CGPath {
// draw your part path, return it as a CGPath
}
然后从您的控制器中实例化一个新部件并将其添加到您的 hangmanview 中,它将按照您在绘图代码中绘制的方式绘制图层。 UIBezierPath 有一个 .cgPath 变量可以转换为核心图形路径。
let newPart = BodyPart(rawValue: incorrectGuesses - 1)
hangmanView.add(part: newPart)
当你想要一个新游戏时,只需从 hangmanView 中删除所有子层。 CAShapeLayers 对于简单的小形状非常有用,您可以动画更改路径(keyPath:"path")或在任一方向上抚摸路径。会好看的如果保留对所有零件层的引用,您甚至可以独立地移动零件或使用它们做其他有趣的事情。这样,您就有点遵守 model-view-controller 范例。您让游戏状态远离您的视图,您的所有视图都会添加 body 部分,并且您的控制器提供这样做的部分。简单的事情没什么大不了的,但是当你变得更好,事情变得更复杂时,记住这一点是很好的。
快速编辑:您可能想尝试 keyPath: "path",给它一个开始路径(可能是 body 路径)和一个结束路径,它看起来就像部分从body。祝你好运!
*** 已编辑答案。
我在这里清理了你的代码。我不完全确定为什么你在那里有某些变量或小 class 私有结构,但没关系,我把它们留在里面。我移动了一些东西但大部分保持不变,除了你很清楚的地方正在绘制一个矩形而不是其他形状。我没试过运行。让我知道进展如何。
enum BodyPart: Int {
case noose = 0
case head
case body
case leftLeg
case rightLeg
case leftArm
case rightArm
case leftEye
case rightEye
case mouth
case unknown
}
class HangmanView: UIView {
var partLayers = [CAShapeLayer]()
private var bodyStart: CGPoint = CGPoint.zero
private var bodyEnd: CGPoint = CGPoint.zero
private var headMiddle: CGPoint = CGPoint.zero
var currentPart: BodyPart?
func clear() {
for i in partLayers {
i.removeFromSuperlayer()
}
partLayers.removeAll()
}
func add(part: BodyPart){
currentPart = part
let newPartLayer = CAShapeLayer()
let partPath = path(forPart: part)
newPartLayer.frame = bounds
newPartLayer.path = partPath.cgPath
newPartLayer.strokeColor = UIColor.black.cgColor
newPartLayer.fillColor = UIColor.clear.cgColor
newPartLayer.lineCap = kCALineCapRound
newPartLayer.lineWidth = part == .rightEye || part == .leftEye ? 1 : 2
let strokeAnim = CABasicAnimation(keyPath: "strokeEnd")
strokeAnim.fromValue = 0
strokeAnim.toValue = 1
strokeAnim.duration = 1
layer.addSublayer(newPartLayer)
newPartLayer.add(strokeAnim, forKey: "path")
partLayers.append(newPartLayer)
}
func path(forPart: BodyPart) -> UIBezierPath {
switch forPart {
case .noose:
return UIBezierPath()
case .head :
let centerX = CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - (DrawingConstants.gallowHeightWidth / 2))
let centerY = CGFloat(bounds.size.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight + DrawingConstants.gallowTipHeight + ScaleConstants.headRadius)
let center = CGPoint(x: centerX, y: centerY)
headMiddle = center
let path = UIBezierPath(arcCenter: center, radius: ScaleConstants.headRadius, startAngle: CGFloat(0), endAngle: CGFloat(2 * M_PI), clockwise: true)
return path
case .body :
let add = CGFloat(DrawingConstants.gallowBaseHeight + DrawingConstants.gallowTipHeight + 2 * ScaleConstants.headRadius)
let startPointY = CGFloat(bounds.size.height * DrawingConstants.gallowHeight + add)
let startPointX = CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - (DrawingConstants.gallowHeightWidth / 2))
let startPoint = CGPoint(x: startPointX, y: startPointY)
let endPoint = CGPoint(x: startPoint.x, y: startPoint.y + ScaleConstants.bodyLength)
bodyStart = startPoint
bodyEnd = endPoint
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case .leftLeg :
let startPoint = CGPoint(x: bodyEnd.x, y: bodyEnd.y)
let endPoint = CGPoint(x: startPoint.x - ScaleConstants.limbLength, y: startPoint.y + ScaleConstants.limbLength)
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case .rightLeg :
let startPoint = CGPoint(x: bodyEnd.x, y: bodyEnd.y)
let endPoint = CGPoint(x: startPoint.x + ScaleConstants.limbLength, y: startPoint.y + ScaleConstants.limbLength)
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case .leftArm :
let startPoint = CGPoint(x: bodyStart.x, y: bodyStart.y + ScaleConstants.handHeightScale * ScaleConstants.bodyLength)
let endPoint = CGPoint(x: startPoint.x - ScaleConstants.limbLength, y: startPoint.y - ScaleConstants.limbLength * ScaleConstants.handHeightScale)
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case .rightArm :
let startPoint = CGPoint(x: bodyStart.x, y: bodyStart.y + ScaleConstants.handHeightScale * ScaleConstants.bodyLength)
let endPoint = CGPoint(x: startPoint.x + ScaleConstants.limbLength, y: startPoint.y - ScaleConstants.limbLength * ScaleConstants.handHeightScale)
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case .leftEye :
let eyeMiddle = CGPoint(x: headMiddle.x - ScaleConstants.eyeOffset, y: headMiddle.y - ScaleConstants.eyeOffset)
let path = UIBezierPath(arcCenter: eyeMiddle, radius: ScaleConstants.eyeRadius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)
path.lineWidth = CGFloat(1)
return path
case .rightEye :
let eyeMiddle = CGPoint(x: headMiddle.x + ScaleConstants.eyeOffset, y: headMiddle.y - ScaleConstants.eyeOffset)
let path = UIBezierPath(arcCenter: eyeMiddle, radius: ScaleConstants.eyeRadius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)
path.lineWidth = CGFloat(1)
return path
default:
return UIBezierPath()
}
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// get the drawing context and save the ground state
context.saveGState()
// add sky and grass, now they are just rectangles
context.setFillColor(UIColor.cyan.cgColor)
context.fill(sky(rect))
context.setFillColor(UIColor.green.cgColor)
context.fill(grass(rect))
context.setFillColor(UIColor.brown.cgColor)
context.addPath(gallowBase(rect))
context.addPath(gallowHeight(rect))
context.addPath(gallowAcross(rect))
context.addPath(gallowTip(rect))
context.fillPath()
context.restoreGState()
}
func gallowBase(_ rect: CGRect) -> CGPath {
let bottomLeftPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowBaseStartScale), y: CGFloat(rect.height * DrawingConstants.grassHeightScale))
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: bottomLeftPoint.y - DrawingConstants.gallowBaseHeight)
let topRightPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowBaseEndScale), y: topLeftPoint.y)
let bottomRightPoint = CGPoint(x: topRightPoint.x, y: bottomLeftPoint.y)
let path = CGMutablePath()
path.addLines(between: [bottomLeftPoint,topLeftPoint, topRightPoint,bottomRightPoint])
path.closeSubpath()
return path
}
func gallowHeight(_ rect: CGRect) -> CGPath {
let bottomLeftPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowHeightStart), y: CGFloat(rect.height * DrawingConstants.grassHeightScale - DrawingConstants.gallowBaseHeight))
let bottomRightPoint = CGPoint(x: bottomLeftPoint.x + DrawingConstants.gallowHeightWidth, y: bottomLeftPoint.y)
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: rect.height * DrawingConstants.gallowHeight)
let topRightPoint = CGPoint(x: bottomRightPoint.x, y: topLeftPoint.y)
let path = CGMutablePath()
path.addLines(between: [bottomLeftPoint,topLeftPoint, topRightPoint,bottomRightPoint])
path.closeSubpath()
return path
}
func gallowAcross(_ rect: CGRect) -> CGPath {
let bottomLeftPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowHeightStart) + DrawingConstants.gallowHeightWidth, y: CGFloat(rect.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight))
let bottomRightPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowAcrossScale), y: bottomLeftPoint.y)
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: CGFloat(rect.height * DrawingConstants.gallowHeight))
let topRightPoint = CGPoint(x: CGFloat(bottomRightPoint.x), y: topLeftPoint.y)
let path = CGMutablePath()
path.addLines(between: [bottomLeftPoint,topLeftPoint, topRightPoint,bottomRightPoint])
path.closeSubpath()
return path
}
func gallowTip(_ rect: CGRect) -> CGPath {
let topLeftPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowAcrossScale - DrawingConstants.gallowHeightWidth), y: CGFloat(rect.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight))
let topRightPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowAcrossScale), y: topLeftPoint.y)
let bottomLeftPoint = CGPoint(x: topLeftPoint.x, y: topLeftPoint.y + DrawingConstants.gallowTipHeight)
let bottomRightPoint = CGPoint(x: topRightPoint.x, y: bottomLeftPoint.y)
let path = CGMutablePath()
path.addLines(between: [bottomLeftPoint,topLeftPoint, topRightPoint,bottomRightPoint])
path.closeSubpath()
return path
}
func grass(_ rect: CGRect) -> CGRect {
let grassRect = CGRect(x: 0, y: rect.height * DrawingConstants.grassHeightScale, width: rect.width, height: (1 - DrawingConstants.grassHeightScale) * rect.height)
return grassRect
}
func sky(_ rect: CGRect) -> CGRect {
let skyRect = CGRect(x: 0, y: 0, width: rect.width, height: DrawingConstants.grassHeightScale * rect.height)
return skyRect
}
func calculateMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint {
return CGPoint(x: (point1.x + point2.x) / 2, y: (point1.y + point2.y) / 2)
}
struct DrawingConstants {
static let gallowBaseStartScale: CGFloat = 0.15
static let gallowBaseEndScale: CGFloat = 0.85
static let gallowBaseHeight: CGFloat = 10
static let gallowHeight: CGFloat = 0.05 //static let gallowHeight: CGFloat = 0.15
static let gallowHeightStart: CGFloat = 0.175
static let gallowHeightWidth: CGFloat = 10
static let gallowAcrossScale: CGFloat = 0.5
static let gallowTipHeight: CGFloat = 17.5
static let headRadius: CGFloat = 16
static let bodyLength: CGFloat = 25
static let bodyHeight: CGFloat = 25
static let legLength: CGFloat = 50
static let grassHeightScale: CGFloat = 0.68
static let armBack: CGFloat = 5
}
struct ScaleConstants {
static let bodyLength: CGFloat = 50
static let limbLength: CGFloat = 25
static let handHeightScale: CGFloat = 0.4
static let headRadius: CGFloat = 20
static let eyeRadius = 0.15 * headRadius
static let eyeOffset = 0.3 * headRadius
static let mouthOffSet = 0.3 * headRadius
static let mouthRadius = 0.25 * headRadius
}
}