CoreAnimationでスクラッチを作ってみる

# はじめに
年末で宝くじとか運試ししたいな、
って事でスクラッチアニメーションを作ってみました。

今回UIBezierPathを用いて削る軌跡を描画して、
CoreAnimationでアニメーションさせる事で実現させることにしました。

# 線を描く
```swift
func strokeLine() {
    let layer = CAShapeLayer()
    layer.bounds = self.bounds
    layer.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
    self.layer.addSublayer(layer)
    layer.strokeColor = UIColor.black.cgColor // 線の色
    layer.lineWidth = 10.0                    // 線の太さ

    // パスの生成
    let startPoint = CGPoint(x: self.bounds.midX, y: self.bounds.minY)
    let endPoint = CGPoint(x: self.bounds.midX, y: self.bounds.maxY)
    let path = UIBezierPath()
    path.move(to: startPoint)
    path.addLine(to: endPoint)
    
    layer.path = path.cgPath
}
```
これで一本縦線引くことが出来ます。
これを応用してスクラッチの削る動作を作っていきます。
# 描くアニメーション
CoreAnimationを用いて描く動作をアニメーションします。

```swift
func strokeLine() {
    let layer = CAShapeLayer()
    layer.bounds = self.bounds
    layer.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
    self.layer.addSublayer(layer)
    layer.strokeColor = UIColor.black.cgColor // 線の色
    layer.lineWidth = 10.0                    // 線の太さ

    // パスの生成
    let startPoint = CGPoint(x: self.bounds.midX, y: self.bounds.minY)
    let endPoint = CGPoint(x: self.bounds.midX, y: self.bounds.maxY)
    let startPath = UIBezierPath()
    startPath.move(to: startPoint)
    startPath.addLine(to: startPoint)
    let endPath = UIBezierPath(cgPath: startPath.cgPath)
    endPath.addLine(to: endPoint)

    // アニメーション
    let pathKeyframe = CABasicAnimation(keyPath: "path")
    pathKeyframe.fromValue = startPath.cgPath
    pathKeyframe.toValue = endPath.cgPath
    pathKeyframe.duration = 1.0
    pathKeyframe.isRemovedOnCompletion = false
    pathKeyframe.fillMode = .forwards
    maskLayer.add(pathKeyframe, forKey: "stroke")   
}
```
![stroke.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322831/5d182267-30d2-b037-1c11-a0429950709d.gif)


# 複数のパスを用いたアニメーション
CABasicAnimationの代わりにCAKeyframeAnimationを用います。

```swift
func strokeLine() {
    let layer = CAShapeLayer()
    layer.bounds = self.bounds
    layer.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
    self.layer.addSublayer(layer)
    layer.strokeColor = UIColor.black.cgColor // 線の色
    layer.lineWidth = 10.0                    // 線の太さ
    layer.fillColor = UIColor.clear.cgColor   // 線が閉じられてしまうためclearを設定

    // パスの生成
    let startPoint = CGPoint(x: self.bounds.midX, y: self.bounds.minY)
    let endPoint = CGPoint(x: self.bounds.midX, y: self.bounds.maxY)
    let startPath = UIBezierPath()
    startPath.move(to: startPoint)
    startPath.addLine(to: startPoint)
    let endPath = UIBezierPath(cgPath: startPath.cgPath)
    endPath.addLine(to: endPoint)

    // アニメーション
    let pathKeyframe = CAKeyframeAnimation(keyPath: "path")
    pathKeyframe.values = self.generateCGPaths()
    pathKeyframe.keyTimes = self.generateKeyTimes()
    pathKeyframe.duration = 1.0
    pathKeyframe.isRemovedOnCompletion = false
    pathKeyframe.fillMode = .forwards
    layer.add(pathKeyframe, forKey: "stroke")   
}

/// KeyTime生成
private func generateKeyTimes() -> [NSNumber] {
    var times = [NSNumber]()
    for sequence in 0..<9 {
        let time = (Float(1) / Float(9)) * Float(sequence)
        times.append(NSNumber(value: time))
    }
    
    return times
}

/// CGPath生成
private func generateCGPaths() -> [CGPath] {
    let firstPositionX = self.bounds.width / 9
        
    var points = [CGPoint]()
    for sequence in 0..<9 {
        if sequence % 2 != 0 { // bottom point
            points.append(CGPoint(x: firstPositionX * CGFloat(sequence - 1), y: self.bounds.maxY))
        } else { // top point
            points.append(CGPoint(x: firstPositionX * CGFloat((sequence + 1)), y: self.bounds.minY))
        }
    }
    
    var bezierPaths = [UIBezierPath]()
    points.enumerated().forEach { offset, point in
        if offset == 0 {
            let path = UIBezierPath()
            path.move(to: point)
            path.addLine(to: point)
            bezierPaths.append(path)
            return
        }
        let path = UIBezierPath(cgPath: bezierPaths[offset - 1].cgPath)
        path.addLine(to: point)
        bezierPaths.append(path)
    }
    
    return bezierPaths.map { $0.cgPath }
}
```
![gizagiza.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322831/cf4c518e-1ba0-cef2-69cb-0bdb68a7f0af.gif)

