Search code examples
iosobjective-cswiftcore-animationuiviewanimation

How to animate a stitched image?


I'm not sure how to create this animation. Would you somehow split the 1 jpg file evenly in 3 pieces and animate that? Or would you have to make multiple copies of the jpg and do something with them?

Any help would be awesome!


Solution

  • UPDATE

    Since you want a crossfade, it's probably easiest to do this by splitting the image into separate cel images:

    import UIKit
    import PlaygroundSupport
    
    extension UIImage {
        func subImage(inUnitRect unitRect: CGRect) -> UIImage? {
            guard imageOrientation == .up, let cgImage = self.cgImage else { return nil }
            let cgImageWidth = CGFloat(cgImage.width)
            let cgImageHeight = CGFloat(cgImage.height)
            let scaledRect = CGRect(x: unitRect.origin.x * cgImageWidth, y: unitRect.origin.y * cgImageHeight, width: unitRect.size.width * cgImageWidth, height: unitRect.size.height * cgImageHeight)
            guard let croppedCgImage = cgImage.cropping(to: scaledRect) else { return nil }
            return UIImage(cgImage: croppedCgImage, scale: scale, orientation: .up)
        }
    }
    
    let image = #imageLiteral(resourceName: "image.png")
    
    let celCount: CGFloat = 3
    let cels = stride(from: 0, to: celCount, by: 1).map({ (i) -> UIImage in
        image.subImage(inUnitRect: CGRect(x: i / celCount, y: 0, width: 1/3, height: 1))!
    })
    

    Then we can use a keyframe animation to crossfade the layer contents:

    let imageView = UIImageView(image: cels[0])
    
    PlaygroundPage.current.liveView = imageView
    
    let animation = CAKeyframeAnimation(keyPath: "contents")
    var values = [CGImage]()
    var keyTimes = [Double]()
    for (i, cel) in cels.enumerated() {
        keyTimes.append(Double(i) / Double(cels.count))
        values.append(cel.cgImage!)
        // The 0.9 means 90% of the time will be spent *outside* of crossfade.
        keyTimes.append((Double(i) + 0.9) / Double(cels.count))
        values.append(cel.cgImage!)
    }
    values.append(cels[0].cgImage!)
    keyTimes.append(1.0)
    animation.keyTimes = keyTimes.map({ NSNumber(value: $0) })
    animation.values = values
    animation.repeatCount = .infinity
    animation.duration = 5
    imageView.layer.add(animation, forKey: animation.keyPath)
    

    Result:

    animation with crossfade

    ORIGINAL

    There are multiple ways you can do this. One is by setting or animating the contentsRect property of the image view's layer.

    In your image, there are three cels, and each occupies exactly 1/3 of the image. The contentsRect is in the unit coordinate space, which makes computation easy. The contentsRect for cel i is CGRect(x: i/3, y: 0, width: 1/3, height: 0).

    You want discrete jumps between cels, instead of smooth sliding transitions, so you need to use a keyframe animation with a kCAAnimationDiscrete calculationMode.

    import UIKit
    import PlaygroundSupport
    
    let image = #imageLiteral(resourceName: "image.png")
    let celSize = CGSize(width: image.size.width / 3, height: image.size.height)
    let imageView = UIImageView()
    imageView.frame = CGRect(origin: .zero, size: celSize)
    imageView.image = image
    
    PlaygroundPage.current.liveView = imageView
    
    let animation = CAKeyframeAnimation(keyPath: "contentsRect")
    animation.duration = 1.5
    animation.calculationMode = kCAAnimationDiscrete
    animation.repeatCount = .infinity
    animation.values = [
        CGRect(x: 0, y: 0, width: 1/3.0, height: 1),
        CGRect(x: 1/3.0, y: 0, width: 1/3.0, height: 1),
        CGRect(x: 2/3.0, y: 0, width: 1/3.0, height: 1)
    ] as [CGRect]
    imageView.layer.add(animation, forKey: animation.keyPath!)
    

    Result:

    animation demo