在iOS中实现谷歌灭霸彩蛋的完整示例

时间:2022-09-20 10:13:39

前言

最近上映的复仇者联盟4据说没有片尾彩蛋,不过谷歌帮我们做了。只要在谷歌搜索灭霸,在结果的右侧点击无限手套,你将化身为灭霸,其中一半的搜索结果会化为灰烬消失...那么这么酷的动画在ios中可以实现吗?答案是肯定的。整个动画主要包含以下几部分:响指动画、沙化消失以及背景音效和复原动画,让我们分别来看看如何实现。

在iOS中实现谷歌灭霸彩蛋的完整示例在iOS中实现谷歌灭霸彩蛋的完整示例

图1 左为沙化动画,右为复原动画

响指动画

google的方法是利用了48帧合成的一张sprite图进行动画的:

在iOS中实现谷歌灭霸彩蛋的完整示例

图2 响指sprite图片

原始图片中48幅全部排成一行,这里为了显示效果截成2行

ios 中通过这张图片来实现动画并不难。calayer有一个属性contentsrect,通过它可以控制内容显示的区域,而且是animateable的。它的类型是cgrect,默认值为(x:0.0, y:0.0, width:1.0, height:1.0),它的单位不是常见的point,而是单位坐标空间,所以默认值显示100%的内容区域。新建sprite播放视图层animatablespritelayer:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class animatablespritelayer: calayer {
 private var animationvalues = [cgfloat]()
 convenience init(spritesheetimage: uiimage, spriteframesize: cgsize ) {
 self.init()
 //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() {
 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")
 }
}

//1: maskstobounds = true和contentsgravity = calayercontentsgravity.left是为了当前只显示sprite图的第一幅画面

//2: 根据sprite图大小和每幅画面的大小计算出画面数量,预先计算出每幅画面的contentsrect.origin.x偏移量

//3: 这里是关键,指定关键帧动画的calculationmode为discrete确保关键帧动画依次使用values中指定的关键帧值进行变化,而不是默认情况下采用线性插值进行过渡,来个对比图可能比较容易理解:

在iOS中实现谷歌灭霸彩蛋的完整示例在iOS中实现谷歌灭霸彩蛋的完整示例

图3 左边为离散模式,右边为默认的线性模式

沙化消失

这个效果是整个动画较难的部分,google的实现很巧妙,它将需要沙化消失内容的html通过html2canvas渲染成canvas,然后将其转换为图片后的每一个像素点随机地分配到32块canvas中,最后对每块画布进行随机地移动和旋转即达到了沙化消失的效果。

像素处理

新建自定义视图 dusteffectview,这个视图的作用是用来接收图片并将其进行沙化消失。首先创建函数createdustimages,它将一张图片的像素随机分配到32张等待动画的图片上:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class dusteffectview: uiview {
 private func createdustimages(image: uiimage) -> [uiimage] {
 var result = [uiimage]()
 guard let inputcgimage = image.cgimage else {
  return result
 }
 //1
 let colorspace = cgcolorspacecreatedevicergb()
 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
 }
}

//1: 根据指定格式创建位图上下文,然后将输入的图片绘制上去之后获取其像素数据

//2: 创建像素二维数组,遍历输入图片每个像素,将其随机分配到数组32个元素之一的相同位置。随机方法有点特别,原始图片左边的像素只会分配到前几张图片,而原始图片右边的像素只会分配到后几张。

在iOS中实现谷歌灭霸彩蛋的完整示例

图4 上部分为原始图片,下部分为像素分配后的32张图片依次显示效果

//3: 这里循环2次将像素分配两次,可能 google 觉得只分配一遍会造成像素比较稀疏。个人认为在移动端,只要一遍就好了。

//4: 创建32张图片并返回

添加动画

google的实现是给canvas中css的transform属性设置为rotate(deg) translate(px, px) rotate(deg),值都是随机生成的。如果你对css的动画不熟悉,那你会觉得在ios中只要添加三个cabasicanimation然后将它们添加到animationgroup就好了嘛,实际上并没有那么简单... 因为css的transform中后一个变换函数是基于前一个变换后的新transform坐标系。假如某张图片的动画样式是这样的:rotate(90deg) translate(0px, 100px) rotate(-90deg) 直觉告诉我应该是旋转着向下移动100px,然而在css中的元素是这么运动的:

在iOS中实现谷歌灭霸彩蛋的完整示例

图5 css中transform多值动画

第一个rotate和translate决定了最终的位置和运动轨迹,至于第二个rotate作用,只是叠加第一个rotate的值作为最终的旋转弧度,这里刚好为0也就是不旋转。那么在ios中该如何实现相似的运动轨迹呢?可以利用uibezierpath, cakeyframeanimation的属性path可以指定这个uibezierpath为动画的运动轨迹。确定起点和实际终点作为贝塞尔曲线的起始点和终止点,那么如何确定控制点?好像可以将“预想”的终点(下图中的(0,-1))作为控制点。

在iOS中实现谷歌灭霸彩蛋的完整示例

图6 将“预想”的终点作为控制点的贝塞尔曲线,看起来和css中的运动轨迹差不多

扩展问题

通过文章中描述的方式生成的贝塞尔曲线是否与css中的动画轨迹完全一致呢?

现在可以给视图添加动画了:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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)

//1: 实际的偏移量旋转了radian1弧度,这个可以通过公式x' = x*cos(rad) - y*sin(rad), y' = y*cos(rad) + x*sin(rad)算出

//2: 创建uibezierpath并关联到cakeyframeanimation中

//3: 两个弧度叠加作为最终的旋转弧度

//4: 设置caanimationgroup的开始时间,让每层layer的动画延迟开始

结尾

到这里,谷歌灭霸彩蛋中较复杂的技术点均已实现。如果您感兴趣,完整的代码(包含音效和复原动画)可以通过文章开头的链接进行查看,可以尝试将沙化图片的数量从32提高至更多,效果会越好,内存也会消耗更多 :-d。

示例代码下载

参考资料

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对服务器之家的支持。

原文链接:https://juejin.im/post/5cc652adf265da03540316e3