不断涌出的爱意,使用SwiftUI搭建一个爱心粒子动画~
项目背景
近期某音上的粒子特效很火,一个中心点不断涌出各种颜色的粒子,形成一种炫彩缤纷的效果。
搜了网上很多资料,大多数都是AE的实现的动画,那在App
上能不能实现这个效果呢?
说干就干。
项目搭建
首先,创建一个新的SwiftUI
项目,命名为ParticleEffects
。
粒子运动
为了实现粒子动画效果,我们这里使用到的是GeometryEffect
几何效果函数,它可以实现Animatable
和ViewModifier
这两个协议,来模拟关键帧动画。
// 粒子动画 struct ParticleMotion: GeometryEffect { func effectValue(size: CGSize) -> ProjectionTransform { <#code#> } }
上述代码是GeometryEffect
在SwiftUI
中的应用,当我们创建一个结构体ParticleMotion
遵循GeometryEffect
协议时,SwiftUI
会自动帮助我们补全该协议所涵盖的必要代码。
我们可以看到在ParticleMotion
结构体中,SwiftUI
补充了一个方法effectValue
,它传入一个CGSize
类型的size
参数,返回ProjectionTransform
投影变换效果。
在计算机图形学中,投影变换是把3D几何体转换成一种可作为二维图像渲染的方法。
然后我们声明几个必要变量来实现粒子运动动画,示例:
var time: Double let v0: Double = Double.random(in: 40...80) let alpha: Double = Double.random(in: 0.0 ..< 2 * Double.pi) let g = 15 * 9.81 var animatableData: Double { get { time } set { time = newValue } }
粒子效果我们可以当作一种某一个视图随着重力场不断做布朗运动,简单来说就是做随机运动。
因此我们声明了一个Double
类型的参数time
来作为时间,声明一个Double
类型的常量v0
作为粒子的随机初始速度,声明了一个Double
类型的常量alpha
作为粒子的随机投射角度,声明了一个常量g
来作为重力,最后通过get
和set
说明计算属性time
是可读写的。
通过上面声明好的参数,我们来实现投影变换运动,示例:
func effectValue(size: CGSize) -> ProjectionTransform { let dx = v0 * time * cos(alpha) let dy = v0 * sin(alpha) * time - 0.5 * g * time * time let affineTransform = CGAffineTransform(translationX: CGFloat(dx), y: CGFloat(-dy)) return ProjectionTransform(affineTransform) }
上述代码中使用到的数学公式这里简单说明下,我们使用投影变换中的affineTransform
二维平面变换,这里的translationX
参数代表X轴平移系数为【随机的初始速度*时间*随机投射角度余弦角
】,Y轴
平移系数为【随机初始速度*随机投射角度正弦角*时间-0.5倍重力*时间平方
】。
粒子运动主要使用的原理是随机的连续位置形变,来模拟重力场下的布朗运动。
粒子效果
完成粒子运动方法后,我们来完成粒子视图,首先我们需要使用到ViewModifier
属性修饰符来实现样式粒子效果,然后将ViewModifier
属性修饰符赋予视图,示例:
//粒子视图 struct ParticleEffectView: ViewModifier { func body(content: Content) -> some View { <#code#> } }
上述代码中,我们声明了一个结构体ParticleEffectView
遵循ViewModifier
协议。
然后创建了一个标准视图用来构建视图样式效果,我们还需要声明几个变量作为前期准备,示例:
let count: Int let duration: Double = 2.0 @State var time: Double = 0.0
上述代码中,我们声明了一个Int
类型常量count
代表粒子数量,声明了一个Double
类型的常量duration来代表粒子持续时间,声明了一个Double
类型的变量time
代表时间。
然后我们在粒子视图中构建样式,示例:
func body(content: Content) -> some View { let animation = Animation.linear(duration: duration).repeatForever(autoreverses: false) ZStack { ForEach(0 ..< count, id: \.self) { index in content .scaleEffect(CGFloat((duration - self.time) / duration)) .modifier(ParticleMotion(time: self.time)) .opacity((duration - self.time) / duration) .animation(animation.delay(Double.random(in: 0 ..< self.duration))) .blendMode(.plusLighter) } .onAppear { withAnimation { self.time = duration } } } }
上述代码中,我们使用ForEach
循环根据粒子数量count
循环创建粒子,然后设置了粒子的scaleEffect
缩放效果,设置粒子的modifier
修饰为之前构建好的ParticleMotion
粒子运动,使用opacity
修饰符设置粒子的透明度,使用animation
修饰符设置了粒子的延迟动画,使用blendMode
修饰符设置了粒子的进行叠加图像计算。
粒子视图
完成上述准备后,我们来正式构建视图部分内容。示例:
struct ContentView: View { var body: some View { ZStack { Color.black Image(systemName: "heart.fill") .foregroundColor(Color.red) .font(.system(size: 60)) .modifier(ParticleEffectView(count: 100)) Image(systemName: "heart.fill") .foregroundColor(Color.green) .font(.system(size: 60)) .modifier(ParticleEffectView(count: 100)) Image(systemName: "heart.fill") .foregroundColor(Color.blue) .font(.system(size: 60)) .modifier(ParticleEffectView(count: 100)) } .edgesIgnoringSafeArea(.all) } }
上述代码中,我们构建了3个Image
图片,使用ZStack
层叠视图包裹在一起,每一个Image
图片都使用modifier
修饰符使用ParticleEffectView
粒子效果,最后整个背景颜色
填充为黑色。
项目预览
恭喜你,完成了整个项目的全部内容!
快来动手试试吧。