それっぽくなってきたかな。。。

# マスク
ここまで出来たら、あとは表示させたいImageViewに今まで作ったLayerをマスクさせます。

```swift
@IBOutlet private weak var imageView: UIImageView!
private var layer: CAShapeLayer?

func setup() {
    self.backgroundColor = UIColor.lightGray

    let layer = CAShapeLayer()
    layer.bounds = self.bounds
    layer.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
    layer.strokeColor = UIColor.black.cgColor // 線の色
    layer.lineWidth = 10.0                    // 線の太さ
    layer.fillColor = UIColor.clear.cgColor   // 線が閉じられてしまうためclearを設定
    
    self.imageView.layer.mask = layer
    self.layer = layer
}

func strokeLine() {
    // パスの生成
    let startPoint = CGPoint(x: self.bounds.midX, y: self.bounds.minY)
    let endPoint = CGPoint(x: self.bounds.midX, y: self.bounds.maxY)
    let startPath = UIBezierPath()
    startPath.move(to: startPoint)
    startPath.addLine(to: startPoint)
    let endPath = UIBezierPath(cgPath: startPath.cgPath)
    endPath.addLine(to: endPoint)

    // アニメーション
    let pathKeyframe = CAKeyframeAnimation(keyPath: "path")
    pathKeyframe.values = self.generateCGPaths()
    pathKeyframe.keyTimes = self.generateKeyTimes()
    pathKeyframe.duration = 1.0
    pathKeyframe.isRemovedOnCompletion = false
    pathKeyframe.fillMode = .forwards
    self.layer.add(pathKeyframe, forKey: "stroke")   
}

/// KeyTime生成
private func generateKeyTimes() -> [NSNumber] {
    var times = [NSNumber]()
    for sequence in 0..<9 {
        let time = (Float(1) / Float(9)) * Float(sequence)
        times.append(NSNumber(value: time))
    }
    
    return times
}

/// CGPath生成
private func generateCGPaths() -> [CGPath] {
    let firstPositionX = self.bounds.width / 9
        
    var points = [CGPoint]()
    for sequence in 0..<9 {
        if sequence % 2 != 0 { // bottom point
            points.append(CGPoint(x: firstPositionX * CGFloat(sequence - 1), y: self.bounds.maxY))
        } else { // top point
            points.append(CGPoint(x: firstPositionX * CGFloat((sequence + 1)), y: self.bounds.minY))
        }
    }
    
    var bezierPaths = [UIBezierPath]()
    points.enumerated().forEach { offset, point in
        if offset == 0 {
            let path = UIBezierPath()
            path.move(to: point)
            path.addLine(to: point)
            bezierPaths.append(path)
            return
        }
        let path = UIBezierPath(cgPath: bezierPaths[offset - 1].cgPath)
        path.addLine(to: point)
        bezierPaths.append(path)
    }
    
    return bezierPaths.map { $0.cgPath }
}
```
# 完成
![scratch_demomo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322831/bc2e4453-6531-fe04-f8b7-e9b5b2e3bb97.gif)


# 課題
削り終わった後何かアクションが欲しかったり、
削り損なっている箇所が残ったままだったりと、まだまだ課題はありますが、
メインとなる削るアニメーションは出来上がりました。
これに＋α付け足していって楽しいアニメーションを作っていきたいと思います。

タッチイベントと併用すればアニメーションではなく触れたところを削るように見せることも出来そうです。