Sample code download

The recently-released Avengers 4 supposedly didn’t have any post-credits scenes, but Google did. Just Google Thanos and click Infinite Gloves on the right side of the results and you will be transformed into Thanos and half of the results will disappear into ashes… So is this cool animation possible on iOS? The answer is yes. The whole animation mainly includes the following parts: finger snap animation, sand disappear and background sound and restoration animation, let’s see how to achieve it respectively.


Figure 1 shows desertification animation on the left and restoration animation on the right

Ring animation

Google’s method uses 48 frames of a Sprite image to animate it:

Figure 2 Finger snap Sprite picture

All 48 of the original images are arranged in one line, which is cut into 2 lines for effect

It’s not hard to animate this image in iOS. CALayer has a property called contentsRect, which controls the area of the content display, and is Animateable. It is of type CGRect and defaults to (x:0.0, y:0.0, Width :1.0, height:1.0). Its units are not the usual Point, but the unit coordinate space, so the default shows 100% of the content area. New Sprite playview layer AnimatableSpriteLayer:

class AnimatableSpriteLayer: CALayer {
    private var animationValues = [CGFloat] ()convenience init(spriteSheetImage: UIImage, spriteFrameSize: CGSize ) {
        self.init(a)/ / 1
        masksToBounds = true
        contentsGravity = CALayerContentsGravity.left
        contents = spriteSheetImage.cgImage
        bounds.size = spriteFrameSize
        / / 2
        let frameCount = Int(spriteSheetImage.size.width / spriteFrameSize.width)
        for frameIndex in 0..<frameCount {
            animationValues.append(CGFloat(frameIndex) / CGFloat(frameCount))
        }
    }
    
    func play(a) {
        let spriteKeyframeAnimation = CAKeyframeAnimation(keyPath: "contentsRect.origin.x")
        spriteKeyframeAnimation.values = animationValues
        spriteKeyframeAnimation.duration = 2.0
        spriteKeyframeAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
        / / 3
        spriteKeyframeAnimation.calculationMode = CAAnimationCalculationMode.discrete
        add(spriteKeyframeAnimation, forKey: "spriteKeyframeAnimation")}}Copy the code

/ / 1: masksToBounds = true and contentsGravity = CALayerContentsGravity. The left is for the sake of the current display only the first picture / / Sprite figure 2: Calculate the number of frames according to the Sprite image size and the size of each frame, and pre-calculate the offset of each frame contentsrect.origine.x //3: Here’s the key, specify calculationMode for keyframe animation to discrete to ensure that keyframe animation changes sequentially using the keyframe values specified in VALUES, instead of linear interpolation as the default. A comparison diagram might be easier to understand:


Figure 3 shows the discrete mode on the left and the default linear mode on the right

Desertification disappear

This effect is a difficult part of the whole animation. Google’s implementation is very clever. It renders the HTML that needs to be sanded and disappeared into canvas through HTML2Canvas, and then randomly allocates every pixel after it is converted into a picture to 32 canvas. Finally, each canvas is moved and rotated randomly to achieve the effect of desertification disappearing.

Pixel processing

Create a new custom view, DustEffectView. This view is used to receive images and desertize them. Start by creating a function called createDustImages, which randomly assigns pixels of an image to 32 images waiting to be animated:

class DustEffectView: UIView {
    private func createDustImages(image: UIImage)- > [UIImage] {
        var result = [UIImage] ()guard let inputCGImage = image.cgImage else {
            return result
        }
        / / 1
        let colorSpace = CGColorSpaceCreateDeviceRGB(a)let width = inputCGImage.width
        let height = inputCGImage.height
        let bytesPerPixel = 4
        let bitsPerComponent = 8
        let bytesPerRow = bytesPerPixel * width
        let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
        
        guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
            return result
        }
        context.draw(inputCGImage, in: CGRect(x: 0, y: 0, width: width, height: height))
        guard let buffer = context.data else {
            return result
        }
        let pixelBuffer = buffer.bindMemory(to: UInt32.self, capacity: width * height)
        / / 2
        let imagesCount = 32
        var framePixels = Array(repeating: Array(repeating: UInt32(0), count: width * height), count: imagesCount)
        for column in 0..<width {
            for row in 0..<height {
                let offset = row * width + column
                / / 3
                for _ in 0.1 { 
                    let factor = Double.random(in: 0..<1) + 2 * (Double(column)/Double(width))
                    let index = Int(floor(Double(imagesCount) * ( factor / 3)))
                    framePixels[index][offset] = pixelBuffer[offset]
                }
            }
        }
        / / 4
        for frame in framePixels {
            let data = UnsafeMutablePointer(mutating: frame)
            guard let context = CGContext(data: data, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
                continue
            }
            result.append(UIImage(cgImage: context.makeImage()! , scale: image.scale, orientation: image.imageOrientation)) }return result
    }
}
Copy the code

