diff --git a/Sources/ConfettiSwiftUI/ConfettiSwiftUI.swift b/Sources/ConfettiSwiftUI/ConfettiSwiftUI.swift index 30f16b3..7acf6c3 100644 --- a/Sources/ConfettiSwiftUI/ConfettiSwiftUI.swift +++ b/Sources/ConfettiSwiftUI/ConfettiSwiftUI.swift @@ -78,8 +78,8 @@ public struct ConfettiCannon: View { closingAngle:Angle = .degrees(120), radius:CGFloat = 300, repetitions:Int = 0, - repetitionInterval:Double = 1.0 - + repetitionInterval:Double = 1.0, + realisticPhysics: Bool = false ) { self._counter = counter var shapes = [AnyView]() @@ -107,7 +107,8 @@ public struct ConfettiCannon: View { closingAngle: closingAngle, radius: radius, repetitions: repetitions, - repetitionInterval: repetitionInterval + repetitionInterval: repetitionInterval, + realisticPhysics: realisticPhysics )) } @@ -179,13 +180,45 @@ struct ConfettiView: View{ let spinDirections:[CGFloat] = [-1.0, 1.0] return spinDirections.randomElement()! } + + func getRandomExplosionTimeVariation() -> CGFloat { + CGFloat((0...999).randomElement()!) / 2100 + } + + func getAnimationDuration() -> CGFloat { + if confettiConfig.realisticPhysics { + return 0.2 + confettiConfig.explosionAnimationDuration + getRandomExplosionTimeVariation() + } else { + return confettiConfig.explosionAnimationDuration + } + } + + func getAnimation() -> Animation { + if confettiConfig.realisticPhysics { + return Animation.timingCurve(0, 1, 0, 1, duration: getAnimationDuration()) + } else { + return Animation.timingCurve(0.61, 1, 0.88, 1, duration: getAnimationDuration()) + } + } + + func getDistance() -> CGFloat { + if confettiConfig.realisticPhysics { + return pow(CGFloat.random(in: 0.01...1), 2.0/7.0) * confettiConfig.radius + } else { + return CGFloat.random(in: 0.5...1) * confettiConfig.radius + } + } + + func getDelayBeforeRainAnimation() -> TimeInterval { + confettiConfig.explosionAnimationDuration * (confettiConfig.realisticPhysics ? 0.1 : 1) + } var body: some View{ ConfettiAnimationView(shape:getShape(), color:getColor(), spinDirX: getSpinDirection(), spinDirZ: getSpinDirection()) .offset(x: location.x, y: location.y) .opacity(opacity) .onAppear(){ - withAnimation(Animation.timingCurve(0.61, 1, 0.88, 1, duration: confettiConfig.explosionAnimationDuration)) { + withAnimation(getAnimation()) { opacity = confettiConfig.opacity let randomAngle:CGFloat @@ -195,13 +228,13 @@ struct ConfettiView: View{ randomAngle = CGFloat.random(in: CGFloat(confettiConfig.openingAngle.degrees)...CGFloat(confettiConfig.closingAngle.degrees + 360)).truncatingRemainder(dividingBy: 360) } - let distance = CGFloat.random(in: 0.5...1) * confettiConfig.radius + let distance = getDistance() location.x = distance * cos(deg2rad(randomAngle)) location.y = -distance * sin(deg2rad(randomAngle)) } - DispatchQueue.main.asyncAfter(deadline: .now() + confettiConfig.explosionAnimationDuration) { + DispatchQueue.main.asyncAfter(deadline: .now() + getDelayBeforeRainAnimation()) { withAnimation(Animation.timingCurve(0.12, 0, 0.39, 0, duration: confettiConfig.rainAnimationDuration)) { location.y += confettiConfig.rainHeight opacity = confettiConfig.fadesOut ? 0 : confettiConfig.opacity @@ -225,9 +258,9 @@ struct ConfettiAnimationView: View { @State var move = false - @State var xSpeed:Double = Double.random(in: 1...2) + @State var xSpeed:Double = Double.random(in: 1.001...2.001) - @State var zSpeed = Double.random(in: 1...2) + @State var zSpeed = Double.random(in: 1.001...2.001) @State var anchor = CGFloat.random(in: 0...1).rounded() var body: some View { @@ -247,7 +280,7 @@ struct ConfettiAnimationView: View { } class ConfettiConfig: ObservableObject { - internal init(num: Int, shapes: [AnyView], colors: [Color], confettiSize: CGFloat, rainHeight: CGFloat, fadesOut: Bool, opacity: Double, openingAngle:Angle, closingAngle:Angle, radius:CGFloat, repetitions:Int, repetitionInterval:Double) { + internal init(num: Int, shapes: [AnyView], colors: [Color], confettiSize: CGFloat, rainHeight: CGFloat, fadesOut: Bool, opacity: Double, openingAngle:Angle, closingAngle:Angle, radius:CGFloat, repetitions:Int, repetitionInterval:Double, realisticPhysics:Bool) { self.num = num self.shapes = shapes self.colors = colors @@ -260,7 +293,8 @@ class ConfettiConfig: ObservableObject { self.radius = radius self.repetitions = repetitions self.repetitionInterval = repetitionInterval - self.explosionAnimationDuration = Double(radius / 1500) + self.realisticPhysics = realisticPhysics + self.explosionAnimationDuration = Double(radius / (realisticPhysics ? 1400 : 1500)) self.rainAnimationDuration = Double((rainHeight + radius) / 200) } @@ -276,6 +310,7 @@ class ConfettiConfig: ObservableObject { @Published var radius:CGFloat @Published var repetitions:Int @Published var repetitionInterval:Double + @Published var realisticPhysics:Bool @Published var explosionAnimationDuration:Double @Published var rainAnimationDuration:Double