当前位置: 首页 > 科技观察

在iOS中实现GoogleThanos复活节彩蛋

时间:2023-03-16 13:36:40 科技观察

示例代码下载最近发布的复仇者联盟4据说没有片尾彩蛋,但Google已经为我们做到了。只需在Google中搜索Thanos,点击结果右侧的手套,你就会变成Thanos,搜索结果的一半就会化为灰烬消失……那么这么炫酷的动画在iOS中有可能吗?答案是肯定的。整个动画主要包括以下几个部分:弹指动画、沙子消失、背景音效和恢复动画,下面分别来看如何实现。图一:左边是沙化动画,右边是修复动画。弹指动画Google的方法是用48帧合成的sprite图片做动画:图2.所有48张弹指sprite图片排成一排,这里为了显示效果剪成2行实现动画并不难通过iOS中的这张图片。CALayer有一个属性contentsRect,通过它可以控制内容显示的区域,它是Animateable。它的类型是CGRect,默认值是(x:0.0,y:0.0,width:1.0,height:1.0),它的单位不是普通的Point,而是单位坐标空间,所以默认值显示100%内容区。新建Sprite播放图层AnimatableSpriteLayer:classAnimatableSpriteLayer:CALayer{privatevaranimationValues=[CGFloat]()convenienceinit(spriteSheetImage:UIImage,spriteFrameSize:CGSize){self.init()//1masksToBounds=truecontentsGravity=CALayerboundsize.SheftcontentsgravityspriteFrameSize//2letframeCount=Int(spriteSheetImage.size.width/spriteFrameSize.width)forframeIndexin0..animationValues.append(CGFloat(frameIndex)/CGFloat(frameCount))}}funcplay(){letspriteKeyframeAnimation=CAKeyframeAnimation(keyPath:"contentsRect.origin.x")spriteKeyframeAnimation.values=animationValuesspriteKeyframeAnimation.duration=2.0spriteKeyframeAnimation.timingFunction=CAMediaTimingFunction(name:CAMediaTimingFunctionName.linear)//3spriteKeyframeAnimation.calculationMode=CAAnimationCalculationMode.discreteadd(spriteKeyframeAnimation,forKey:"spriteKeyframeAnimation:masksToBounds")}}//1=true和contentsGravity=CALayerContentsGravity.left目前只显示精灵图片//2:根据精灵图片的大小和每张图片的大小计算图片数量,预先计算每张图片的contentsRect.origin.x偏移量//3:这里是关键,指定关键帧动画的calculationMode为discrete保证关键帧动画使用values中指定的关键帧值依次变化,而不是默认使用线性插值进行过渡。用一张对比图可能更容易理解:图3左边是离散模式,右边是默认的线性模式。打磨和消失的效果是整个动画中最难的部分。Google的实现非常巧妙。它通过html2canvas将需要打磨消失的html渲染成canvas,然后转成图片。每个像素点随机分配到32张画布上,每张画布随机移动旋转,达到消失沙子的效果。像素处理创建一个新的自定义视图DustEffectView,用于接收图片并通过沙漠化消失。首先创建函数createDustImages,将一张图片的像素点随机分配给32张等待动画的图片:cgImageelse{returnresult}//1letcolorSpace=CGColorSpaceCreateDeviceRGB()letwidth=inputCGImage.widthletheight=inputCGImage.heightletbytesPerPixel=4letbitsPerComponent=8letbytesPerRow=bytesPerPixel*widthletbitmapInfo=CGImageAlphaInfo.premultipliedLast.rawValue|CGBitmapInfo.byteOrder32Little.rawValueguardletcontext=CGContext(data:nil,width:width,height:height,bitsPerComponent:bitsPerComponent,bytesPerRow:bytesPerRow,space:colorSpace,bitmapInfo:bitmapInfo)else{returnresult}context.draw(inputCGImage,in:CGRect(x:0,y:0,width:width,height:height))guardletbuffer=context.dataelse{returnresult}letpixelBuffer=buffer.bindMemory(to:UInt32.self,capacity:width*height)//2letimagesCount=32varframePixels=Array(repeating:Array(repeating:UInt32(0),count:width*高度),计数:imagesCount)对于columnin0..forrowin0..letoffset=row*width+column//3for_in0...1{letfactor=Double.random(in:0..<1)+2*(Double(column)/Double(width))letindex=Int(floor(Double(imagesCount)*(factor/3)))framePixels[index][offset]=pixelBuffer[offset]}}}//4forframeinframePixels{letdata=UnsafeMutablePointer(mutating:frame)guardletcontext=CGContext(data:数据,宽度:宽度,高度:高度,bitsPerComponent:bitsPerComponent,bytesPerRow:bytesPerRow,空间:colorSpace,bitmapInfo:bitmapInfo)其他{继续}result.append(UIImage(cgImage:context.makeImage()!,scale:image.scale,orientation:image.imageOrientation))}returnresult}}//1:根据指定格式创建位图上下文,然后绘制输入图像,获取其像素数据//2:创建像素的二维数组,遍历输入图像每随机分配到数组32个元素之一的相同位置的随机方法有点特殊,原始图像左边的像素只会分配到前几个images,而原图右边的像素只会被赋给后面几张图。图4上半部分是原始图片,下半部分是32张图片依次分配像素后的显示效果//3:这里通过循环两次,分配了两次像素。也许谷歌认为只有一次分配会导致像素稀疏。我个人认为在移动端,只需要做一次。//4:创建32张图片返回添加动画。Google的实现是将canvas中CSS的transform属性设置为rotate(deg)translate(px,px)rotate(deg),数值随机生成。如果你对CSS动画不熟悉,你会认为在iOS中只需要添加三个CABasicAnimations,然后将它们添加到AnimationGroup中即可,但其实并没有那么简单……因为CSS中后面的transform函数transform是基于在先前变换后的新变换坐标系上。如果一张图片的动画风格是这样的:rotate(90deg)translate(0px,100px)rotate(-90deg)直觉告诉我应该旋转并向下移动100px,但是CSS中的元素移动是这样的:5变换CSS中的多值动画,旋转和平移,决定最终的位置和轨迹。至于第二个rotate函数,它只是叠加了rotate的值作为最终的旋转弧度,恰好为0,表示不旋转。那么如何在iOS中实现类似的运动轨迹呢?可以使用UIBezierPath,CAKeyframeAnimation属性path可以指定这个UIBezierPath作为动画的运动轨迹。确定起点和实际终点作为贝塞尔曲线的起点和终点,那么如何确定控制点呢?似乎“预期”的终点(下图中的(0,-1))可以作为控制点。图6.以“预期”终点为控制点的贝塞尔曲线看起来与CSS中的运动轨迹相似扩展问题文中描述的方法生成的贝塞尔曲线是否与CSS中的动画轨迹完全相同??现在可以对视图进行动画处理了:)letradian1=Double.pi/12*Double.random(in:-0.5..<0.5)letradian2=Double.pi/12*Double.random(in:-0.5..<0.5)letrandom=Double.pi*2*Double.random(in:-0.5..<0.5)lettransX=60*cos(random)lettransY=30*sin(random)//1://x'=x*cos(rad)-y*sin(rad)//y'=y*cos(rad)+x*sin(rad)letrealTransX=transX*cos(radian1)-transY*sin(radian1)letrealTransY=transY*cos(radian1)+transX*sin(radian1))letrealEndPoint=CGPoint(x:centerX+realTransX,y:centerY+realTransY)letcontrolPoint=CGPoint(x:centerX+transX,y:centerY+transY)//2:letmovePath=UIBezierPath()movePath.move(to:layer.位置)movePath.addQuadCurve(to:realEndPoint,controlPoint:controlPoint)letmoveAnimation=CAKeyframeAnimation(keyPath:"position")移动Animation.path=movePath.cgPathmoveAnimation.calculationMode=.paced//3:letrotateAnimation=CABasicAnimation(keyPath:"transform.rotation")rotateAnimation.toValue=radian1+radian2letfadeOutAnimation=CABasicAnimation(keyPath:"opacity")fadeOutAnimation.toValueGroup=0.0letan=CAAnimationGroup()animationGroup.animations=[moveAnimation,rotateAnimation,fadeOutAnimation]animationGroup.duration=1//4:animationGroup.beginTime=CACurrentMediaTime()+1.35*Double(i)/Double(imagesCount)animationGroup.isRemovedOnCompletion=falseanimationGroup.fillMode=.forwardslayer.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的动画延迟都在这里开始和结束,更复杂的teGoogleThanosegg中的技术点已经实现。感兴趣的可以通过文首链接查看完整代码(包括音效和恢复动画)。可以尝试将打磨图片的数量从32张增加到更多,效果越好,内存消耗越多:-D。