//1: create bitmap context according to the specified format, and then draw the input image to get its pixel data //2: create a two-dimensional array of pixels, traverse each pixel of the input image, randomly assign it to the same position in one of the 32 elements of the array. The random method is a bit special, the pixels to the left of the original image are allocated to the first few images, and the pixels to the right of the original image are allocated to the next few.

//3: Here we loop twice to allocate pixels twice. Maybe Google thinks that allocating pixels only once will cause sparse pixels. Personally, I think on mobile, just once is good. //4: Create 32 images and return

Add animation

Rotate (deG) Translate (px, px) Rotate (deG) is the transform property of the CANVAS CSS. The values are randomly generated. If you are not familiar with CSS animations, you might think that adding three CabasicAnimations to iOS and adding them to the AnimationGroup is not that easy… Because the CSS transform function is based on the transform coordinate system of the previous transform. Rotate (90deg) translate(0px, 100px) Rotate (-90deg) rotate(-90deg) rotate(100px) rotate(-90deg)

The first rotate and Translate determine the final position and trajectory. The second rotate simply adds the value of the first rotate as the final rotation curve, which is zero. So how do you achieve a similar trajectory in iOS? You can use UIBezierPath, the property path of CAKeyframeAnimation to specify this UIBezierPath as the trajectory of the animation. Determine the starting point and the actual end point as the starting and ending points of the Bezier curve, then how to determine the control point? It seems possible to use the “desired” endpoint ((0,-1) in the figure below) as a control point.

Extension question Does the Bessel curve generated in the way described in this article exactly match the animation trajectory in CSS?

Now you can animate the view:

    let layer = CALayer()
    layer.frame = bounds
    layer.contents = image.cgImage
    self.layer.addSublayer(layer)
    let centerX = Double(layer.position.x)
    let centerY = Double(layer.position.y)
    let radian1 = Double.pi / 12 * Double.random(in: -0.5..<0.5)
    let radian2 = Double.pi / 12 * Double.random(in: -0.5..<0.5)
    let random = Double.pi * 2 * Double.random(in: -0.5..<0.5)
    let transX = 60 * cos(random)
    let transY = 30 * sin(random)
    / / 1:
    // x' = x*cos(rad) - y*sin(rad)
    // y' = y*cos(rad) + x*sin(rad)
    let realTransX = transX * cos(radian1) - transY * sin(radian1)
    let realTransY = transY * cos(radian1) + transX * sin(radian1)
    let realEndPoint = CGPoint(x: centerX + realTransX, y: centerY + realTransY)
    let controlPoint = CGPoint(x: centerX + transX, y: centerY + transY)
    / / 2:
    let movePath = UIBezierPath()
    movePath.move(to: layer.position)
    movePath.addQuadCurve(to: realEndPoint, controlPoint: controlPoint)
    let moveAnimation = CAKeyframeAnimation(keyPath: "position")
    moveAnimation.path = movePath.cgPath
    moveAnimation.calculationMode = .paced
    / / 3:
    let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
    rotateAnimation.toValue = radian1 + radian2
    let fadeOutAnimation = CABasicAnimation(keyPath: "opacity")
    fadeOutAnimation.toValue = 0.0
    let animationGroup = CAAnimationGroup()
    animationGroup.animations = [moveAnimation, rotateAnimation, fadeOutAnimation]
    animationGroup.duration = 1
    / / 4:
    animationGroup.beginTime = CACurrentMediaTime() + 1.35 * Double(i) / Double(imagesCount)
    animationGroup.isRemovedOnCompletion = false
    animationGroup.fillMode = .forwards
    layer.add(animationGroup, forKey: nil)
Copy the code

X ‘= x*cos(rad) -y *sin(rad), y’ = y*cos(rad) + x*sin(rad) Create UIBezierPath and associate it with CAKeyframeAnimation //3: add two radians as the final rotation radian //4: Set the start time of CAAnimationGroup so that the animation of each Layer is delayed

At the end

At this point, all of the more complex technical points in Google’s Thanos eggs have been implemented. If you’re interested, the full code (including sound effects and restored animations) can be viewed at the link at the beginning of this article. Try increasing the number of sand images from 32 to more, the better, and the more memory you’ll need: -d.

The resources

  1. www.calayer.com/core-animat…
  2. Stackoverflow.com/questions/3…
  3. Weibo.com/1727858283/…